From b3bf66fccece82ff3d772b7921ff6c31bd7cf5b9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 12 May 2026 11:39:46 -0400 Subject: [PATCH] feat(notebooks): add append-only edit command Adds `pup notebooks edit --file ` which appends cells to an existing notebook without clobbering existing content. Internally does a read-modify-write: fetches the current notebook, appends the cells array from --file, then writes back via the existing update endpoint. The --file should contain a JSON array of cell objects (not the full notebook body). This is distinct from `notebooks update` which does a full replace. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/notebooks.rs | 44 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 14 ++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/commands/notebooks.rs b/src/commands/notebooks.rs index e61f1ff3..9b80de7a 100644 --- a/src/commands/notebooks.rs +++ b/src/commands/notebooks.rs @@ -53,6 +53,50 @@ pub async fn update(cfg: &Config, notebook_id: i64, file: &str) -> Result<()> { formatter::output(cfg, &resp) } +/// Append-only update: fetches the current notebook, appends cells from +/// `file` (an array of cell objects), then writes the full modified notebook back. +pub async fn edit(cfg: &Config, notebook_id: i64, file: &str) -> Result<()> { + let api = crate::make_api!(NotebooksAPI, cfg); + + // Fetch current notebook so we can append without clobbering existing cells. + let current = api + .get_notebook(notebook_id) + .await + .map_err(|e| anyhow::anyhow!("failed to fetch notebook {notebook_id}: {e:?}"))?; + + // Serialize current notebook to Value so we can manipulate cells generically. + let mut nb: serde_json::Value = serde_json::to_value(¤t) + .map_err(|e| anyhow::anyhow!("failed to serialize notebook: {e:?}"))?; + + // Read the new cells to append from the file (expected: array of cell objects). + let new_cells: serde_json::Value = util::read_json_file(file)?; + let new_cells_arr = new_cells + .as_array() + .ok_or_else(|| anyhow::anyhow!("--file must contain a JSON array of cell objects"))?; + + // Append new cells to the existing cells array. + let cells = nb + .pointer_mut("/data/attributes/cells") + .and_then(|v| v.as_array_mut()) + .ok_or_else(|| anyhow::anyhow!("could not locate cells array in notebook response"))?; + cells.extend(new_cells_arr.iter().cloned()); + + // Write back via the typed update endpoint. + let update_body: NotebookUpdateRequest = serde_json::from_value( + nb.get("data") + .cloned() + .map(|data| serde_json::json!({ "data": data })) + .unwrap_or(nb.clone()), + ) + .map_err(|e| anyhow::anyhow!("failed to build update request: {e:?}"))?; + + let resp = api + .update_notebook(notebook_id, update_body) + .await + .map_err(|e| anyhow::anyhow!("failed to edit notebook: {e:?}"))?; + formatter::output(cfg, &resp) +} + #[cfg(test)] mod tests { diff --git a/src/main.rs b/src/main.rs index e40bf27e..8e6c2c2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5644,12 +5644,21 @@ enum NotebookActions { #[arg(long, help = "JSON file with notebook data (required)")] file: String, }, - /// Update a notebook + /// Update a notebook (full replace) Update { notebook_id: i64, #[arg(long, help = "JSON file with notebook data (required)")] file: String, }, + /// Append cells to an existing notebook (reads current notebook first, then appends) + Edit { + notebook_id: i64, + #[arg( + long, + help = "JSON file containing an array of cell objects to append (required)" + )] + file: String, + }, /// Delete a notebook Delete { notebook_id: i64 }, } @@ -12041,6 +12050,9 @@ async fn main_inner() -> anyhow::Result<()> { NotebookActions::Update { notebook_id, file } => { commands::notebooks::update(&cfg, notebook_id, &file).await?; } + NotebookActions::Edit { notebook_id, file } => { + commands::notebooks::edit(&cfg, notebook_id, &file).await?; + } NotebookActions::Delete { notebook_id } => { commands::notebooks::delete(&cfg, notebook_id).await?; }