From 3c5799bd469b5564ead5ab90960724a5d019ee5b Mon Sep 17 00:00:00 2001 From: "Felix D." <24978665+felix-exon@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:25:53 +0100 Subject: [PATCH 1/2] fix(page): merge append/prepend content client-side --- src/commands/page/update.ts | 21 ++++++++-- test/page-commands.test.ts | 83 +++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 test/page-commands.test.ts diff --git a/src/commands/page/update.ts b/src/commands/page/update.ts index cafe48a..53d3e73 100644 --- a/src/commands/page/update.ts +++ b/src/commands/page/update.ts @@ -1,5 +1,7 @@ import { getFlagBoolean, getFlagString } from "../../core/args.js"; +import { CliUsageError } from "../../core/errors.js"; import { parseNullableString, readTextInput } from "../../core/utils.js"; +import type { OutlineDocument } from "../../types/outline.js"; import type { CommandContext, CommandExecution } from "./shared.js"; import { getDocumentId, maybeCheckUpdatedAtGuard } from "./shared.js"; @@ -34,7 +36,15 @@ async function runPageUpdateWithMode(ctx: CommandContext, mode: EditMode): Promi const done = getFlagBoolean(ctx.args, "done"); if (title !== undefined) request.title = title; - if (text !== undefined) request.text = text; + if (mode === "replace") { + if (text !== undefined) request.text = text; + } else { + if (text === undefined) { + throw new CliUsageError(`Missing text input for page ${mode}. Use --text, --file, or --stdin.`); + } + const currentText = await getCurrentDocumentText(ctx, id); + request.text = mode === "append" ? `${currentText}${text}` : `${text}${currentText}`; + } if (icon !== undefined) request.icon = icon; if (color !== undefined) request.color = color; if (collectionId !== undefined) request.collectionId = collectionId; @@ -44,9 +54,7 @@ async function runPageUpdateWithMode(ctx: CommandContext, mode: EditMode): Promi if (publish !== undefined) request.publish = publish; if (done !== undefined) request.done = done; - if (mode !== "replace") { - request.editMode = mode; - } else { + if (mode === "replace") { const explicitEditMode = getFlagString(ctx.args, "edit-mode"); if (explicitEditMode) request.editMode = explicitEditMode; } @@ -55,3 +63,8 @@ async function runPageUpdateWithMode(ctx: CommandContext, mode: EditMode): Promi return { method: "documents.update", request, response }; } +async function getCurrentDocumentText(ctx: CommandContext, id: string): Promise { + const info = await ctx.client.post("documents.info", { id }); + const text = info.data?.text; + return typeof text === "string" ? text : ""; +} diff --git a/test/page-commands.test.ts b/test/page-commands.test.ts new file mode 100644 index 0000000..98e2ffe --- /dev/null +++ b/test/page-commands.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, test } from "bun:test"; +import { parseArgv } from "../src/core/args.js"; +import { CliUsageError } from "../src/core/errors.js"; +import { runPageCommand } from "../src/commands/page/index.js"; + +describe("runPageCommand append/prepend", () => { + test("append loads existing text and sends merged update", async () => { + const calls: Array<{ method: string; body: Record }> = []; + + const execution = await runPageCommand( + { + async post(method: string, body: Record) { + calls.push({ method, body }); + if (method === "documents.info") { + return { ok: true, data: { id: "doc-1", text: "Existing body.\n" } } as never; + } + return { ok: true, data: { id: "doc-1" } } as never; + }, + } as never, + parseArgv(["page", "append", "doc-1", "--text", "Appended chunk"]), + ); + + expect(calls).toEqual([ + { method: "documents.info", body: { id: "doc-1" } }, + { method: "documents.update", body: { id: "doc-1", text: "Existing body.\nAppended chunk" } }, + ]); + expect(execution.method).toBe("documents.update"); + }); + + test("prepend loads existing text and sends merged update", async () => { + const calls: Array<{ method: string; body: Record }> = []; + + const execution = await runPageCommand( + { + async post(method: string, body: Record) { + calls.push({ method, body }); + if (method === "documents.info") { + return { ok: true, data: { id: "doc-1", text: "Existing body." } } as never; + } + return { ok: true, data: { id: "doc-1" } } as never; + }, + } as never, + parseArgv(["page", "prepend", "doc-1", "--text", "Prepended chunk\n"]), + ); + + expect(calls).toEqual([ + { method: "documents.info", body: { id: "doc-1" } }, + { method: "documents.update", body: { id: "doc-1", text: "Prepended chunk\nExisting body." } }, + ]); + expect(execution.method).toBe("documents.update"); + }); + + test("append requires text input", async () => { + const client = { + async post() { + return { ok: true } as never; + }, + } as never; + + await expect(runPageCommand(client, parseArgv(["page", "append", "doc-1"]))).rejects.toBeInstanceOf( + CliUsageError, + ); + }); + + test("update with explicit edit-mode still passes mode through", async () => { + const calls: Array<{ method: string; body: Record }> = []; + + const execution = await runPageCommand( + { + async post(method: string, body: Record) { + calls.push({ method, body }); + return { ok: true, data: { id: "doc-1" } } as never; + }, + } as never, + parseArgv(["page", "update", "doc-1", "--text", "Chunk", "--edit-mode", "append"]), + ); + + expect(calls).toEqual([ + { method: "documents.update", body: { id: "doc-1", text: "Chunk", editMode: "append" } }, + ]); + expect(execution.method).toBe("documents.update"); + }); +}); From d962d33b27e5004f2c3231c979c41b2cdb76c64a Mon Sep 17 00:00:00 2001 From: Wren Date: Tue, 3 Mar 2026 17:33:55 +0000 Subject: [PATCH 2/2] style: tidy imports in page update command --- src/commands/page/update.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/page/update.ts b/src/commands/page/update.ts index 53d3e73..fb019f9 100644 --- a/src/commands/page/update.ts +++ b/src/commands/page/update.ts @@ -1,8 +1,10 @@ import { getFlagBoolean, getFlagString } from "../../core/args.js"; import { CliUsageError } from "../../core/errors.js"; import { parseNullableString, readTextInput } from "../../core/utils.js"; + import type { OutlineDocument } from "../../types/outline.js"; import type { CommandContext, CommandExecution } from "./shared.js"; + import { getDocumentId, maybeCheckUpdatedAtGuard } from "./shared.js"; export async function runPageUpdate(ctx: CommandContext): Promise {