@clickraft/cli 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,8 +1,10 @@
1
- ## [0.6.1](https://github.com/clickraft/cli/compare/v0.6.0...v0.6.1) (2026-05-25)
1
+ ## [0.7.0](https://github.com/clickraft/cli/compare/v0.6.1...v0.7.0) (2026-05-25)
2
2
 
3
- ### Bug Fixes
3
+ ### Features
4
4
 
5
- * **auth:** include models:read in DEFAULT_SCOPES ([284764b](https://github.com/clickraft/cli/commit/284764bfff8b132a224c6f3634fa230309d42ce5))
5
+ * **cli:** register workflow subcommand group ([cba5118](https://github.com/clickraft/cli/commit/cba51181f05b157de3f1bf64005d4b5ba1e51d83))
6
+ * **workflows:** add server error codes and response schemas ([1687ad8](https://github.com/clickraft/cli/commit/1687ad8d0b622cc82c7c25d6cca1ebe3c1ca9ef1))
7
+ * **workflows:** add workflow command functions ([1988878](https://github.com/clickraft/cli/commit/198887856866983da66917a718323cb4ddd9bd7b))
6
8
 
7
9
  # Changelog
8
10
 
package/README.md CHANGED
@@ -45,6 +45,28 @@ clickraft generate create --model-slug nano-banana-2 \
45
45
 
46
46
  > **Note:** `--brand-model` only works with reference-supporting models (nano-banana-2 recommended).
47
47
 
48
+ ### Workflows
49
+
50
+ Create, inspect, and mutate visual workflows from your terminal:
51
+
52
+ ```bash
53
+ clickraft workflow create --name "Product Shoot Pipeline"
54
+ clickraft workflow list
55
+ clickraft workflow list --tags marketing,ecommerce
56
+ clickraft workflow get <workflow-id>
57
+
58
+ # Apply operations with auto-rev (fetches current canvas_rev, retries once on conflict)
59
+ clickraft workflow apply <workflow-id> --auto-rev --from-file ops.json
60
+
61
+ # Or inline a single operation
62
+ clickraft workflow apply <workflow-id> --auto-rev \
63
+ --op '{"op":"add_node","node":{"id":"n1","type":"generate","config":{}}}'
64
+ ```
65
+
66
+ The `--auto-rev` flag is recommended for agent use — it fetches the current `canvas_rev` before mutating and retries once if a conflict occurs.
67
+
68
+ See [docs/workflows.md](docs/workflows.md) for the full operation reference and examples.
69
+
48
70
  Full documentation: [docs.clickraft.ai/cli](https://docs.clickraft.ai/cli) _(coming soon)_
49
71
 
50
72
  ## Connecting to staging
package/dist/cli.js CHANGED
@@ -146,17 +146,35 @@ var EXIT_CODE_MAP = {
146
146
  retryable: false,
147
147
  hint: "Run `node-type list` for the current set of node types."
148
148
  },
149
- WORKFLOW_VERSION_REQUIRED: {
149
+ CANVAS_REV_MISMATCH: {
150
+ exitCode: 7,
151
+ category: "conflict",
152
+ retryable: false,
153
+ hint: "The workflow was updated elsewhere. Re-fetch and retry with the current canvas_rev."
154
+ },
155
+ CANVAS_REV_REQUIRED: {
150
156
  exitCode: 2,
151
157
  category: "usage",
152
158
  retryable: false,
153
- hint: 'Send `If-Match: "<version>"` from the last `workflow get` response.'
159
+ hint: "Include --canvas-rev or use --auto-rev."
154
160
  },
155
- WORKFLOW_VERSION_CONFLICT: {
161
+ WORKER_UNREACHABLE: {
162
+ exitCode: 6,
163
+ category: "network",
164
+ retryable: true,
165
+ hint: "Collaboration service temporarily unavailable. Retry."
166
+ },
167
+ IDEMPOTENCY_IN_PROGRESS: {
156
168
  exitCode: 7,
157
169
  category: "conflict",
158
170
  retryable: false,
159
- hint: "The workflow was updated elsewhere. Re-fetch and retry."
171
+ hint: "A request with this idempotency key is currently in-flight. Retry in a moment."
172
+ },
173
+ TOKEN_NAME_INVALID: {
174
+ exitCode: 2,
175
+ category: "usage",
176
+ retryable: false,
177
+ hint: "Agent token name is malformed."
160
178
  },
161
179
  WORKFLOW_NOT_FOUND: {
162
180
  exitCode: 2,
@@ -2881,6 +2899,157 @@ async function listTokens(options) {
2881
2899
  return data;
2882
2900
  }
2883
2901
 
2902
+ // src/schemas/workflows.ts
2903
+ import { z as z12 } from "zod";
2904
+ var WorkflowDeclaredParamSchema = z12.object({
2905
+ type: z12.string(),
2906
+ required: z12.boolean().optional(),
2907
+ default: z12.unknown().optional(),
2908
+ description: z12.string().optional()
2909
+ }).strict();
2910
+ var WorkflowSummarySchema = z12.object({
2911
+ id: z12.string().uuid(),
2912
+ name: z12.string(),
2913
+ tags: z12.array(z12.string()),
2914
+ declared_params: z12.record(z12.string(), WorkflowDeclaredParamSchema).nullable(),
2915
+ canvas_rev: z12.number().int().nonnegative(),
2916
+ updated_at: z12.string()
2917
+ }).strict();
2918
+ var WorkflowsListResponseSchema = z12.object({
2919
+ items: z12.array(WorkflowSummarySchema),
2920
+ nextCursor: z12.string().nullable()
2921
+ }).strict();
2922
+ var WorkflowDetailSchema = z12.object({
2923
+ id: z12.string().uuid(),
2924
+ name: z12.string(),
2925
+ description: z12.string().nullable(),
2926
+ tags: z12.array(z12.string()),
2927
+ declared_params: z12.record(z12.string(), WorkflowDeclaredParamSchema).nullable(),
2928
+ canvas_rev: z12.number().int().nonnegative(),
2929
+ nodes: z12.array(z12.unknown()),
2930
+ edges: z12.array(z12.unknown()),
2931
+ created_at: z12.string(),
2932
+ updated_at: z12.string()
2933
+ }).strict();
2934
+ var WorkflowCreateResponseSchema = z12.object({
2935
+ id: z12.string().uuid(),
2936
+ name: z12.string(),
2937
+ description: z12.string().nullable(),
2938
+ tags: z12.array(z12.string()),
2939
+ declared_params: z12.record(z12.string(), WorkflowDeclaredParamSchema).nullable(),
2940
+ canvas_rev: z12.number().int().nonnegative(),
2941
+ created_at: z12.string(),
2942
+ updated_at: z12.string()
2943
+ }).strict();
2944
+ var WorkflowMutateResponseSchema = z12.object({
2945
+ canvas_rev: z12.number().int().nonnegative(),
2946
+ applied: z12.number().int().nonnegative(),
2947
+ workflow: WorkflowDetailSchema
2948
+ }).strict();
2949
+
2950
+ // src/commands/workflow/internal.ts
2951
+ async function buildWorkflowClient(opts) {
2952
+ const cfg = await loadConfig({
2953
+ flagProfile: opts.profile,
2954
+ flagApiBaseUrl: opts.apiBaseUrl,
2955
+ flagToken: opts.token
2956
+ });
2957
+ return new ApiClient({
2958
+ apiBaseUrl: cfg.apiBaseUrl,
2959
+ accessToken: cfg.accessToken,
2960
+ signal: opts.signal,
2961
+ cloudflareAccess: cfg.cloudflareAccess
2962
+ });
2963
+ }
2964
+
2965
+ // src/commands/workflow/get.ts
2966
+ async function getWorkflow(opts) {
2967
+ const client = opts.client ?? await buildWorkflowClient(opts);
2968
+ return client.request({
2969
+ method: "GET",
2970
+ path: `/workflows/${encodeURIComponent(opts.id)}`,
2971
+ responseSchema: WorkflowDetailSchema,
2972
+ signal: opts.signal
2973
+ });
2974
+ }
2975
+
2976
+ // src/commands/workflow/apply.ts
2977
+ async function applyWorkflow(opts) {
2978
+ const client = opts.client ?? await buildWorkflowClient(opts);
2979
+ try {
2980
+ return await patchWorkflow(client, opts);
2981
+ } catch (err) {
2982
+ if (!(err instanceof ApiError)) throw err;
2983
+ const isRevConflict = err.code === "CANVAS_REV_MISMATCH" || err.meta.status === 409 && err.meta.details != null;
2984
+ if (!isRevConflict || !opts.autoRev) throw err;
2985
+ const snapshot = await getWorkflow({ id: opts.id, client, signal: opts.signal });
2986
+ const retryResult = await patchWorkflow(client, {
2987
+ ...opts,
2988
+ canvasRev: snapshot.canvas_rev,
2989
+ // Fresh key — the body changed (different canvas_rev), so reusing
2990
+ // the caller's key would trigger IDEMPOTENCY_KEY_CONFLICT.
2991
+ idempotencyKey: void 0
2992
+ });
2993
+ return { ...retryResult, retriedWithRev: snapshot.canvas_rev };
2994
+ }
2995
+ }
2996
+ async function patchWorkflow(client, opts) {
2997
+ const data = await client.request({
2998
+ method: "PATCH",
2999
+ path: `/workflows/${encodeURIComponent(opts.id)}`,
3000
+ body: {
3001
+ canvas_rev: opts.canvasRev,
3002
+ operations: opts.operations
3003
+ },
3004
+ idempotencyKey: opts.idempotencyKey,
3005
+ responseSchema: WorkflowMutateResponseSchema,
3006
+ signal: opts.signal
3007
+ });
3008
+ return {
3009
+ canvasRev: data.canvas_rev,
3010
+ applied: data.applied,
3011
+ workflow: data.workflow
3012
+ };
3013
+ }
3014
+
3015
+ // src/commands/workflow/create.ts
3016
+ async function createWorkflow(opts) {
3017
+ const client = opts.client ?? await buildWorkflowClient(opts);
3018
+ const body = { name: opts.name };
3019
+ if (opts.description !== void 0) body.description = opts.description;
3020
+ if (opts.tags !== void 0 && opts.tags.length > 0) body.tags = opts.tags;
3021
+ if (opts.declaredParams !== void 0) body.declared_params = opts.declaredParams;
3022
+ return client.request({
3023
+ method: "POST",
3024
+ path: "/workflows",
3025
+ body,
3026
+ responseSchema: WorkflowCreateResponseSchema,
3027
+ signal: opts.signal
3028
+ });
3029
+ }
3030
+
3031
+ // src/commands/workflow/list.ts
3032
+ async function listWorkflows(opts) {
3033
+ const client = opts.client ?? await buildWorkflowClient(opts);
3034
+ const data = await client.request({
3035
+ method: "GET",
3036
+ path: "/workflows",
3037
+ query: {
3038
+ cursor: opts.cursor,
3039
+ limit: opts.limit,
3040
+ tags: opts.tags?.join(","),
3041
+ declared_params_has_key: opts.declaredParamsHasKey
3042
+ },
3043
+ responseSchema: WorkflowsListResponseSchema,
3044
+ signal: opts.signal
3045
+ });
3046
+ return {
3047
+ items: data.items,
3048
+ nextCursor: data.nextCursor,
3049
+ hasMore: data.nextCursor !== null
3050
+ };
3051
+ }
3052
+
2884
3053
  // src/errors/format.ts
2885
3054
  var FALLBACK_HINT = "See `clickraft --help` or contact support.";
2886
3055
  function formatHumanError(err) {
@@ -3330,6 +3499,81 @@ function formatExpiresAt(iso) {
3330
3499
  const days = Math.round((ms - Date.now()) / (24 * 60 * 60 * 1e3));
3331
3500
  return `${date.toISOString()} (~${days}d)`;
3332
3501
  }
3502
+ function renderWorkflowCreateResult(result, out) {
3503
+ const lines = [
3504
+ `Created workflow "${result.name}"`,
3505
+ ` ID: ${result.id}`,
3506
+ ` Canvas rev: ${result.canvas_rev}`,
3507
+ ` Created: ${result.created_at.slice(0, 19)}`
3508
+ ];
3509
+ out.write(lines.join("\n") + "\n");
3510
+ }
3511
+ function renderWorkflowsList(result, out) {
3512
+ const { items } = result;
3513
+ if (items.length === 0) {
3514
+ out.write("No workflows.\n");
3515
+ return;
3516
+ }
3517
+ const MAX_TAGS = 3;
3518
+ const headers = ["ID", "NAME", "TAGS", "REV", "UPDATED"];
3519
+ const data = items.map((w) => {
3520
+ const shown = w.tags.slice(0, MAX_TAGS).join(", ");
3521
+ const extra = w.tags.length > MAX_TAGS ? ` +${w.tags.length - MAX_TAGS} more` : "";
3522
+ return [
3523
+ w.id.slice(0, 8),
3524
+ w.name,
3525
+ w.tags.length > 0 ? shown + extra : "\u2014",
3526
+ String(w.canvas_rev),
3527
+ w.updated_at.slice(0, 10)
3528
+ ];
3529
+ });
3530
+ const widths = headers.map(
3531
+ (header, i) => Math.max(header.length, ...data.map((cells) => cells[i].length))
3532
+ );
3533
+ const padRow = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
3534
+ out.write(padRow(headers) + "\n");
3535
+ for (const cells of data) out.write(padRow(cells) + "\n");
3536
+ if (result.hasMore) {
3537
+ out.write("\nMore results available. Use --cursor to fetch the next page.\n");
3538
+ }
3539
+ }
3540
+ function renderWorkflowDetail(detail, out) {
3541
+ const pad = 17;
3542
+ const lines = [
3543
+ detail.name,
3544
+ `${"ID:".padEnd(pad)}${detail.id}`,
3545
+ `${"Canvas rev:".padEnd(pad)}${detail.canvas_rev}`,
3546
+ `${"Tags:".padEnd(pad)}${detail.tags.length > 0 ? detail.tags.join(", ") : "\u2014"}`,
3547
+ `${"Created:".padEnd(pad)}${detail.created_at.slice(0, 19)}`,
3548
+ `${"Updated:".padEnd(pad)}${detail.updated_at.slice(0, 19)}`,
3549
+ `${"Nodes:".padEnd(pad)}${detail.nodes.length}`,
3550
+ `${"Edges:".padEnd(pad)}${detail.edges.length}`
3551
+ ];
3552
+ if (detail.description) {
3553
+ lines.push("", detail.description);
3554
+ }
3555
+ const params = detail.declared_params;
3556
+ lines.push("", "Declared parameters:");
3557
+ if (!params || Object.keys(params).length === 0) {
3558
+ lines.push(" (none)");
3559
+ } else {
3560
+ for (const [name, p] of Object.entries(params)) {
3561
+ const req = p.required ? "required" : "optional";
3562
+ const desc = p.description ? `: ${p.description}` : "";
3563
+ lines.push(` - ${name} (${p.type}, ${req})${desc}`);
3564
+ }
3565
+ }
3566
+ out.write(lines.join("\n") + "\n");
3567
+ }
3568
+ function renderWorkflowApplyResult(result, out) {
3569
+ const lines = [
3570
+ `Applied ${result.applied} operation${result.applied === 1 ? "" : "s"}. New canvas_rev: ${result.canvasRev}`
3571
+ ];
3572
+ if (result.retriedWithRev !== void 0) {
3573
+ lines.push(` (auto-rev: resolved conflict, retried with canvas_rev ${result.retriedWithRev})`);
3574
+ }
3575
+ out.write(lines.join("\n") + "\n");
3576
+ }
3333
3577
 
3334
3578
  // src/cli/dispatch.ts
3335
3579
  async function dispatch(options) {
@@ -3559,6 +3803,10 @@ function renderRootHelp(version) {
3559
3803
  " generate wait Wait for an existing job to reach a terminal state",
3560
3804
  " generate get Fetch the current state of a job",
3561
3805
  " jobs wait Alias of `generate wait`",
3806
+ " workflow create Create a new empty workflow",
3807
+ " workflow list List workflows in your organization",
3808
+ " workflow get Fetch a workflow snapshot by ID",
3809
+ " workflow apply Apply operations to a workflow (canvas mutation)",
3562
3810
  " upload <file> Upload a local file and return its public URL",
3563
3811
  "",
3564
3812
  "Options:",
@@ -3852,6 +4100,83 @@ function renderCommandHelp(command) {
3852
4100
  " --json",
3853
4101
  ""
3854
4102
  ].join("\n");
4103
+ case "workflow":
4104
+ return [
4105
+ "Usage: clickraft workflow <subcommand> [options]",
4106
+ "",
4107
+ "Subcommands:",
4108
+ " create Create a new empty workflow",
4109
+ " list List workflows in your organization",
4110
+ " get Fetch a workflow snapshot by ID",
4111
+ " apply Apply operations to a workflow (canvas mutation)",
4112
+ ""
4113
+ ].join("\n");
4114
+ case "workflow create":
4115
+ return [
4116
+ "Usage: clickraft workflow create --name <name> [options]",
4117
+ "",
4118
+ "Creates a new empty workflow.",
4119
+ "",
4120
+ "Options:",
4121
+ " --name <name> Workflow name (required unless --from-file)",
4122
+ " --description <text> Optional description",
4123
+ " --tags <tag> Tag (repeatable)",
4124
+ " --from-file <path> Read the full create payload as JSON from a file",
4125
+ " --profile <name>",
4126
+ " --api-base-url <url>",
4127
+ " --token <token>",
4128
+ " --json",
4129
+ ""
4130
+ ].join("\n");
4131
+ case "workflow list":
4132
+ return [
4133
+ "Usage: clickraft workflow list [options]",
4134
+ "",
4135
+ "Lists workflows in your organization. Supports cursor-based pagination.",
4136
+ "",
4137
+ "Options:",
4138
+ " --tags <csv> Filter by tags (comma-separated)",
4139
+ " --has-param <key> Filter to workflows that declare this param",
4140
+ " --limit <n> Max results per page (1-100, default: 20)",
4141
+ " --cursor <token> Pagination cursor from a previous response",
4142
+ " --profile <name>",
4143
+ " --api-base-url <url>",
4144
+ " --token <token>",
4145
+ " --json",
4146
+ ""
4147
+ ].join("\n");
4148
+ case "workflow get":
4149
+ return [
4150
+ "Usage: clickraft workflow get <id> [options]",
4151
+ "",
4152
+ "Fetches a workflow snapshot including nodes and edges.",
4153
+ "",
4154
+ "Options:",
4155
+ " --profile <name>",
4156
+ " --api-base-url <url>",
4157
+ " --token <token>",
4158
+ " --json",
4159
+ ""
4160
+ ].join("\n");
4161
+ case "workflow apply":
4162
+ return [
4163
+ "Usage: clickraft workflow apply <id> [options]",
4164
+ "",
4165
+ "Applies mutation operations to a workflow. Requires --canvas-rev or --auto-rev.",
4166
+ "",
4167
+ "Options:",
4168
+ " --from-file <path> JSON file with operations (array or { canvas_rev, operations })",
4169
+ " --op <json> Single operation as JSON string (repeatable)",
4170
+ " --canvas-rev <n> The canvas_rev preconditioning the mutation",
4171
+ " --auto-rev Fetch current canvas_rev first; retry once on 409",
4172
+ " --idempotency-key <key> Override the auto-generated idempotency key",
4173
+ " --dry-run Print the request payload without sending it",
4174
+ " --profile <name>",
4175
+ " --api-base-url <url>",
4176
+ " --token <token>",
4177
+ " --json",
4178
+ ""
4179
+ ].join("\n");
3855
4180
  default:
3856
4181
  return `Unknown command: ${path}
3857
4182
  Run \`clickraft --help\` for available commands.
@@ -3898,6 +4223,15 @@ function matchNewCommand(argv) {
3898
4223
  if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "models" };
3899
4224
  return { kind: "unknown-sub", verb: "models", sub };
3900
4225
  }
4226
+ if (verb === "workflow") {
4227
+ const sub = argv[1];
4228
+ if (sub === "create") return { kind: "cmd", path: "workflow.create", verbTokenCount: 2 };
4229
+ if (sub === "list") return { kind: "cmd", path: "workflow.list", verbTokenCount: 2 };
4230
+ if (sub === "get") return { kind: "cmd", path: "workflow.get", verbTokenCount: 2 };
4231
+ if (sub === "apply") return { kind: "cmd", path: "workflow.apply", verbTokenCount: 2 };
4232
+ if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "workflow" };
4233
+ return { kind: "unknown-sub", verb: "workflow", sub };
4234
+ }
3901
4235
  return null;
3902
4236
  }
3903
4237
  async function runNewCommand(newCmd, options, stdout, stderr) {
@@ -3958,6 +4292,14 @@ async function runNewCommand(newCmd, options, stdout, stderr) {
3958
4292
  return await runProductList(argvAfterVerb, ctx, rc);
3959
4293
  case "models.list":
3960
4294
  return await runModelsList(argvAfterVerb, ctx, rc);
4295
+ case "workflow.create":
4296
+ return await runWorkflowCreate(argvAfterVerb, ctx, rc);
4297
+ case "workflow.list":
4298
+ return await runWorkflowList(argvAfterVerb, ctx, rc);
4299
+ case "workflow.get":
4300
+ return await runWorkflowGet(argvAfterVerb, ctx, rc);
4301
+ case "workflow.apply":
4302
+ return await runWorkflowApply(argvAfterVerb, ctx, rc);
3961
4303
  }
3962
4304
  } catch (err) {
3963
4305
  if (isAbortedError(err, options.signal)) {
@@ -4329,6 +4671,211 @@ function emitModelsList(result, ctx, rc, parsed) {
4329
4671
  }
4330
4672
  });
4331
4673
  }
4674
+ async function runWorkflowCreate(argv, ctx, rc) {
4675
+ const parsed = parseArgs(argv, {
4676
+ string: [...COMMON_RICH_STRING_FLAGS, "name", "description", "from-file"],
4677
+ boolean: [...COMMON_RICH_BOOL_FLAGS],
4678
+ array: ["tags"]
4679
+ });
4680
+ let name = parsed.string.name;
4681
+ let description = parsed.string.description;
4682
+ let tags = parsed.array.tags;
4683
+ let declaredParams;
4684
+ if (parsed.string["from-file"]) {
4685
+ const raw = await readJsonFile(parsed.string["from-file"]);
4686
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
4687
+ const obj = raw;
4688
+ name = name ?? (typeof obj.name === "string" ? obj.name : void 0);
4689
+ description = description ?? (typeof obj.description === "string" ? obj.description : void 0);
4690
+ if (!tags && Array.isArray(obj.tags)) tags = obj.tags;
4691
+ if (obj.declared_params && typeof obj.declared_params === "object") {
4692
+ declaredParams = obj.declared_params;
4693
+ }
4694
+ }
4695
+ }
4696
+ if (!name) throw new ArgError("--name is required (or provide it in --from-file).");
4697
+ const result = await createWorkflow({
4698
+ name,
4699
+ description,
4700
+ tags,
4701
+ declaredParams,
4702
+ profile: parsed.string.profile,
4703
+ apiBaseUrl: parsed.string["api-base-url"],
4704
+ token: parsed.string.token,
4705
+ signal: rc.signal
4706
+ });
4707
+ emitWorkflowCreate(result, ctx, rc);
4708
+ return 0;
4709
+ }
4710
+ async function runWorkflowList(argv, ctx, rc) {
4711
+ const parsed = parseArgs(argv, {
4712
+ string: [...COMMON_RICH_STRING_FLAGS, "tags", "has-param", "cursor", "limit"],
4713
+ boolean: [...COMMON_RICH_BOOL_FLAGS]
4714
+ });
4715
+ const limit = optionalInt(parsed.string.limit, "limit");
4716
+ if (limit !== void 0 && (limit < 1 || limit > 100)) {
4717
+ throw new ArgError("--limit must be between 1 and 100.");
4718
+ }
4719
+ const result = await listWorkflows({
4720
+ cursor: parsed.string.cursor,
4721
+ limit,
4722
+ tags: parsed.string.tags ? parsed.string.tags.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
4723
+ declaredParamsHasKey: parsed.string["has-param"],
4724
+ profile: parsed.string.profile,
4725
+ apiBaseUrl: parsed.string["api-base-url"],
4726
+ token: parsed.string.token,
4727
+ signal: rc.signal
4728
+ });
4729
+ ctx.nextCursor = result.nextCursor ?? void 0;
4730
+ emitWorkflowList(result, ctx, rc);
4731
+ return 0;
4732
+ }
4733
+ async function runWorkflowGet(argv, ctx, rc) {
4734
+ const parsed = parseArgs(argv, {
4735
+ string: [...COMMON_RICH_STRING_FLAGS],
4736
+ boolean: [...COMMON_RICH_BOOL_FLAGS]
4737
+ });
4738
+ const id = parsed.positional[0];
4739
+ if (!id) throw new ArgError("Missing required <id> argument.");
4740
+ const detail = await getWorkflow({
4741
+ id,
4742
+ profile: parsed.string.profile,
4743
+ apiBaseUrl: parsed.string["api-base-url"],
4744
+ token: parsed.string.token,
4745
+ signal: rc.signal
4746
+ });
4747
+ emitWorkflowDetail(detail, ctx, rc);
4748
+ return 0;
4749
+ }
4750
+ async function runWorkflowApply(argv, ctx, rc) {
4751
+ const parsed = parseArgs(argv, {
4752
+ string: [...COMMON_RICH_STRING_FLAGS, "canvas-rev", "from-file", "idempotency-key"],
4753
+ boolean: [...COMMON_RICH_BOOL_FLAGS, "auto-rev", "dry-run"],
4754
+ array: ["op"]
4755
+ });
4756
+ const id = parsed.positional[0];
4757
+ if (!id) throw new ArgError("Missing required <id> argument.");
4758
+ let operations = [];
4759
+ let fileCanvasRev;
4760
+ if (parsed.string["from-file"]) {
4761
+ const raw = await readJsonFile(parsed.string["from-file"]);
4762
+ if (Array.isArray(raw)) {
4763
+ operations = raw;
4764
+ } else if (typeof raw === "object" && raw !== null && "operations" in raw) {
4765
+ const obj = raw;
4766
+ operations = obj.operations;
4767
+ if (typeof obj.canvas_rev === "number") fileCanvasRev = obj.canvas_rev;
4768
+ } else {
4769
+ throw new ArgError(
4770
+ '--from-file must contain a JSON array or an object with an "operations" key.'
4771
+ );
4772
+ }
4773
+ }
4774
+ if (parsed.array.op) {
4775
+ for (const opStr of parsed.array.op) {
4776
+ try {
4777
+ operations.push(JSON.parse(opStr));
4778
+ } catch {
4779
+ throw new ArgError(`Invalid JSON in --op: ${opStr}`);
4780
+ }
4781
+ }
4782
+ }
4783
+ if (operations.length === 0) {
4784
+ throw new ArgError("No operations provided. Use --from-file or --op.");
4785
+ }
4786
+ if (operations.length > 100) {
4787
+ throw new ArgError("At most 100 operations per request.");
4788
+ }
4789
+ const autoRev = parsed.boolean["auto-rev"] === true;
4790
+ let canvasRev;
4791
+ if (parsed.string["canvas-rev"] !== void 0) {
4792
+ const n = optionalInt(parsed.string["canvas-rev"], "canvas-rev");
4793
+ if (n === void 0) throw new ArgError("--canvas-rev must be a non-negative integer.");
4794
+ canvasRev = n;
4795
+ } else if (autoRev) {
4796
+ const snapshot = await getWorkflow({
4797
+ id,
4798
+ profile: parsed.string.profile,
4799
+ apiBaseUrl: parsed.string["api-base-url"],
4800
+ token: parsed.string.token,
4801
+ signal: rc.signal
4802
+ });
4803
+ canvasRev = snapshot.canvas_rev;
4804
+ } else if (fileCanvasRev !== void 0) {
4805
+ canvasRev = fileCanvasRev;
4806
+ } else {
4807
+ throw new ArgError(
4808
+ "Must provide --canvas-rev, use --auto-rev, or include canvas_rev in --from-file."
4809
+ );
4810
+ }
4811
+ if (parsed.boolean["dry-run"] === true) {
4812
+ const payload = { canvas_rev: canvasRev, operations };
4813
+ rc.stdout.write(JSON.stringify(payload, null, 2) + "\n");
4814
+ return 0;
4815
+ }
4816
+ const result = await applyWorkflow({
4817
+ id,
4818
+ canvasRev,
4819
+ operations,
4820
+ idempotencyKey: parsed.string["idempotency-key"],
4821
+ autoRev,
4822
+ profile: parsed.string.profile,
4823
+ apiBaseUrl: parsed.string["api-base-url"],
4824
+ token: parsed.string.token,
4825
+ signal: rc.signal
4826
+ });
4827
+ emitWorkflowApply(result, ctx, rc);
4828
+ return 0;
4829
+ }
4830
+ function emitWorkflowCreate(result, ctx, rc) {
4831
+ if (rc.signal?.aborted) throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
4832
+ if (rc.jsonMode) {
4833
+ rc.stdout.write(JSON.stringify(wrapSuccess(result, ctx)) + "\n");
4834
+ return;
4835
+ }
4836
+ renderWorkflowCreateResult(result, rc.stdout);
4837
+ }
4838
+ function emitWorkflowList(result, ctx, rc) {
4839
+ if (rc.signal?.aborted) throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
4840
+ if (rc.jsonMode) {
4841
+ rc.stdout.write(JSON.stringify(wrapSuccess(result, ctx)) + "\n");
4842
+ return;
4843
+ }
4844
+ renderWorkflowsList(result, rc.stdout);
4845
+ }
4846
+ function emitWorkflowDetail(detail, ctx, rc) {
4847
+ if (rc.signal?.aborted) throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
4848
+ if (rc.jsonMode) {
4849
+ rc.stdout.write(JSON.stringify(wrapSuccess(detail, ctx)) + "\n");
4850
+ return;
4851
+ }
4852
+ renderWorkflowDetail(detail, rc.stdout);
4853
+ }
4854
+ function emitWorkflowApply(result, ctx, rc) {
4855
+ if (rc.signal?.aborted) throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
4856
+ if (rc.jsonMode) {
4857
+ rc.stdout.write(JSON.stringify(wrapSuccess(result, ctx)) + "\n");
4858
+ return;
4859
+ }
4860
+ renderWorkflowApplyResult(result, rc.stdout);
4861
+ }
4862
+ async function readJsonFile(filePath) {
4863
+ const { readFile: readFile3 } = await import("fs/promises");
4864
+ let text;
4865
+ try {
4866
+ text = await readFile3(filePath, "utf8");
4867
+ } catch (err) {
4868
+ const code = err.code;
4869
+ if (code === "ENOENT") throw new ArgError(`File not found: ${filePath}`);
4870
+ if (code === "EACCES") throw new ArgError(`Permission denied: ${filePath}`);
4871
+ throw new ArgError(`Failed to read file: ${filePath}`);
4872
+ }
4873
+ try {
4874
+ return JSON.parse(text);
4875
+ } catch {
4876
+ throw new ArgError(`File is not valid JSON: ${filePath}`);
4877
+ }
4878
+ }
4332
4879
 
4333
4880
  // src/index.ts
4334
4881
  var __filename2 = fileURLToPath(import.meta.url);