// CLI command implementations. Each returns an exit code; all human-facing // output goes through the writer passed in (testable, no direct console coupling). import { existsSync } from "node:fs"; import { mkdir, readdir, rm as fsRm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, isAbsolute, join, relative, resolve } from "node:path"; import { bold, cyan, dim, fail, note, ok, warn } from "./colors.ts"; import { exportFileSlug, findPads, resolvePad, resolveRoot, slugify, toPosix, validateName, type Pad } from "./discovery.ts"; import { DEFAULT_TYPE, groupKey, hasManifest, isFileType, manifestPath, newManifest, orderGroupKeys, readManifest, writeManifest, type FileEntry, type FileType, } from "./manifest.ts"; import type { CommentItem } from "./comments.ts"; import type { UiSettings } from "./ui/render.ts"; export interface IO { out: (s: string) => void; err: (s: string) => void; } export const defaultIO: IO = { out: (s) => process.stdout.write(s + "\n"), err: (s) => process.stderr.write(s + "\n"), }; /** Emit a value as pretty JSON on one logical write (the shape `show ` uses). */ const emitJson = (io: IO, value: unknown) => io.out(JSON.stringify(value, null, 2)); /** scratch new --dir [--id] [--force] */ export async function cmdNew( args: { name?: string; dir?: string; id?: string; force?: boolean }, io: IO, ): Promise { if (!args.name) { fail(io, "`new` requires a .\n usage: scratch new --dir [--id ]"); return 2; } const nameErr = validateName(args.name); if (nameErr) { fail(io, nameErr); return 2; } if (!args.dir) { fail( io, "`new` requires an explicit --dir . Placement is always deliberate;\n" + ` there is no assumed location. e.g. scratch new ${slugify(args.name)} --dir _plans`, ); return 2; } const parent = resolve(args.dir); const slug = slugify(args.name); const padDir = join(parent, slug); if (await hasManifest(padDir)) { if (!args.force) { fail( io, `a scratchpad already exists at ${padDir}\n use --force to adopt/overwrite its manifest.`, ); return 1; } } await mkdir(padDir, { recursive: true }); const m = newManifest(args.name, args.id); await writeManifest(padDir, m); printOnboarding(padDir, m.name, io); return 0; } function printOnboarding(padDir: string, name: string, io: IO): void { ok(io, `scratchpad "${bold(name)}" ready`); io.out(""); io.out(` ${dim("pad dir :")} ${padDir}`); io.out(` ${dim("manifest :")} ${manifestPath(padDir)}`); io.out(""); io.out(bold("How to use this scratchpad:")); io.out(` 1. Write files directly into the pad dir using your normal tools.`); io.out(` 2. Register each file so it gets metadata + shows in the viewer:`); io.out(cyan(` scratch add "${name}" --title "..." --desc "why it exists" --type note --tag a,b`)); io.out(dim(` --type ∈ {note, snippet, output, artifact, reference} (default note); --desc is the most useful field.`)); io.out(` 3. Inspect: ${cyan(`scratch ls "${name}"`)} · ${cyan(`scratch show "${name}" `)}`); io.out(` 4. Browse : ${cyan(`scratch ui "${name}"`)} ${dim("(markdown, code highlight, mermaid; read-only)")}`); io.out(""); io.out(dim("The CLI never authors or moves content — you own the files; it tracks metadata.")); } /** scratch add [--title --desc --tag --type] */ export async function cmdAdd( args: { pad?: string; file?: string; dir?: string; title?: string; desc?: string; tag?: string; type?: string; group?: string; link?: boolean; as?: string; }, io: IO, ): Promise { if (!args.pad || !args.file) { fail(io, "usage: scratch add [--title .. --desc .. --tag a,b --type note --group ..] [--link [--as