Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions src/commands/page/update.ts
Original file line number Diff line number Diff line change
@@ -1,6 +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<CommandExecution> {
Expand Down Expand Up @@ -34,7 +38,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;
Expand All @@ -44,9 +56,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;
}
Expand All @@ -55,3 +65,8 @@ async function runPageUpdateWithMode(ctx: CommandContext, mode: EditMode): Promi
return { method: "documents.update", request, response };
}

async function getCurrentDocumentText(ctx: CommandContext, id: string): Promise<string> {
const info = await ctx.client.post<OutlineDocument>("documents.info", { id });
const text = info.data?.text;
return typeof text === "string" ? text : "";
}
83 changes: 83 additions & 0 deletions test/page-commands.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> }> = [];

const execution = await runPageCommand(
{
async post(method: string, body: Record<string, unknown>) {
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<string, unknown> }> = [];

const execution = await runPageCommand(
{
async post(method: string, body: Record<string, unknown>) {
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<string, unknown> }> = [];

const execution = await runPageCommand(
{
async post(method: string, body: Record<string, unknown>) {
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");
});
});