feat(cli): add init workspace command (#36 Phase 1)
- Add cmd-init.ts with cmdInitWorkspace and stub cmdInitTemplate - Wire init subcommands into cli-dispatch.ts - Generate monorepo skeleton: package.json (bun workspace), biome.json, tsconfig.json, AGENTS.md placeholder, README.md, templates/, workflows/ - Error on existing directory - Add init-workspace.test.ts (all passing) Testing: #46
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { mkdir, readFile, rm } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { formatCliUsage, runCli } from "../src/cli-dispatch.js";
|
||||||
|
import { cmdInitTemplate, cmdInitWorkspace } from "../src/cmd-init.js";
|
||||||
|
import { pathExists } from "../src/fs-utils.js";
|
||||||
|
|
||||||
|
describe("init workspace", () => {
|
||||||
|
let parent: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
parent = join(tmpdir(), `wf-init-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||||
|
await mkdir(parent, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rm(parent, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates expected files and directories", async () => {
|
||||||
|
const created = await cmdInitWorkspace(parent, "my-workflows");
|
||||||
|
expect(created.ok).toBe(true);
|
||||||
|
if (!created.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = created.value.rootPath;
|
||||||
|
expect(await pathExists(join(root, "package.json"))).toBe(true);
|
||||||
|
expect(await pathExists(join(root, "biome.json"))).toBe(true);
|
||||||
|
expect(await pathExists(join(root, "tsconfig.json"))).toBe(true);
|
||||||
|
expect(await pathExists(join(root, "AGENTS.md"))).toBe(true);
|
||||||
|
expect(await pathExists(join(root, "README.md"))).toBe(true);
|
||||||
|
expect(await pathExists(join(root, "templates"))).toBe(true);
|
||||||
|
expect(await pathExists(join(root, "templates", ".gitkeep"))).toBe(true);
|
||||||
|
expect(await pathExists(join(root, "workflows", "package.json"))).toBe(true);
|
||||||
|
|
||||||
|
const rootPkg = JSON.parse(await readFile(join(root, "package.json"), "utf8")) as {
|
||||||
|
workspaces: string[];
|
||||||
|
};
|
||||||
|
expect(rootPkg.workspaces).toEqual(["templates/*", "workflows"]);
|
||||||
|
|
||||||
|
const wfPkg = JSON.parse(await readFile(join(root, "workflows", "package.json"), "utf8")) as {
|
||||||
|
type: string;
|
||||||
|
dependencies: Record<string, string>;
|
||||||
|
};
|
||||||
|
expect(wfPkg.type).toBe("module");
|
||||||
|
expect(wfPkg.dependencies["@uncaged/workflow"]).toBeDefined();
|
||||||
|
expect(wfPkg.dependencies.zod).toBeDefined();
|
||||||
|
|
||||||
|
const tsconfig = JSON.parse(await readFile(join(root, "tsconfig.json"), "utf8")) as {
|
||||||
|
compilerOptions: { strict: boolean; module: string; target: string };
|
||||||
|
};
|
||||||
|
expect(tsconfig.compilerOptions.strict).toBe(true);
|
||||||
|
expect(tsconfig.compilerOptions.module).toBe("ESNext");
|
||||||
|
expect(tsconfig.compilerOptions.target).toBe("ESNext");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("errors when directory already exists", async () => {
|
||||||
|
const first = await cmdInitWorkspace(parent, "dup");
|
||||||
|
expect(first.ok).toBe(true);
|
||||||
|
|
||||||
|
const second = await cmdInitWorkspace(parent, "dup");
|
||||||
|
expect(second.ok).toBe(false);
|
||||||
|
if (!second.ok) {
|
||||||
|
expect(second.error).toContain("already exists");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("errors on invalid workspace name", async () => {
|
||||||
|
const slash = await cmdInitWorkspace(parent, "a/b");
|
||||||
|
expect(slash.ok).toBe(false);
|
||||||
|
|
||||||
|
const dots = await cmdInitWorkspace(parent, "..");
|
||||||
|
expect(dots.ok).toBe(false);
|
||||||
|
|
||||||
|
const empty = await cmdInitWorkspace(parent, "");
|
||||||
|
expect(empty.ok).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("usage lists init subcommands", () => {
|
||||||
|
const u = formatCliUsage();
|
||||||
|
expect(u).toContain("uncaged-workflow init workspace <name>");
|
||||||
|
expect(u).toContain("uncaged-workflow init template <name>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("init template command is stubbed", () => {
|
||||||
|
const r = cmdInitTemplate(parent, "x");
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
if (!r.ok) {
|
||||||
|
expect(r.error).toBe("not implemented yet");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("runCli rejects unknown init subcommand", async () => {
|
||||||
|
const code = await runCli(join(parent, "_storage"), ["init", "bogus", "name"]);
|
||||||
|
expect(code).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial("runCli init workspace uses cwd", async () => {
|
||||||
|
const prev = process.cwd();
|
||||||
|
try {
|
||||||
|
process.chdir(parent);
|
||||||
|
const code = await runCli(join(parent, "_storage"), ["init", "workspace", "from-cli"]);
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(await pathExists(join(parent, "from-cli", "workflows", "package.json"))).toBe(true);
|
||||||
|
} finally {
|
||||||
|
process.chdir(prev);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("runCli init template exits with error", async () => {
|
||||||
|
const code = await runCli(join(parent, "_storage"), ["init", "template", "t"]);
|
||||||
|
expect(code).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "./cmd-cas.js";
|
|||||||
import { cmdFork, parseForkArgv } from "./cmd-fork.js";
|
import { cmdFork, parseForkArgv } from "./cmd-fork.js";
|
||||||
import { cmdGc } from "./cmd-gc.js";
|
import { cmdGc } from "./cmd-gc.js";
|
||||||
import { cmdHistory } from "./cmd-history.js";
|
import { cmdHistory } from "./cmd-history.js";
|
||||||
|
import { cmdInitTemplate, cmdInitWorkspace } from "./cmd-init.js";
|
||||||
import { cmdKill } from "./cmd-kill.js";
|
import { cmdKill } from "./cmd-kill.js";
|
||||||
import { cmdList, formatListLines } from "./cmd-list.js";
|
import { cmdList, formatListLines } from "./cmd-list.js";
|
||||||
import { cmdPause } from "./cmd-pause.js";
|
import { cmdPause } from "./cmd-pause.js";
|
||||||
@@ -17,7 +18,7 @@ import { cmdThreadRemove, cmdThreadShow } from "./cmd-thread.js";
|
|||||||
import { cmdThreads } from "./cmd-threads.js";
|
import { cmdThreads } from "./cmd-threads.js";
|
||||||
import { parseRunArgv } from "./run-argv.js";
|
import { parseRunArgv } from "./run-argv.js";
|
||||||
|
|
||||||
function usage(): string {
|
export function formatCliUsage(): string {
|
||||||
return [
|
return [
|
||||||
"Usage:",
|
"Usage:",
|
||||||
" uncaged-workflow add <name> <file.esm.js> [--types <path>]",
|
" uncaged-workflow add <name> <file.esm.js> [--types <path>]",
|
||||||
@@ -40,13 +41,46 @@ function usage(): string {
|
|||||||
" uncaged-workflow cas put <thread-id> <content>",
|
" uncaged-workflow cas put <thread-id> <content>",
|
||||||
" uncaged-workflow cas list <thread-id>",
|
" uncaged-workflow cas list <thread-id>",
|
||||||
" uncaged-workflow cas rm <thread-id> <hash>",
|
" uncaged-workflow cas rm <thread-id> <hash>",
|
||||||
|
" uncaged-workflow init workspace <name>",
|
||||||
|
" uncaged-workflow init template <name>",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dispatchInit(_storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const sub = argv[0];
|
||||||
|
const name = argv[1];
|
||||||
|
if (sub === undefined || name === undefined || argv.length > 2) {
|
||||||
|
printCliError(`${formatCliUsage()}\n\nerror: init requires workspace|template <name>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === "workspace") {
|
||||||
|
const result = await cmdInitWorkspace(process.cwd(), name);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(`initialized workflow workspace at ${result.value.rootPath}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === "template") {
|
||||||
|
const result = cmdInitTemplate(process.cwd(), name);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
printCliError(`${formatCliUsage()}\n\nerror: unknown init subcommand: ${sub}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
async function dispatchAdd(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchAdd(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const parsed = parseAddArgv(argv);
|
const parsed = parseAddArgv(argv);
|
||||||
if (!parsed.ok) {
|
if (!parsed.ok) {
|
||||||
printCliError(`${usage()}\n\nerror: ${parsed.error}`);
|
printCliError(`${formatCliUsage()}\n\nerror: ${parsed.error}`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdAdd(storageRoot, parsed.value);
|
const result = await cmdAdd(storageRoot, parsed.value);
|
||||||
@@ -63,7 +97,7 @@ async function dispatchAdd(storageRoot: string, argv: string[]): Promise<number>
|
|||||||
|
|
||||||
async function dispatchList(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchList(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
if (argv.length > 0) {
|
if (argv.length > 0) {
|
||||||
printCliError(`${usage()}\n\nerror: list takes no arguments`);
|
printCliError(`${formatCliUsage()}\n\nerror: list takes no arguments`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdList(storageRoot);
|
const result = await cmdList(storageRoot);
|
||||||
@@ -80,7 +114,7 @@ async function dispatchList(storageRoot: string, argv: string[]): Promise<number
|
|||||||
async function dispatchShow(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchShow(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const name = argv[0];
|
const name = argv[0];
|
||||||
if (name === undefined || argv.length > 1) {
|
if (name === undefined || argv.length > 1) {
|
||||||
printCliError(`${usage()}\n\nerror: show requires <name>`);
|
printCliError(`${formatCliUsage()}\n\nerror: show requires <name>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdShow(storageRoot, name);
|
const result = await cmdShow(storageRoot, name);
|
||||||
@@ -95,7 +129,7 @@ async function dispatchShow(storageRoot: string, argv: string[]): Promise<number
|
|||||||
async function dispatchRemove(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchRemove(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const name = argv[0];
|
const name = argv[0];
|
||||||
if (name === undefined || argv.length > 1) {
|
if (name === undefined || argv.length > 1) {
|
||||||
printCliError(`${usage()}\n\nerror: remove requires <name>`);
|
printCliError(`${formatCliUsage()}\n\nerror: remove requires <name>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdRemove(storageRoot, name);
|
const result = await cmdRemove(storageRoot, name);
|
||||||
@@ -110,7 +144,7 @@ async function dispatchRemove(storageRoot: string, argv: string[]): Promise<numb
|
|||||||
async function dispatchRun(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchRun(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const parsed = parseRunArgv(argv);
|
const parsed = parseRunArgv(argv);
|
||||||
if (!parsed.ok) {
|
if (!parsed.ok) {
|
||||||
printCliError(`${usage()}\n\nerror: ${parsed.error}`);
|
printCliError(`${formatCliUsage()}\n\nerror: ${parsed.error}`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +165,7 @@ async function dispatchRun(storageRoot: string, argv: string[]): Promise<number>
|
|||||||
|
|
||||||
async function dispatchPs(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchPs(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
if (argv.length > 0) {
|
if (argv.length > 0) {
|
||||||
printCliError(`${usage()}\n\nerror: ps takes no arguments`);
|
printCliError(`${formatCliUsage()}\n\nerror: ps takes no arguments`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
for (const line of await cmdPs(storageRoot)) {
|
for (const line of await cmdPs(storageRoot)) {
|
||||||
@@ -143,7 +177,7 @@ async function dispatchPs(storageRoot: string, argv: string[]): Promise<number>
|
|||||||
async function dispatchKill(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchKill(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const threadId = argv[0];
|
const threadId = argv[0];
|
||||||
if (threadId === undefined || argv.length > 1) {
|
if (threadId === undefined || argv.length > 1) {
|
||||||
printCliError(`${usage()}\n\nerror: kill requires <thread-id>`);
|
printCliError(`${formatCliUsage()}\n\nerror: kill requires <thread-id>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdKill(storageRoot, threadId);
|
const result = await cmdKill(storageRoot, threadId);
|
||||||
@@ -158,7 +192,7 @@ async function dispatchKill(storageRoot: string, argv: string[]): Promise<number
|
|||||||
async function dispatchHistory(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchHistory(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const name = argv[0];
|
const name = argv[0];
|
||||||
if (name === undefined || argv.length > 1) {
|
if (name === undefined || argv.length > 1) {
|
||||||
printCliError(`${usage()}\n\nerror: history requires <name>`);
|
printCliError(`${formatCliUsage()}\n\nerror: history requires <name>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdHistory(storageRoot, name);
|
const result = await cmdHistory(storageRoot, name);
|
||||||
@@ -175,7 +209,7 @@ async function dispatchHistory(storageRoot: string, argv: string[]): Promise<num
|
|||||||
async function dispatchRollback(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchRollback(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const name = argv[0];
|
const name = argv[0];
|
||||||
if (name === undefined || argv.length > 2) {
|
if (name === undefined || argv.length > 2) {
|
||||||
printCliError(`${usage()}\n\nerror: rollback requires <name> [hash]`);
|
printCliError(`${formatCliUsage()}\n\nerror: rollback requires <name> [hash]`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const hashArg = argv[1];
|
const hashArg = argv[1];
|
||||||
@@ -191,7 +225,7 @@ async function dispatchRollback(storageRoot: string, argv: string[]): Promise<nu
|
|||||||
async function dispatchPause(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchPause(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const threadId = argv[0];
|
const threadId = argv[0];
|
||||||
if (threadId === undefined || argv.length > 1) {
|
if (threadId === undefined || argv.length > 1) {
|
||||||
printCliError(`${usage()}\n\nerror: pause requires <thread-id>`);
|
printCliError(`${formatCliUsage()}\n\nerror: pause requires <thread-id>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdPause(storageRoot, threadId);
|
const result = await cmdPause(storageRoot, threadId);
|
||||||
@@ -206,7 +240,7 @@ async function dispatchPause(storageRoot: string, argv: string[]): Promise<numbe
|
|||||||
async function dispatchResume(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchResume(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const threadId = argv[0];
|
const threadId = argv[0];
|
||||||
if (threadId === undefined || argv.length > 1) {
|
if (threadId === undefined || argv.length > 1) {
|
||||||
printCliError(`${usage()}\n\nerror: resume requires <thread-id>`);
|
printCliError(`${formatCliUsage()}\n\nerror: resume requires <thread-id>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdResume(storageRoot, threadId);
|
const result = await cmdResume(storageRoot, threadId);
|
||||||
@@ -233,7 +267,7 @@ async function dispatchThreads(storageRoot: string, argv: string[]): Promise<num
|
|||||||
async function dispatchThread(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchThread(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const id = argv[0];
|
const id = argv[0];
|
||||||
if (id === undefined || argv.length > 1) {
|
if (id === undefined || argv.length > 1) {
|
||||||
printCliError(`${usage()}\n\nerror: thread requires <id>`);
|
printCliError(`${formatCliUsage()}\n\nerror: thread requires <id>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdThreadShow(storageRoot, id);
|
const result = await cmdThreadShow(storageRoot, id);
|
||||||
@@ -248,7 +282,7 @@ async function dispatchThread(storageRoot: string, argv: string[]): Promise<numb
|
|||||||
async function dispatchThreadRm(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchThreadRm(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const id = argv[0];
|
const id = argv[0];
|
||||||
if (id === undefined || argv.length > 1) {
|
if (id === undefined || argv.length > 1) {
|
||||||
printCliError(`${usage()}\n\nerror: thread rm requires <id>`);
|
printCliError(`${formatCliUsage()}\n\nerror: thread rm requires <id>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdThreadRemove(storageRoot, id);
|
const result = await cmdThreadRemove(storageRoot, id);
|
||||||
@@ -270,7 +304,7 @@ async function dispatchThreadBranch(storageRoot: string, rest: string[]): Promis
|
|||||||
|
|
||||||
async function dispatchGc(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchGc(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
if (argv.length > 0) {
|
if (argv.length > 0) {
|
||||||
printCliError(`${usage()}\n\nerror: gc takes no arguments`);
|
printCliError(`${formatCliUsage()}\n\nerror: gc takes no arguments`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdGc(storageRoot);
|
const result = await cmdGc(storageRoot);
|
||||||
@@ -288,7 +322,7 @@ async function dispatchGc(storageRoot: string, argv: string[]): Promise<number>
|
|||||||
async function dispatchFork(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchFork(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const parsed = parseForkArgv(argv);
|
const parsed = parseForkArgv(argv);
|
||||||
if (!parsed.ok) {
|
if (!parsed.ok) {
|
||||||
printCliError(`${usage()}\n\nerror: ${parsed.error}`);
|
printCliError(`${formatCliUsage()}\n\nerror: ${parsed.error}`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdFork(storageRoot, parsed.value.threadId, parsed.value.fromRole);
|
const result = await cmdFork(storageRoot, parsed.value.threadId, parsed.value.fromRole);
|
||||||
@@ -304,7 +338,7 @@ async function dispatchCasGet(storageRoot: string, rest: string[]): Promise<numb
|
|||||||
const threadId = rest[0];
|
const threadId = rest[0];
|
||||||
const hash = rest[1];
|
const hash = rest[1];
|
||||||
if (threadId === undefined || hash === undefined || rest.length > 2) {
|
if (threadId === undefined || hash === undefined || rest.length > 2) {
|
||||||
printCliError(`${usage()}\n\nerror: cas get requires <thread-id> <hash>`);
|
printCliError(`${formatCliUsage()}\n\nerror: cas get requires <thread-id> <hash>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdCasGet(storageRoot, threadId, hash);
|
const result = await cmdCasGet(storageRoot, threadId, hash);
|
||||||
@@ -320,7 +354,7 @@ async function dispatchCasPut(storageRoot: string, rest: string[]): Promise<numb
|
|||||||
const threadId = rest[0];
|
const threadId = rest[0];
|
||||||
const content = rest[1];
|
const content = rest[1];
|
||||||
if (threadId === undefined || content === undefined || rest.length > 2) {
|
if (threadId === undefined || content === undefined || rest.length > 2) {
|
||||||
printCliError(`${usage()}\n\nerror: cas put requires <thread-id> <content>`);
|
printCliError(`${formatCliUsage()}\n\nerror: cas put requires <thread-id> <content>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdCasPut(storageRoot, threadId, content);
|
const result = await cmdCasPut(storageRoot, threadId, content);
|
||||||
@@ -335,7 +369,7 @@ async function dispatchCasPut(storageRoot: string, rest: string[]): Promise<numb
|
|||||||
async function dispatchCasList(storageRoot: string, rest: string[]): Promise<number> {
|
async function dispatchCasList(storageRoot: string, rest: string[]): Promise<number> {
|
||||||
const threadId = rest[0];
|
const threadId = rest[0];
|
||||||
if (threadId === undefined || rest.length > 1) {
|
if (threadId === undefined || rest.length > 1) {
|
||||||
printCliError(`${usage()}\n\nerror: cas list requires <thread-id>`);
|
printCliError(`${formatCliUsage()}\n\nerror: cas list requires <thread-id>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdCasList(storageRoot, threadId);
|
const result = await cmdCasList(storageRoot, threadId);
|
||||||
@@ -353,7 +387,7 @@ async function dispatchCasRm(storageRoot: string, rest: string[]): Promise<numbe
|
|||||||
const threadId = rest[0];
|
const threadId = rest[0];
|
||||||
const hash = rest[1];
|
const hash = rest[1];
|
||||||
if (threadId === undefined || hash === undefined || rest.length > 2) {
|
if (threadId === undefined || hash === undefined || rest.length > 2) {
|
||||||
printCliError(`${usage()}\n\nerror: cas rm requires <thread-id> <hash>`);
|
printCliError(`${formatCliUsage()}\n\nerror: cas rm requires <thread-id> <hash>`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const result = await cmdCasRm(storageRoot, threadId, hash);
|
const result = await cmdCasRm(storageRoot, threadId, hash);
|
||||||
@@ -378,12 +412,12 @@ const CAS_SUBCOMMAND_TABLE: Record<
|
|||||||
async function dispatchCas(storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchCas(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
const sub = argv[0];
|
const sub = argv[0];
|
||||||
if (sub === undefined) {
|
if (sub === undefined) {
|
||||||
printCliError(`${usage()}\n\nerror: unknown cas subcommand: (none)`);
|
printCliError(`${formatCliUsage()}\n\nerror: unknown cas subcommand: (none)`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const handler = CAS_SUBCOMMAND_TABLE[sub];
|
const handler = CAS_SUBCOMMAND_TABLE[sub];
|
||||||
if (handler === undefined) {
|
if (handler === undefined) {
|
||||||
printCliError(`${usage()}\n\nerror: unknown cas subcommand: ${sub}`);
|
printCliError(`${formatCliUsage()}\n\nerror: unknown cas subcommand: ${sub}`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return handler(storageRoot, argv.slice(1));
|
return handler(storageRoot, argv.slice(1));
|
||||||
@@ -393,6 +427,7 @@ type DispatchFn = (storageRoot: string, argv: string[]) => Promise<number>;
|
|||||||
|
|
||||||
const COMMAND_TABLE: Record<string, DispatchFn> = {
|
const COMMAND_TABLE: Record<string, DispatchFn> = {
|
||||||
add: dispatchAdd,
|
add: dispatchAdd,
|
||||||
|
init: dispatchInit,
|
||||||
list: dispatchList,
|
list: dispatchList,
|
||||||
show: dispatchShow,
|
show: dispatchShow,
|
||||||
remove: dispatchRemove,
|
remove: dispatchRemove,
|
||||||
@@ -412,18 +447,18 @@ const COMMAND_TABLE: Record<string, DispatchFn> = {
|
|||||||
|
|
||||||
export async function runCli(storageRoot: string, argv: string[]): Promise<number> {
|
export async function runCli(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
if (argv.length === 0) {
|
if (argv.length === 0) {
|
||||||
printCliError(usage());
|
printCliError(formatCliUsage());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const command = argv[0];
|
const command = argv[0];
|
||||||
if (command === undefined) {
|
if (command === undefined) {
|
||||||
printCliError(usage());
|
printCliError(formatCliUsage());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const rest = argv.slice(1);
|
const rest = argv.slice(1);
|
||||||
const dispatch = COMMAND_TABLE[command];
|
const dispatch = COMMAND_TABLE[command];
|
||||||
if (dispatch === undefined) {
|
if (dispatch === undefined) {
|
||||||
printCliError(`${usage()}\n\nerror: unknown command ${command}`);
|
printCliError(`${formatCliUsage()}\n\nerror: unknown command ${command}`);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return dispatch(storageRoot, rest);
|
return dispatch(storageRoot, rest);
|
||||||
|
|||||||
Regular → Executable
@@ -0,0 +1,160 @@
|
|||||||
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { err, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
import { pathExists } from "./fs-utils.js";
|
||||||
|
|
||||||
|
export type CmdInitWorkspaceSuccess = {
|
||||||
|
rootPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateWorkspaceSegment(name: string): Result<void, string> {
|
||||||
|
if (name.length === 0) {
|
||||||
|
return err("workspace name must not be empty");
|
||||||
|
}
|
||||||
|
if (name === "." || name === "..") {
|
||||||
|
return err("invalid workspace name");
|
||||||
|
}
|
||||||
|
if (name.includes("/") || name.includes("\\")) {
|
||||||
|
return err("workspace name must not contain path separators");
|
||||||
|
}
|
||||||
|
return ok(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rootPackageJson(workspaceName: string): string {
|
||||||
|
return `${JSON.stringify(
|
||||||
|
{
|
||||||
|
name: workspaceName,
|
||||||
|
private: true,
|
||||||
|
type: "module",
|
||||||
|
workspaces: ["templates/*", "workflows"],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function workflowsPackageJson(): string {
|
||||||
|
return `${JSON.stringify(
|
||||||
|
{
|
||||||
|
name: "workflows",
|
||||||
|
version: "0.0.0",
|
||||||
|
private: true,
|
||||||
|
type: "module",
|
||||||
|
dependencies: {
|
||||||
|
"@uncaged/workflow": "^0.1.0",
|
||||||
|
zod: "^4.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function biomeJson(): string {
|
||||||
|
return `${JSON.stringify(
|
||||||
|
{
|
||||||
|
$schema: "https://biomejs.dev/schemas/2.4.14/schema.json",
|
||||||
|
files: {
|
||||||
|
includes: ["**", "!**/node_modules", "!**/dist"],
|
||||||
|
},
|
||||||
|
formatter: {
|
||||||
|
indentWidth: 2,
|
||||||
|
},
|
||||||
|
linter: {
|
||||||
|
enabled: true,
|
||||||
|
rules: {
|
||||||
|
recommended: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tsconfigJson(): string {
|
||||||
|
return `${JSON.stringify(
|
||||||
|
{
|
||||||
|
compilerOptions: {
|
||||||
|
strict: true,
|
||||||
|
target: "ESNext",
|
||||||
|
module: "ESNext",
|
||||||
|
moduleResolution: "Bundler",
|
||||||
|
skipLibCheck: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function agentsMd(): string {
|
||||||
|
return `# AGENTS
|
||||||
|
|
||||||
|
Placeholder: coding agent instructions for this workflow workspace will be added in a later phase.
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readmeMd(workspaceName: string): string {
|
||||||
|
return `# ${workspaceName}
|
||||||
|
|
||||||
|
Local workflow development workspace (Bun monorepo).
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- \`templates/\` — reusable workflow definition packages (roles + moderator), no agent binding
|
||||||
|
- \`workflows/\` — workflow instances that bind templates to agents and export \`run\` + \`descriptor\`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
\`\`\`sh
|
||||||
|
bun install
|
||||||
|
bun run check # after you add scripts / Biome
|
||||||
|
uncaged-workflow add <name> <bundle.esm.js>
|
||||||
|
uncaged-workflow run <name>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Create this skeleton with:
|
||||||
|
|
||||||
|
\`\`\`sh
|
||||||
|
uncaged-workflow init workspace ${workspaceName}
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdInitWorkspace(
|
||||||
|
parentDir: string,
|
||||||
|
workspaceName: string,
|
||||||
|
): Promise<Result<CmdInitWorkspaceSuccess, string>> {
|
||||||
|
const validated = validateWorkspaceSegment(workspaceName);
|
||||||
|
if (!validated.ok) {
|
||||||
|
return validated;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootPath = join(parentDir, workspaceName);
|
||||||
|
if (await pathExists(rootPath)) {
|
||||||
|
return err(`directory already exists: ${rootPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await mkdir(rootPath, { recursive: false });
|
||||||
|
await mkdir(join(rootPath, "templates"), { recursive: false });
|
||||||
|
await mkdir(join(rootPath, "workflows"), { recursive: false });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
writeFile(join(rootPath, "package.json"), rootPackageJson(workspaceName), "utf8"),
|
||||||
|
writeFile(join(rootPath, "biome.json"), biomeJson(), "utf8"),
|
||||||
|
writeFile(join(rootPath, "tsconfig.json"), tsconfigJson(), "utf8"),
|
||||||
|
writeFile(join(rootPath, "AGENTS.md"), agentsMd(), "utf8"),
|
||||||
|
writeFile(join(rootPath, "README.md"), readmeMd(workspaceName), "utf8"),
|
||||||
|
writeFile(join(rootPath, "templates", ".gitkeep"), "", "utf8"),
|
||||||
|
writeFile(join(rootPath, "workflows", "package.json"), workflowsPackageJson(), "utf8"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ok({ rootPath });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cmdInitTemplate(_parentDir: string, _templateName: string): Result<void, string> {
|
||||||
|
return err("not implemented yet");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user