chore: replace hand-written xxhashjs.d.ts with @types/xxhashjs
小橘 <xiaoju@shazhou.work>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import type { ParsedAddArgv } from "../src/add-argv.js";
|
||||
|
||||
export const MINIMAL_DESCRIPTOR_YAML = `description: "fixture"
|
||||
roles: {}
|
||||
`;
|
||||
|
||||
export function addCliArgs(name: string, filePath: string): ParsedAddArgv {
|
||||
return { name, filePath, descriptorPath: null, typesPath: null };
|
||||
}
|
||||
@@ -2,9 +2,11 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow";
|
||||
|
||||
import { addCliArgs, MINIMAL_DESCRIPTOR_YAML } from "./bundle-fixture.js";
|
||||
import { cmdAdd } from "../src/cmd-add.js";
|
||||
import { cmdHistory } from "../src/cmd-history.js";
|
||||
import { cmdList, formatListLines } from "../src/cmd-list.js";
|
||||
@@ -47,8 +49,9 @@ export default async function* (input) {
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
|
||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(added.ok).toBe(true);
|
||||
|
||||
const listed = await cmdList(storageRoot);
|
||||
@@ -88,10 +91,83 @@ export default async function* (input) {
|
||||
'import x from "./local";\nexport default async function* (input) { return { returnCode: 0, summary: input.prompt }; }\n',
|
||||
"utf8",
|
||||
);
|
||||
const r = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const r = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(r.ok).toBe(false);
|
||||
});
|
||||
|
||||
test("add rejects .esm.js without companion YAML", async () => {
|
||||
const bundlePath = join(storageRoot, "solo.esm.js");
|
||||
await writeFile(
|
||||
bundlePath,
|
||||
`export default async function* (input) {
|
||||
yield { role: "x", content: input.prompt, meta: {} };
|
||||
return { returnCode: 0, summary: "ok" };
|
||||
}
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
const r = await cmdAdd(storageRoot, addCliArgs("solo", bundlePath));
|
||||
expect(r.ok).toBe(false);
|
||||
if (r.ok) {
|
||||
return;
|
||||
}
|
||||
expect(r.error).toContain("descriptor YAML not found");
|
||||
});
|
||||
|
||||
test("add from .ts builds bundle + yaml + d.ts and registers hash", async () => {
|
||||
const helloTs = fileURLToPath(new URL("../../../examples/hello-world.ts", import.meta.url));
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("hello", helloTs));
|
||||
expect(added.ok).toBe(true);
|
||||
if (!added.ok) {
|
||||
return;
|
||||
}
|
||||
const { hash } = added.value;
|
||||
const bundles = join(storageRoot, "bundles");
|
||||
const esm = await readFile(join(bundles, `${hash}.esm.js`), "utf8");
|
||||
expect(esm.length).toBeGreaterThan(100);
|
||||
const yaml = await readFile(join(bundles, `${hash}.yaml`), "utf8");
|
||||
expect(yaml).toContain("hello world");
|
||||
const dts = await readFile(join(bundles, `${hash}.d.ts`), "utf8");
|
||||
expect(dts).toContain("export type Roles");
|
||||
expect(dts).toContain("WorkflowFn");
|
||||
|
||||
const reg = await readWorkflowRegistry(storageRoot);
|
||||
expect(reg.ok).toBe(true);
|
||||
if (!reg.ok) {
|
||||
return;
|
||||
}
|
||||
const entry = getRegisteredWorkflow(reg.value, "hello");
|
||||
expect(entry).not.toBeNull();
|
||||
if (entry === null) {
|
||||
return;
|
||||
}
|
||||
expect(entry.hash).toBe(hash);
|
||||
});
|
||||
|
||||
test("add from .esm.js warns when optional .d.ts is missing", async () => {
|
||||
const bundleDir = join(storageRoot, "src");
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||
await writeFile(
|
||||
bundlePath,
|
||||
`export default async function* (input) {
|
||||
yield { role: "a", content: "x", meta: {} };
|
||||
return { returnCode: 0, summary: "x" };
|
||||
}
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(added.ok).toBe(true);
|
||||
if (!added.ok) {
|
||||
return;
|
||||
}
|
||||
expect(added.value.warnings.length).toBe(1);
|
||||
expect(added.value.warnings[0]).toContain("demo.d.ts");
|
||||
});
|
||||
|
||||
test("history lists current + prior versions sorted by time descending", async () => {
|
||||
const bundleDir = join(storageRoot, "src");
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
@@ -107,11 +183,12 @@ export default async function* (input) {
|
||||
}
|
||||
`;
|
||||
await writeFile(bundlePath, v1, "utf8");
|
||||
const add1 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(add1.ok).toBe(true);
|
||||
await new Promise((r) => setTimeout(r, 15));
|
||||
await writeFile(bundlePath, v2, "utf8");
|
||||
const add2 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(add2.ok).toBe(true);
|
||||
|
||||
const hist = await cmdHistory(storageRoot, "solve-issue");
|
||||
@@ -145,14 +222,15 @@ export default async function* (input) {
|
||||
}
|
||||
`;
|
||||
await writeFile(bundlePath, v1, "utf8");
|
||||
const add1 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(add1.ok).toBe(true);
|
||||
if (!add1.ok) {
|
||||
return;
|
||||
}
|
||||
const hash1 = add1.value.hash;
|
||||
await writeFile(bundlePath, v2, "utf8");
|
||||
const add2 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(add2.ok).toBe(true);
|
||||
if (!add2.ok) {
|
||||
return;
|
||||
@@ -189,7 +267,8 @@ export default async function* (input) {
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
const add1 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(add1.ok).toBe(true);
|
||||
await writeFile(
|
||||
bundlePath,
|
||||
@@ -200,7 +279,7 @@ export default async function* (input) {
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
const add2 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(add2.ok).toBe(true);
|
||||
|
||||
const bad = await cmdRollback(storageRoot, "solve-issue", "0000000000000");
|
||||
@@ -220,7 +299,8 @@ export default async function* (input) {
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
const add1 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(add1.ok).toBe(true);
|
||||
if (!add1.ok) {
|
||||
return;
|
||||
@@ -235,7 +315,7 @@ export default async function* (input) {
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
const add2 = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(add2.ok).toBe(true);
|
||||
if (!add2.ok) {
|
||||
return;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { addCliArgs, MINIMAL_DESCRIPTOR_YAML } from "./bundle-fixture.js";
|
||||
import { cmdAdd } from "../src/cmd-add.js";
|
||||
import { cmdFork } from "../src/cmd-fork.js";
|
||||
import { cmdRun } from "../src/cmd-run.js";
|
||||
@@ -82,8 +83,9 @@ describe("cli fork", () => {
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||
await writeFile(bundlePath, threeRoleBundleSource, "utf8");
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
|
||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(added.ok).toBe(true);
|
||||
if (!added.ok) {
|
||||
return;
|
||||
@@ -132,8 +134,9 @@ describe("cli fork", () => {
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||
await writeFile(bundlePath, threeRoleBundleSource, "utf8");
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
|
||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(added.ok).toBe(true);
|
||||
if (!added.ok) {
|
||||
return;
|
||||
@@ -183,8 +186,9 @@ describe("cli fork", () => {
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||
await writeFile(bundlePath, threeRoleBundleSource, "utf8");
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
|
||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(added.ok).toBe(true);
|
||||
if (!added.ok) {
|
||||
return;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { addCliArgs, MINIMAL_DESCRIPTOR_YAML } from "./bundle-fixture.js";
|
||||
import { cmdAdd } from "../src/cmd-add.js";
|
||||
import { cmdKill } from "../src/cmd-kill.js";
|
||||
import { cmdPause } from "../src/cmd-pause.js";
|
||||
@@ -113,8 +114,9 @@ describe("cli thread commands", () => {
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||
await writeFile(bundlePath, fastBundleSource, "utf8");
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
|
||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(added.ok).toBe(true);
|
||||
if (!added.ok) {
|
||||
return;
|
||||
@@ -174,8 +176,9 @@ describe("cli thread commands", () => {
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||
await writeFile(bundlePath, slowPlannerBundleSource, "utf8");
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
|
||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(added.ok).toBe(true);
|
||||
if (!added.ok) {
|
||||
return;
|
||||
@@ -204,8 +207,9 @@ describe("cli thread commands", () => {
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||
await writeFile(bundlePath, abortablePlannerBundleSource, "utf8");
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
|
||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(added.ok).toBe(true);
|
||||
if (!added.ok) {
|
||||
return;
|
||||
@@ -243,8 +247,9 @@ describe("cli thread commands", () => {
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||
await writeFile(bundlePath, pauseResumeBundleSource, "utf8");
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
|
||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(added.ok).toBe(true);
|
||||
if (!added.ok) {
|
||||
return;
|
||||
@@ -284,8 +289,9 @@ describe("cli thread commands", () => {
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||
await writeFile(bundlePath, fastBundleSource, "utf8");
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
|
||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(added.ok).toBe(true);
|
||||
if (!added.ok) {
|
||||
return;
|
||||
@@ -313,8 +319,9 @@ describe("cli thread commands", () => {
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
const bundlePath = join(bundleDir, "demo.esm.js");
|
||||
await writeFile(bundlePath, delayedFirstYieldBundleSource, "utf8");
|
||||
await writeFile(join(bundleDir, "demo.yaml"), MINIMAL_DESCRIPTOR_YAML, "utf8");
|
||||
|
||||
const added = await cmdAdd(storageRoot, "solve-issue", bundlePath);
|
||||
const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath));
|
||||
expect(added.ok).toBe(true);
|
||||
if (!added.ok) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
export type ParsedAddArgv = {
|
||||
name: string;
|
||||
filePath: string;
|
||||
/** Override path to descriptor YAML when adding an `.esm.js` bundle. */
|
||||
descriptorPath: string | null;
|
||||
/** Override path to `.d.ts` when adding an `.esm.js` bundle. */
|
||||
typesPath: string | null;
|
||||
};
|
||||
|
||||
export function parseAddArgv(argv: string[]): Result<ParsedAddArgv, string> {
|
||||
let name: string | undefined;
|
||||
let filePath: string | undefined;
|
||||
let descriptorPath: string | null = null;
|
||||
let typesPath: string | null = null;
|
||||
|
||||
let i = 0;
|
||||
while (i < argv.length) {
|
||||
const tok = argv[i];
|
||||
if (tok === "--descriptor") {
|
||||
const value = argv[i + 1];
|
||||
if (value === undefined || value.startsWith("--")) {
|
||||
return err("missing value for --descriptor");
|
||||
}
|
||||
descriptorPath = value;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (tok === "--types") {
|
||||
const value = argv[i + 1];
|
||||
if (value === undefined || value.startsWith("--")) {
|
||||
return err("missing value for --types");
|
||||
}
|
||||
typesPath = value;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (tok !== undefined && tok.startsWith("--")) {
|
||||
return err(`unknown add flag: ${tok}`);
|
||||
}
|
||||
if (tok === undefined) {
|
||||
break;
|
||||
}
|
||||
if (name === undefined) {
|
||||
name = tok;
|
||||
} else if (filePath === undefined) {
|
||||
filePath = tok;
|
||||
} else {
|
||||
return err("too many arguments");
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if (name === undefined || name === "" || filePath === undefined || filePath === "") {
|
||||
return err("add requires <name> <file>");
|
||||
}
|
||||
|
||||
return ok({ name, filePath, descriptorPath, typesPath });
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { copyFile, mkdir, readFile, stat } from "node:fs/promises";
|
||||
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
@@ -12,28 +12,39 @@ async function pathExists(path: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function storeWorkflowBundleCopy(
|
||||
storageRoot: string,
|
||||
hash: string,
|
||||
resolvedSourcePath: string,
|
||||
sourceText: string,
|
||||
): Promise<Result<void, string>> {
|
||||
const bundlesDir = join(storageRoot, "bundles");
|
||||
const destPath = join(bundlesDir, `${hash}.esm.js`);
|
||||
export type BundleFileSource =
|
||||
| { kind: "text"; text: string }
|
||||
| { kind: "path"; path: string };
|
||||
|
||||
export type WorkflowBundleStoreInput = {
|
||||
esmJs: BundleFileSource;
|
||||
yaml: BundleFileSource;
|
||||
dts: BundleFileSource | null;
|
||||
};
|
||||
|
||||
async function resolveSourceText(src: BundleFileSource): Promise<Result<string, string>> {
|
||||
if (src.kind === "text") {
|
||||
return ok(src.text);
|
||||
}
|
||||
try {
|
||||
await mkdir(bundlesDir, { recursive: true });
|
||||
return ok(await readFile(src.path, "utf8"));
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
return err(`failed to store bundle: ${message}`);
|
||||
return err(`failed to read bundle artifact: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureMatchingOrWrite(
|
||||
destPath: string,
|
||||
text: string,
|
||||
label: string,
|
||||
): Promise<Result<void, string>> {
|
||||
if (!(await pathExists(destPath))) {
|
||||
try {
|
||||
await copyFile(resolvedSourcePath, destPath);
|
||||
await writeFile(destPath, text, "utf8");
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
return err(`failed to store bundle: ${message}`);
|
||||
return err(`failed to write ${label}: ${message}`);
|
||||
}
|
||||
return ok(undefined);
|
||||
}
|
||||
@@ -43,10 +54,67 @@ export async function storeWorkflowBundleCopy(
|
||||
existing = await readFile(destPath, "utf8");
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
return err(`failed to store bundle: ${message}`);
|
||||
return err(`failed to read existing ${label}: ${message}`);
|
||||
}
|
||||
if (existing !== sourceText) {
|
||||
return err(`bundle hash ${hash} already exists with different contents; refusing to overwrite`);
|
||||
if (existing !== text) {
|
||||
return err(
|
||||
`${label} for this hash already exists with different contents; refusing to overwrite`,
|
||||
);
|
||||
}
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
/** Store `.esm.js`, `.yaml`, and optional `.d.ts` under `bundles/` keyed by hash. */
|
||||
export async function storeWorkflowBundleArtifacts(
|
||||
storageRoot: string,
|
||||
hash: string,
|
||||
input: WorkflowBundleStoreInput,
|
||||
): Promise<Result<void, string>> {
|
||||
const bundlesDir = join(storageRoot, "bundles");
|
||||
try {
|
||||
await mkdir(bundlesDir, { recursive: true });
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
return err(`failed to store bundle: ${message}`);
|
||||
}
|
||||
|
||||
const esmText = await resolveSourceText(input.esmJs);
|
||||
if (!esmText.ok) {
|
||||
return esmText;
|
||||
}
|
||||
const yamlText = await resolveSourceText(input.yaml);
|
||||
if (!yamlText.ok) {
|
||||
return yamlText;
|
||||
}
|
||||
|
||||
let dtsText: string | null = null;
|
||||
if (input.dts !== null) {
|
||||
const dtsResolved = await resolveSourceText(input.dts);
|
||||
if (!dtsResolved.ok) {
|
||||
return dtsResolved;
|
||||
}
|
||||
dtsText = dtsResolved.value;
|
||||
}
|
||||
|
||||
const destEsm = join(bundlesDir, `${hash}.esm.js`);
|
||||
const destYaml = join(bundlesDir, `${hash}.yaml`);
|
||||
const destDts = join(bundlesDir, `${hash}.d.ts`);
|
||||
|
||||
const w1 = await ensureMatchingOrWrite(destEsm, esmText.value, "bundle");
|
||||
if (!w1.ok) {
|
||||
return w1;
|
||||
}
|
||||
const w2 = await ensureMatchingOrWrite(destYaml, yamlText.value, "descriptor");
|
||||
if (!w2.ok) {
|
||||
return w2;
|
||||
}
|
||||
|
||||
if (dtsText !== null) {
|
||||
const w3 = await ensureMatchingOrWrite(destDts, dtsText, "types");
|
||||
if (!w3.ok) {
|
||||
return w3;
|
||||
}
|
||||
}
|
||||
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { printCliError, printCliLine } from "./cli-output.js";
|
||||
import { parseAddArgv } from "./add-argv.js";
|
||||
import { printCliError, printCliLine, printCliWarn } from "./cli-output.js";
|
||||
import { cmdAdd, formatAddSuccess } from "./cmd-add.js";
|
||||
import { cmdFork, parseForkArgv } from "./cmd-fork.js";
|
||||
import { cmdHistory } from "./cmd-history.js";
|
||||
@@ -18,7 +19,7 @@ import { parseRunArgv } from "./run-argv.js";
|
||||
function usage(): string {
|
||||
return [
|
||||
"Usage:",
|
||||
" uncaged-workflow add <name> <file>",
|
||||
" uncaged-workflow add <name> <file> [--descriptor <path>] [--types <path>]",
|
||||
" uncaged-workflow list",
|
||||
" uncaged-workflow show <name>",
|
||||
" uncaged-workflow remove <name>",
|
||||
@@ -37,18 +38,20 @@ function usage(): string {
|
||||
}
|
||||
|
||||
async function dispatchAdd(storageRoot: string, argv: string[]): Promise<number> {
|
||||
const name = argv[0];
|
||||
const file = argv[1];
|
||||
if (name === undefined || file === undefined || argv.length > 2) {
|
||||
printCliError(`${usage()}\n\nerror: add requires <name> <file>`);
|
||||
const parsed = parseAddArgv(argv);
|
||||
if (!parsed.ok) {
|
||||
printCliError(`${usage()}\n\nerror: ${parsed.error}`);
|
||||
return 1;
|
||||
}
|
||||
const result = await cmdAdd(storageRoot, name, file);
|
||||
const result = await cmdAdd(storageRoot, parsed.value);
|
||||
if (!result.ok) {
|
||||
printCliError(result.error);
|
||||
return 1;
|
||||
}
|
||||
printCliLine(formatAddSuccess(name, file, result.value.hash));
|
||||
for (const w of result.value.warnings) {
|
||||
printCliWarn(w);
|
||||
}
|
||||
printCliLine(formatAddSuccess(parsed.value.name, parsed.value.filePath, result.value.hash));
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,3 +7,8 @@ export function printCliError(line: string): void {
|
||||
// biome-ignore lint/suspicious/noConsole: CLI user-facing errors
|
||||
console.error(line);
|
||||
}
|
||||
|
||||
export function printCliWarn(line: string): void {
|
||||
// biome-ignore lint/suspicious/noConsole: CLI user-facing warnings
|
||||
console.warn(line);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { readFile, stat } from "node:fs/promises";
|
||||
import { basename, resolve } from "node:path";
|
||||
|
||||
import {
|
||||
buildWorkflowFromTypeScript,
|
||||
err,
|
||||
hashWorkflowBundleBytes,
|
||||
ok,
|
||||
@@ -12,25 +13,97 @@ import {
|
||||
writeWorkflowRegistry,
|
||||
} from "@uncaged/workflow";
|
||||
|
||||
import { storeWorkflowBundleCopy } from "./bundle-store.js";
|
||||
import type { ParsedAddArgv } from "./add-argv.js";
|
||||
import { storeWorkflowBundleArtifacts } from "./bundle-store.js";
|
||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
||||
|
||||
export type CmdAddSuccess = {
|
||||
hash: string;
|
||||
warnings: ReadonlyArray<string>;
|
||||
};
|
||||
|
||||
function isTypeScriptWorkflow(path: string): boolean {
|
||||
return path.endsWith(".ts");
|
||||
}
|
||||
|
||||
function isEsmBundle(path: string): boolean {
|
||||
return path.endsWith(".esm.js");
|
||||
}
|
||||
|
||||
function defaultDescriptorPath(bundlePath: string): string {
|
||||
return bundlePath.replace(/\.esm\.js$/i, ".yaml");
|
||||
}
|
||||
|
||||
function defaultTypesPath(bundlePath: string): string {
|
||||
return bundlePath.replace(/\.esm\.js$/i, ".d.ts");
|
||||
}
|
||||
|
||||
async function registerHash(
|
||||
storageRoot: string,
|
||||
name: string,
|
||||
hash: string,
|
||||
): Promise<Result<void, string>> {
|
||||
const reg = await readWorkflowRegistry(storageRoot);
|
||||
if (!reg.ok) {
|
||||
return err(reg.error.message);
|
||||
}
|
||||
|
||||
const next = registerWorkflowVersion(reg.value, name, hash, Date.now());
|
||||
const written = await writeWorkflowRegistry(storageRoot, next);
|
||||
if (!written.ok) {
|
||||
return err(written.error.message);
|
||||
}
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
export async function cmdAdd(
|
||||
storageRoot: string,
|
||||
name: string,
|
||||
filePath: string,
|
||||
): Promise<Result<{ hash: string }, string>> {
|
||||
const nameOk = validateCliWorkflowName(name);
|
||||
args: ParsedAddArgv,
|
||||
): Promise<Result<CmdAddSuccess, string>> {
|
||||
const nameOk = validateCliWorkflowName(args.name);
|
||||
if (!nameOk.ok) {
|
||||
return nameOk;
|
||||
}
|
||||
|
||||
let resolvedPath: string;
|
||||
try {
|
||||
resolvedPath = resolve(filePath);
|
||||
resolvedPath = resolve(args.filePath);
|
||||
await stat(resolvedPath);
|
||||
} catch {
|
||||
return err(`bundle file not found: ${filePath}`);
|
||||
return err(`file not found: ${args.filePath}`);
|
||||
}
|
||||
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (isTypeScriptWorkflow(resolvedPath)) {
|
||||
const built = await buildWorkflowFromTypeScript(resolvedPath);
|
||||
if (!built.ok) {
|
||||
return built;
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(built.value.esmJsSource);
|
||||
const hash = hashWorkflowBundleBytes(bytes);
|
||||
|
||||
const stored = await storeWorkflowBundleArtifacts(storageRoot, hash, {
|
||||
esmJs: { kind: "text", text: built.value.esmJsSource },
|
||||
yaml: { kind: "text", text: built.value.yamlSource },
|
||||
dts: { kind: "text", text: built.value.dtsSource },
|
||||
});
|
||||
if (!stored.ok) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
const regResult = await registerHash(storageRoot, args.name, hash);
|
||||
if (!regResult.ok) {
|
||||
return regResult;
|
||||
}
|
||||
|
||||
return ok({ hash, warnings });
|
||||
}
|
||||
|
||||
if (!isEsmBundle(resolvedPath)) {
|
||||
return err('workflow file must be ".ts" or end with ".esm.js"');
|
||||
}
|
||||
|
||||
let source: string;
|
||||
@@ -49,27 +122,54 @@ export async function cmdAdd(
|
||||
return validated;
|
||||
}
|
||||
|
||||
const yamlResolved =
|
||||
args.descriptorPath !== null ? resolve(args.descriptorPath) : defaultDescriptorPath(resolvedPath);
|
||||
|
||||
let yamlText: string;
|
||||
try {
|
||||
yamlText = await readFile(yamlResolved, "utf8");
|
||||
} catch {
|
||||
return err(`descriptor YAML not found: ${yamlResolved}`);
|
||||
}
|
||||
|
||||
let dtsSource: { kind: "text"; text: string } | null = null;
|
||||
if (args.typesPath !== null) {
|
||||
const typesResolved = resolve(args.typesPath);
|
||||
try {
|
||||
const text = await readFile(typesResolved, "utf8");
|
||||
dtsSource = { kind: "text", text };
|
||||
} catch {
|
||||
return err(`types file not found: ${typesResolved}`);
|
||||
}
|
||||
} else {
|
||||
const typesDefault = defaultTypesPath(resolvedPath);
|
||||
try {
|
||||
const text = await readFile(typesDefault, "utf8");
|
||||
dtsSource = { kind: "text", text };
|
||||
} catch {
|
||||
warnings.push(`optional types file not found (${basename(typesDefault)}); skipped`);
|
||||
}
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(source);
|
||||
const hash = hashWorkflowBundleBytes(bytes);
|
||||
|
||||
const stored = await storeWorkflowBundleCopy(storageRoot, hash, resolvedPath, source);
|
||||
const stored = await storeWorkflowBundleArtifacts(storageRoot, hash, {
|
||||
esmJs: { kind: "text", text: source },
|
||||
yaml: { kind: "text", text: yamlText },
|
||||
dts: dtsSource,
|
||||
});
|
||||
if (!stored.ok) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
const reg = await readWorkflowRegistry(storageRoot);
|
||||
if (!reg.ok) {
|
||||
return err(reg.error.message);
|
||||
const regResult = await registerHash(storageRoot, args.name, hash);
|
||||
if (!regResult.ok) {
|
||||
return regResult;
|
||||
}
|
||||
|
||||
const next = registerWorkflowVersion(reg.value, name, hash, Date.now());
|
||||
const written = await writeWorkflowRegistry(storageRoot, next);
|
||||
if (!written.ok) {
|
||||
return err(written.error.message);
|
||||
}
|
||||
|
||||
return ok({ hash });
|
||||
return ok({ hash, warnings });
|
||||
}
|
||||
|
||||
export function formatAddSuccess(name: string, filePath: string, hash: string): string {
|
||||
|
||||
Reference in New Issue
Block a user