@relayflows/sdk 2.0.15 → 2.0.16

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.
Files changed (77) hide show
  1. package/dist/agent-artifacts.d.ts +20 -0
  2. package/dist/agent-artifacts.d.ts.map +1 -0
  3. package/dist/agent-artifacts.js +81 -0
  4. package/dist/agent-artifacts.js.map +1 -0
  5. package/dist/authored-flow-executor.d.ts +3 -3
  6. package/dist/authored-flow-executor.d.ts.map +1 -1
  7. package/dist/authored-flow-executor.js +7 -7
  8. package/dist/authored-flow-executor.js.map +1 -1
  9. package/dist/authored-step-output.d.ts +2 -0
  10. package/dist/authored-step-output.d.ts.map +1 -1
  11. package/dist/authored-step-output.js +5 -1
  12. package/dist/authored-step-output.js.map +1 -1
  13. package/dist/authored-worker-step.d.ts.map +1 -1
  14. package/dist/authored-worker-step.js +19 -1
  15. package/dist/authored-worker-step.js.map +1 -1
  16. package/dist/cli/check.js +6 -1
  17. package/dist/cli/check.js.map +1 -1
  18. package/dist/cli/cloud-deploy.d.ts +30 -0
  19. package/dist/cli/cloud-deploy.d.ts.map +1 -0
  20. package/dist/cli/cloud-deploy.js +162 -0
  21. package/dist/cli/cloud-deploy.js.map +1 -0
  22. package/dist/cli/cloud-run.d.ts +3 -1
  23. package/dist/cli/cloud-run.d.ts.map +1 -1
  24. package/dist/cli/cloud-run.js +37 -5
  25. package/dist/cli/cloud-run.js.map +1 -1
  26. package/dist/cli/cloud-sync.d.ts +13 -0
  27. package/dist/cli/cloud-sync.d.ts.map +1 -0
  28. package/dist/cli/cloud-sync.js +39 -0
  29. package/dist/cli/cloud-sync.js.map +1 -0
  30. package/dist/cli/run.d.ts +1 -1
  31. package/dist/cli/run.d.ts.map +1 -1
  32. package/dist/cli/run.js +12 -1
  33. package/dist/cli/run.js.map +1 -1
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +82 -12
  36. package/dist/cli.js.map +1 -1
  37. package/dist/cloud-deploy.d.ts +70 -0
  38. package/dist/cloud-deploy.d.ts.map +1 -0
  39. package/dist/cloud-deploy.js +181 -0
  40. package/dist/cloud-deploy.js.map +1 -0
  41. package/dist/cloud-http.d.ts +37 -2
  42. package/dist/cloud-http.d.ts.map +1 -1
  43. package/dist/cloud-http.js +98 -8
  44. package/dist/cloud-http.js.map +1 -1
  45. package/dist/cloud-run.d.ts +23 -0
  46. package/dist/cloud-run.d.ts.map +1 -1
  47. package/dist/cloud-run.js +30 -1
  48. package/dist/cloud-run.js.map +1 -1
  49. package/dist/cloud-sync.d.ts +66 -0
  50. package/dist/cloud-sync.d.ts.map +1 -0
  51. package/dist/cloud-sync.js +287 -0
  52. package/dist/cloud-sync.js.map +1 -0
  53. package/dist/failure-kinds.d.ts +8 -1
  54. package/dist/failure-kinds.d.ts.map +1 -1
  55. package/dist/failure-kinds.js +8 -0
  56. package/dist/failure-kinds.js.map +1 -1
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +2 -0
  60. package/dist/index.js.map +1 -1
  61. package/package.json +2 -2
  62. package/src/agent-artifacts.ts +76 -0
  63. package/src/authored-flow-executor.ts +8 -8
  64. package/src/authored-step-output.ts +6 -1
  65. package/src/authored-worker-step.ts +19 -1
  66. package/src/cli/check.ts +6 -1
  67. package/src/cli/cloud-deploy.ts +157 -0
  68. package/src/cli/cloud-run.ts +37 -6
  69. package/src/cli/cloud-sync.ts +37 -0
  70. package/src/cli/run.ts +13 -2
  71. package/src/cli.ts +75 -12
  72. package/src/cloud-deploy.ts +236 -0
  73. package/src/cloud-http.ts +129 -9
  74. package/src/cloud-run.ts +47 -1
  75. package/src/cloud-sync.ts +315 -0
  76. package/src/failure-kinds.ts +8 -0
  77. package/src/index.ts +8 -0
@@ -12,6 +12,7 @@ import { isSurfaceCompletionReason, readCompletedStepOutput, readSuccessfulOutpu
12
12
  import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
13
13
  import { snapshotJsonValue } from './json-value.js';
14
14
  import { authoredChildAdmissionKey } from './authored-admission.js';
15
+ import { diffWorkspaceFiles, snapshotWorkspaceFiles } from './agent-artifacts.js';
15
16
 
16
17
  const WORKSPACE_PERMISSION_ANNOTATION = /:\s*(readonly|readwrite)\s*$/i;
17
18
 
@@ -129,6 +130,20 @@ export function authoredWorkerRunner(
129
130
  `f.agent options.transport must be 'direct' or 'relay' (got ${JSON.stringify(options.transport)}).`,
130
131
  );
131
132
  }
133
+ // Artifact detection only tells the truth for the local-agent DIRECT
134
+ // path: that is the only case that runs in this same process, on this
135
+ // same filesystem, so `options.cwd` (or `process.cwd()`) is provably
136
+ // where the CLI actually wrote — a workspace-scoped step never reaches
137
+ // here with a local agent attached (refused above). `transport: 'relay'`
138
+ // dispatches to agent-relay, which executes on a remote host even
139
+ // though a local agent stream is still attached, so it gets no local
140
+ // snapshot either. Any other worker attachment may execute on a
141
+ // different host entirely; snapshotting this process's filesystem for
142
+ // that case would be a guess, not a fact, so `artifacts` stays `[]`
143
+ // there, exactly as before this fix.
144
+ const artifactRoot = localAgentStream === undefined || options.transport === 'relay'
145
+ ? undefined : options.cwd ?? process.cwd();
146
+ const before = artifactRoot === undefined ? undefined : await snapshotWorkspaceFiles(artifactRoot);
132
147
  const output = await run({
133
148
  id, type: 'agent', instruction: options.task,
134
149
  ...(localAgentStream === undefined ? {} : { surfaces: { streams: [{ stream: localAgentStream }] } }),
@@ -143,7 +158,10 @@ export function authoredWorkerRunner(
143
158
  throw new AuthoredFlowExecutionError('journal_protocol_violation', `step "${id}" produced a non-object output`);
144
159
  }
145
160
  const stdout = 'stdout_tail' in output ? output.stdout_tail : undefined;
146
- return { summary: typeof stdout === 'string' ? stdout : JSON.stringify(output), artifacts: [] };
161
+ const artifacts = before === undefined || artifactRoot === undefined
162
+ ? []
163
+ : diffWorkspaceFiles(before, await snapshotWorkspaceFiles(artifactRoot));
164
+ return { summary: typeof stdout === 'string' ? stdout : JSON.stringify(output), artifacts };
147
165
  },
148
166
  async llm(id: string, prompt: string, options?: LlmOptions, verification?: NamedGate): Promise<unknown> {
149
167
  if (options !== undefined && (typeof options !== 'object' || options === null || options.output === undefined)) {
package/src/cli/check.ts CHANGED
@@ -340,7 +340,12 @@ function systemProbes(flowDirectory: string, config: ProjectConfig): PreflightPr
340
340
  helper: helperReady,
341
341
  cli: (cli, source, model) => probeCli(cli, source === 'project' ? config.directory : flowDirectory, model),
342
342
  executor: (trigger) => config.executors.includes(trigger.executor),
343
- command: (binary) => executableExists(binary, flowDirectory),
343
+ // A deterministic step runs in the daemon's working directory — the
344
+ // directory `flows run` was invoked from, or Cloud's code mount — not in
345
+ // the flow file's. Probing `./x` against the flow's directory answered a
346
+ // question the kernel never asks, and refused a Cloud run whose synced
347
+ // tree held the script while its source sat in the state directory.
348
+ command: (binary) => executableExists(binary, process.cwd()),
344
349
  };
345
350
  }
346
351
 
@@ -0,0 +1,157 @@
1
+ import { CloudFlowError } from '../cloud-http.js';
2
+ import {
3
+ deployToCloud, listCloudDeployments, parseAgentHarnesses, parseRepository, parseTriggerSource, undeployFromCloud,
4
+ type FlowTriggerSource,
5
+ } from '../cloud-deploy.js';
6
+ import type { CliIo } from '../cli.js';
7
+
8
+ export interface CloudDeployArgs {
9
+ command: 'cloud-deploy';
10
+ value: string;
11
+ repo: string;
12
+ on: string[];
13
+ approver: string | undefined;
14
+ name: string | undefined;
15
+ agents: string | undefined;
16
+ draft: boolean;
17
+ json: boolean;
18
+ }
19
+
20
+ /**
21
+ * `flows deploy <flow.ts> --repo <owner/name> --on <provider>[:k=v,…] [--on …]
22
+ * --approver <handle> [--name <n>] [--json]`
23
+ *
24
+ * Parsed here rather than in `parseDeployArgs` because the two `deploy` forms
25
+ * share nothing but the word: the digest form copies a sealed bundle into a
26
+ * file bucket, this one creates a hosted listener. The positional decides.
27
+ */
28
+ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs | undefined {
29
+ let value: string | undefined;
30
+ let repo: string | undefined;
31
+ let approver: string | undefined;
32
+ let name: string | undefined;
33
+ let agents: string | undefined;
34
+ let draft = false;
35
+ let json = false;
36
+ const on: string[] = [];
37
+ for (let i = 0; i < args.length; i++) {
38
+ const arg = args[i]!;
39
+ if (arg === '--json') {
40
+ if (json) return undefined;
41
+ json = true;
42
+ continue;
43
+ }
44
+ if (arg === '--draft') {
45
+ if (draft) return undefined;
46
+ draft = true;
47
+ continue;
48
+ }
49
+ if (arg === '--agents') {
50
+ const next = args[i + 1];
51
+ if (agents !== undefined || next === undefined || next.startsWith('-')) return undefined;
52
+ agents = next;
53
+ i += 1;
54
+ continue;
55
+ }
56
+ if (arg === '--repo' || arg === '--approver' || arg === '--name' || arg === '--on') {
57
+ const next = args[i + 1];
58
+ if (next === undefined || next.startsWith('-')) return undefined;
59
+ i += 1;
60
+ if (arg === '--on') { on.push(next); continue; }
61
+ if (arg === '--repo') { if (repo !== undefined) return undefined; repo = next; continue; }
62
+ if (arg === '--approver') { if (approver !== undefined) return undefined; approver = next; continue; }
63
+ if (name !== undefined) return undefined;
64
+ name = next;
65
+ continue;
66
+ }
67
+ if (arg.startsWith('-') || value !== undefined) return undefined;
68
+ value = arg;
69
+ }
70
+ if (value === undefined || repo === undefined || on.length === 0) return undefined;
71
+ return { command: 'cloud-deploy', value, repo, on, approver, name, agents, draft, json };
72
+ }
73
+
74
+ function describeSource(source: FlowTriggerSource): string {
75
+ const settings = Object.entries(source.settings).map(([k, v]) => `${k}=${v}`).join(' ');
76
+ return settings ? `${source.provider} ${settings}` : source.provider;
77
+ }
78
+
79
+ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promise<0 | 1 | 2> {
80
+ try {
81
+ if (args.approver === undefined) {
82
+ throw new CloudFlowError('invalid_input',
83
+ '--approver <handle> is required: every launched run receives it as input.approver for f.human.');
84
+ }
85
+ const deployment = await deployToCloud({
86
+ path: args.value,
87
+ repository: parseRepository(args.repo),
88
+ sources: args.on.map(parseTriggerSource),
89
+ approver: args.approver,
90
+ draft: args.draft,
91
+ ...(args.name === undefined ? {} : { name: args.name }),
92
+ ...(args.agents === undefined ? {} : { agents: parseAgentHarnesses(args.agents) }),
93
+ });
94
+ if (args.json) {
95
+ io.stdout(JSON.stringify({ ok: true, ...deployment }));
96
+ return 0;
97
+ }
98
+ io.stdout(`${deployment.status === 'draft' ? 'SAVED' : 'DEPLOYED'} ${deployment.agentId} ${deployment.status}`);
99
+ io.stdout(` flow: ${deployment.name} (${args.value}, sha256 ${deployment.sourceSha256.slice(0, 12)})`);
100
+ io.stdout(` repository: ${deployment.repository.owner}/${deployment.repository.name}`);
101
+ for (const source of deployment.sources) io.stdout(` on: ${describeSource(source)}`);
102
+ io.stdout(deployment.status === 'draft'
103
+ ? 'Saved without activating; activate it from the Cloud dashboard, or redeploy without --draft.'
104
+ : 'Each matching ticket launches a run of this source in a fresh branch; list with: flows deployments');
105
+ return 0;
106
+ } catch (error) {
107
+ return reportCloudFailure(error, args.json, io);
108
+ }
109
+ }
110
+
111
+ export async function runCloudDeploymentsCli({ json }: { json: boolean }, io: CliIo): Promise<0 | 1 | 2> {
112
+ try {
113
+ const deployments = await listCloudDeployments();
114
+ if (json) {
115
+ io.stdout(JSON.stringify({ ok: true, deployments }));
116
+ return 0;
117
+ }
118
+ if (deployments.length === 0) {
119
+ io.stdout('No flow deployments in this workspace.');
120
+ return 0;
121
+ }
122
+ for (const d of deployments) {
123
+ const repo = d.repository ? ` ${d.repository.owner}/${d.repository.name}` : '';
124
+ io.stdout(`${d.agentId} ${d.status} ${JSON.stringify(d.name)}${repo}`);
125
+ for (const source of d.sources) io.stdout(` on: ${describeSource(source)}`);
126
+ }
127
+ return 0;
128
+ } catch (error) {
129
+ return reportCloudFailure(error, json, io);
130
+ }
131
+ }
132
+
133
+ export async function runCloudUndeployCli({ agentId, json }: { agentId: string; json: boolean }, io: CliIo): Promise<0 | 1 | 2> {
134
+ try {
135
+ await undeployFromCloud(agentId);
136
+ io.stdout(json ? JSON.stringify({ ok: true, agentId, status: 'deleted' }) : `UNDEPLOYED ${agentId}`);
137
+ return 0;
138
+ } catch (error) {
139
+ return reportCloudFailure(error, json, io);
140
+ }
141
+ }
142
+
143
+ function reportCloudFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
144
+ const code = error instanceof CloudFlowError ? error.code : 'cloud_deploy_failed';
145
+ let message = error instanceof Error ? error.message : 'Cloud deploy failed.';
146
+ // The deploy routes take a browser session or a `cli:auth` token. A
147
+ // deployment (CI) token gets 403 `session_required`; say what fixes it.
148
+ if (error instanceof CloudFlowError && error.status === 403) {
149
+ message += ' Deploying needs an interactive `cli:auth` credential: run `agent-relay cloud login` '
150
+ + '(a deployment token can run flows but not deploy them).';
151
+ }
152
+ if (json) io.stdout(JSON.stringify({ ok: false, code, message }));
153
+ else io.stderr(`${code}: ${message}`);
154
+ return error instanceof CloudFlowError
155
+ && (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code) || error.status === 403 || error.status === 401)
156
+ ? 2 : 1;
157
+ }
@@ -1,10 +1,14 @@
1
1
  import { CloudFlowError } from '../cloud-http.js';
2
- import { runInCloud, waitForCloudFlowRun } from '../cloud-run.js';
2
+ import { runInCloud, waitForCloudFlowRun, type RunInCloudOptions } from '../cloud-run.js';
3
+ import { DirectInputError, isAuthoredFlowPath, parseDirectInput } from '../direct-input.js';
4
+ import { snapshotJsonValue } from '../json-value.js';
3
5
  import type { CliIo } from '../cli.js';
4
6
 
5
7
  /** Presentation only: the central CLI parser owns argv; the SDK owns the lifecycle. */
6
8
  export async function runCloudCli(
7
- { value: path, json, wait }: { value: string; json: boolean; wait: boolean },
9
+ { value: path, json, wait, input, syncCode }: {
10
+ value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean;
11
+ },
8
12
  io: CliIo,
9
13
  ): Promise<0 | 1 | 2> {
10
14
  const controller = new AbortController();
@@ -12,12 +16,36 @@ export async function runCloudCli(
12
16
  process.once('SIGINT', abort);
13
17
  process.once('SIGTERM', abort);
14
18
  let runId: string | undefined;
19
+ // Flips at the run submission. An interruption before it — during prepare,
20
+ // packing or upload — admitted nothing and is safe to retry; only an
21
+ // interrupted submission has unknown admission.
22
+ let submitting = false;
15
23
  try {
16
- const receipt = await runInCloud({ path }, { signal: controller.signal });
24
+ const options: RunInCloudOptions = { signal: controller.signal, onSubmit: () => { submitting = true; } };
25
+ if (isAuthoredFlowPath(path)) {
26
+ // Same parse as a local direct run, so a file-or-inline argument means
27
+ // the same thing on both sides of `--cloud`.
28
+ try {
29
+ options.input = snapshotJsonValue(parseDirectInput(input), 'Cloud authored input');
30
+ } catch (error) {
31
+ if (error instanceof DirectInputError) throw new CloudFlowError('invalid_input', error.message);
32
+ throw error;
33
+ }
34
+ }
35
+ // The tree is the invoking directory, as with v1: the flow path is where
36
+ // the body lives, not the boundary of what the run may read.
37
+ if (syncCode) options.syncCode = { root: process.cwd() };
38
+ const receipt = await runInCloud({ path }, options);
17
39
  runId = receipt.runId;
18
40
  if (!json) {
19
41
  io.stdout(`ACCEPTED ${receipt.runId} (${receipt.status})`);
20
42
  io.stdout(receipt.apiUrl);
43
+ if (receipt.synced) {
44
+ io.stdout(`SYNCED ${receipt.synced.files} files (${receipt.synced.bytes} bytes); pull changes with: flows sync ${receipt.runId}`);
45
+ for (const link of receipt.synced.skippedLinks) {
46
+ io.stderr(`WARNING [sync_link_skipped] ${link} points outside the synced tree and was not uploaded`);
47
+ }
48
+ }
21
49
  }
22
50
  if (!wait) {
23
51
  if (json) io.stdout(JSON.stringify({ ok: true, ...receipt }));
@@ -29,16 +57,19 @@ export async function runCloudCli(
29
57
  else io.stdout(`${run.status.toUpperCase()} ${run.runId} completionReason: ${'completionReason' in run ? run.completionReason : 'unavailable'}`);
30
58
  return ok ? 0 : 1;
31
59
  } catch (error) {
32
- const code = controller.signal.aborted ? (runId ? 'observation_aborted' : 'admission_unknown')
60
+ const code = controller.signal.aborted
61
+ ? runId ? 'observation_aborted' : submitting ? 'admission_unknown' : 'submission_aborted'
33
62
  : error instanceof CloudFlowError ? error.code : 'cloud_run_failed';
34
63
  const message = controller.signal.aborted
35
64
  ? runId ? 'Stopped observing; the hosted run has not been cancelled.'
36
- : 'Submission interrupted before a receipt was received. Admission is unknown; Cloud may have started the run. Do not resubmit blindly.'
65
+ : submitting
66
+ ? 'Submission interrupted before a receipt was received. Admission is unknown; Cloud may have started the run. Do not resubmit blindly.'
67
+ : 'Interrupted before the run was submitted; nothing was admitted. Safe to run again.'
37
68
  : error instanceof Error ? error.message : 'Cloud run failed.';
38
69
  if (json) io.stdout(JSON.stringify({ ok: false, code, message, ...(runId ? { runId } : {}) }));
39
70
  else io.stderr(`${code}: ${message}${runId ? ` (run ${runId})` : ''}`);
40
71
  return error instanceof CloudFlowError
41
- && (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code)
72
+ && (['configuration', 'unsupported_source', 'invalid_input', 'unsupported_storage_backend', 'sync_too_large', 'sync_unsupported'].includes(error.code)
42
73
  || (runId === undefined && error.code === 'http_error' && [401, 403].includes(error.status ?? 0))) ? 2 : 1;
43
74
  } finally {
44
75
  process.off('SIGINT', abort);
@@ -0,0 +1,37 @@
1
+ import { CloudFlowError } from '../cloud-http.js';
2
+ import { applyCloudPatch, downloadCloudPatch, patchedPaths } from '../cloud-sync.js';
3
+ import type { CliIo } from '../cli.js';
4
+
5
+ /**
6
+ * `flows sync <run-id>`: fetch the diff a hosted run left in its synced tree
7
+ * and apply it here. The patch is the sandbox's own `git diff` against the
8
+ * uploaded baseline, so applying it reproduces exactly what the run's steps
9
+ * wrote — no re-execution, no re-upload.
10
+ */
11
+ export async function runCloudSyncCli(
12
+ { runId, json, root }: { runId: string; json: boolean; root: string },
13
+ io: CliIo,
14
+ ): Promise<0 | 1 | 2> {
15
+ try {
16
+ const { patch, hasChanges } = await downloadCloudPatch(runId, {});
17
+ if (!hasChanges || !patch.trim()) {
18
+ if (json) io.stdout(JSON.stringify({ ok: true, runId, hasChanges: false, applied: false }));
19
+ else io.stdout(`NO CHANGES ${runId}`);
20
+ return 0;
21
+ }
22
+ applyCloudPatch(root, patch);
23
+ const files = patchedPaths(patch);
24
+ if (json) io.stdout(JSON.stringify({ ok: true, runId, hasChanges: true, applied: true, files }));
25
+ else {
26
+ io.stdout(`APPLIED ${runId}: ${files.length} file${files.length === 1 ? '' : 's'}${files.length ? `\n ${files.join('\n ')}` : ''}`);
27
+ io.stdout('Applied to the working tree, uncommitted: review with git diff before keeping it.');
28
+ }
29
+ return 0;
30
+ } catch (error) {
31
+ const code = error instanceof CloudFlowError ? error.code : 'cloud_sync_failed';
32
+ const message = error instanceof Error ? error.message : 'Cloud sync failed.';
33
+ if (json) io.stdout(JSON.stringify({ ok: false, code, message, runId }));
34
+ else io.stderr(`${code}: ${message} (run ${runId})`);
35
+ return error instanceof CloudFlowError && ['configuration', 'sync_unsupported', 'patch_conflict'].includes(error.code) ? 2 : 1;
36
+ }
37
+ }
package/src/cli/run.ts CHANGED
@@ -37,7 +37,7 @@ export interface ParkedStep {
37
37
  }
38
38
 
39
39
  export interface RunDiagnostic extends StepFailedDetails {
40
- severity: 'refusal' | 'failure' | 'parked' | 'warning';
40
+ severity: 'refusal' | 'failure' | 'parked' | 'warning' | 'declined';
41
41
  kind: RunFailureKind | RunWarningKind | RunCompletionReason;
42
42
  message: string;
43
43
  }
@@ -328,6 +328,17 @@ export function authoredCompletion(
328
328
  exitCode: 0,
329
329
  report: { ...common, ok: true, status: 'completed', completionReason: 'success' },
330
330
  };
331
+ case 'declined':
332
+ return {
333
+ exitCode: 0,
334
+ report: {
335
+ ...common, ok: true, status: 'completed', completionReason: 'success',
336
+ diagnostics: [...base.diagnostics, {
337
+ severity: 'declined', kind: 'run_declined',
338
+ message: 'Flow deliberately chose not to act on this input.',
339
+ }],
340
+ },
341
+ };
331
342
  case 'needs_human':
332
343
  return {
333
344
  exitCode: 3,
@@ -358,7 +369,7 @@ export function authoredCompletion(
358
369
  },
359
370
  };
360
371
  }
361
- // Exhaustive by construction. A fourth lowered completion has to choose its
372
+ // Exhaustive by construction. A new lowered completion has to choose its
362
373
  // own exit code and wording here; it must not inherit "its own checks did not
363
374
  // pass", which would state something the body never declared. Letting an
364
375
  // unlisted reason fall through to the failure branch is how a reporting-side
package/src/cli.ts CHANGED
@@ -24,6 +24,8 @@ import { runDirectFlow } from './cli/direct-run.js';
24
24
  import { parseReplayArgs, replayJournal, type ReplayArgs } from './cli/replay.js';
25
25
  import { checkTypeScriptFlow } from './cli/check-typescript.js';
26
26
  import { runCloudCli } from './cli/cloud-run.js';
27
+ import { runCloudSyncCli } from './cli/cloud-sync.js';
28
+ import { parseCloudDeployArgs, runCloudDeployCli, runCloudDeploymentsCli, runCloudUndeployCli, type CloudDeployArgs } from './cli/cloud-deploy.js';
27
29
  import { isAuthoredFlowPath } from './direct-input.js';
28
30
  import { parseDeployArgs, runDeploy, type DeployArgs } from './cli/deploy.js';
29
31
  import { parseDigestReference } from './bundle-transport.js';
@@ -51,7 +53,11 @@ type ParsedArgs =
51
53
  | BuildArgs
52
54
  | DeployArgs
53
55
  | { command: 'serve-webhook'; dataDir: string; port: number; admitted?: readonly string[] }
54
- | { command: 'cloud-run'; value: string; json: boolean; wait: boolean }
56
+ | { command: 'cloud-run'; value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean }
57
+ | { command: 'sync'; runId: string; json: boolean; root: string }
58
+ | CloudDeployArgs
59
+ | { command: 'deployments'; json: boolean }
60
+ | { command: 'undeploy'; agentId: string; json: boolean }
55
61
  | { command: 'check'; json: boolean; watch: boolean; value: string }
56
62
  | { command: 'run'; bucket: string | undefined; reuseFromRunId: string | undefined; localAgent: boolean; dataDir: string; input: string | undefined; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
57
63
  | { command: 'resume'; localAgent: boolean; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
@@ -66,12 +72,17 @@ const USAGE = [
66
72
  'flows add <helper-name|@flows/helper-name>',
67
73
  'flows build [--out <dir>] <flow.yaml|flow.ts>',
68
74
  'flows build --verify <bundle-dir>',
75
+ 'flows deploy <flow.ts> --repo <owner/name> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--json]',
76
+ 'flows deployments [--json]',
77
+ 'flows undeploy [--json] <deployment-id>',
69
78
  'flows deploy <flow>@sha256:<digest> --to <file-bucket-uri>',
70
79
  'flows run <flow>@sha256:<digest> [--bucket <file-bucket-uri>] [--data-dir <dir>] [--json]',
71
80
  'flows check [--watch] [--json] <flow.ts|flow.yaml|spec.json>',
72
81
  'flows serve-webhook --data-dir <dir> --port <p> [--allow <name>[,<name>]]',
73
82
  'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] [--reuse-from <run-id>] <flow.yaml|spec.json>',
74
- 'flows run --cloud [--json] [--wait] <flow.yaml|spec.json>',
83
+ 'flows run --cloud [--json] [--wait] [--sync-code] <flow.yaml|spec.json>',
84
+ 'flows run --cloud [--json] [--wait] [--sync-code] <flow.ts> --input <inline-json-or-file>',
85
+ 'flows sync [--json] [--dir <path>] <run-id>',
75
86
  'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <flow.ts> --input <inline-json-or-file>',
76
87
  'flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>',
77
88
  'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <run-id>',
@@ -116,6 +127,10 @@ export async function runCli(
116
127
  if (parsed.command === 'serve-webhook') return runServeWebhook(parsed, io);
117
128
 
118
129
  if (parsed.command === 'cloud-run') return runCloudCli(parsed, io);
130
+ if (parsed.command === 'sync') return runCloudSyncCli(parsed, io);
131
+ if (parsed.command === 'cloud-deploy') return runCloudDeployCli(parsed, io);
132
+ if (parsed.command === 'deployments') return runCloudDeploymentsCli(parsed, io);
133
+ if (parsed.command === 'undeploy') return runCloudUndeployCli(parsed, io);
119
134
  if (parsed.command === 'replay') return replayJournal(parsed, io);
120
135
  if (parsed.command === 'build') return runBuild(parsed, io);
121
136
  if (parsed.command === 'deploy') return runDeploy(parsed, io);
@@ -419,17 +434,35 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
419
434
  if (command === 'add') return args.length === 2 ? { command: 'add', value: args[1]! } : undefined;
420
435
  if (command === 'replay') return parseReplayArgs(args.slice(1));
421
436
  if (command === 'build') return parseBuildArgs(args.slice(1));
422
- if (command === 'deploy') return parseDeployArgs(args.slice(1));
437
+ if (command === 'deploy') {
438
+ // The positional decides the form: an authored source deploys a hosted
439
+ // listener; a digest reference copies a sealed bundle into a file bucket.
440
+ const source = args.slice(1).find(a => !a.startsWith('-') && isAuthoredFlowPath(a));
441
+ return source !== undefined ? parseCloudDeployArgs(args.slice(1)) : parseDeployArgs(args.slice(1));
442
+ }
443
+ if (command === 'undeploy') {
444
+ const rest = args.slice(1).filter(a => a !== '--json');
445
+ const json = args.length - 1 - rest.length;
446
+ if (json > 1 || rest.length !== 1 || rest[0]!.startsWith('-')) return undefined;
447
+ return { command: 'undeploy', agentId: rest[0]!, json: json === 1 };
448
+ }
449
+ if (command === 'deployments') {
450
+ const rest = args.slice(1);
451
+ if (rest.length > 1 || (rest.length === 1 && rest[0] !== '--json')) return undefined;
452
+ return { command: 'deployments', json: rest.length === 1 };
453
+ }
423
454
  if (command === 'serve-webhook') return parseWebhookArgs(args.slice(1));
424
455
  if (command === 'hn-monitor') return parseHnMonitorArgs(args.slice(1));
425
456
  if (command === 'tick') return parseTickArgs(args.slice(1));
426
457
  if (command === 'observer') return parseObserverArgs(args.slice(1));
458
+ if (command === 'sync') return parseSyncArgs(args.slice(1));
427
459
  if (command !== 'check' && command !== 'run' && command !== 'resume') return undefined;
428
460
 
429
461
  let json = false;
430
462
  let watch = false;
431
463
  let cloud = false;
432
464
  let wait = false;
465
+ let syncCode = false;
433
466
  let localAgent = false;
434
467
  let allowHumanInfluenced = false;
435
468
  let dataDir = DEFAULT_DATA_DIR;
@@ -443,10 +476,11 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
443
476
  const positionals: string[] = [];
444
477
  for (let index = 1; index < args.length; index += 1) {
445
478
  const argument = args[index]!;
446
- if (argument === '--cloud' || argument === '--wait') {
447
- if (command !== 'run' || (argument === '--cloud' ? cloud : wait)) return undefined;
479
+ if (argument === '--cloud' || argument === '--wait' || argument === '--sync-code') {
480
+ if (command !== 'run' || (argument === '--cloud' ? cloud : argument === '--wait' ? wait : syncCode)) return undefined;
448
481
  if (argument === '--cloud') cloud = true;
449
- else wait = true;
482
+ else if (argument === '--wait') wait = true;
483
+ else syncCode = true;
450
484
  continue;
451
485
  }
452
486
  if (argument === '--allow-human-influenced') {
@@ -520,13 +554,15 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
520
554
  if (bucket !== undefined && (cloud || !parseDigestReference(positionals[0]!))) return undefined;
521
555
  if (cloud) {
522
556
  // `--cloud` submits the spec to Cloud, so every flag that only describes a
523
- // local run -- an inline input, a data dir, a suppressed daemon, a local
524
- // agent, a local observer-link opt-out -- describes nothing there and is
525
- // refused rather than ignored.
526
- if (allowHumanInfluenced || sawInput || sawDataDir || !spawn || localAgent || noObserverLink || reuseFromRunId !== undefined) return undefined;
527
- return { command: 'cloud-run', value: positionals[0]!, json, wait };
557
+ // local run -- a data dir, a suppressed daemon, a local agent, a local
558
+ // observer-link opt-out -- describes nothing there and is refused rather
559
+ // than ignored. `--input` is the authored body's argument and travels with
560
+ // the source, so it is accepted exactly where a local run accepts it.
561
+ if (allowHumanInfluenced || sawDataDir || !spawn || localAgent || noObserverLink || reuseFromRunId !== undefined) return undefined;
562
+ if (sawInput && !isAuthoredFlowPath(positionals[0]!)) return undefined;
563
+ return { command: 'cloud-run', value: positionals[0]!, json, wait, input, syncCode };
528
564
  }
529
- if (wait) return undefined;
565
+ if (wait || syncCode) return undefined;
530
566
  if (reuseFromRunId !== undefined && isAuthoredFlowPath(positionals[0]!)) return undefined;
531
567
 
532
568
  if (command === 'run' && input !== undefined && !isAuthoredFlowPath(positionals[0]!)) return undefined;
@@ -579,6 +615,32 @@ function parseHnMonitorArgs(rest: readonly string[]): ParsedArgs | undefined {
579
615
  * directory at all -- the mint is a pure Relaycast API round-trip. No
580
616
  * positional argument, no other flags.
581
617
  */
618
+ /** `flows sync [--json] [--dir <path>] <run-id>`: apply a hosted run's patch to a local tree. */
619
+ function parseSyncArgs(args: readonly string[]): ParsedArgs | undefined {
620
+ let json = false;
621
+ let root: string | undefined;
622
+ const positionals: string[] = [];
623
+ for (let index = 0; index < args.length; index += 1) {
624
+ const argument = args[index]!;
625
+ if (argument === '--json') {
626
+ if (json) return undefined;
627
+ json = true;
628
+ continue;
629
+ }
630
+ if (argument === '--dir') {
631
+ const value = args[index + 1];
632
+ if (root !== undefined || value === undefined || value.startsWith('-')) return undefined;
633
+ root = value;
634
+ index += 1;
635
+ continue;
636
+ }
637
+ if (argument.startsWith('-')) return undefined;
638
+ positionals.push(argument);
639
+ }
640
+ if (positionals.length !== 1) return undefined;
641
+ return { command: 'sync', runId: positionals[0]!, json, root: root ?? '.' };
642
+ }
643
+
582
644
  function parseObserverArgs(rest: readonly string[]): ParsedArgs | undefined {
583
645
  let dataDir = DEFAULT_DATA_DIR;
584
646
  let sawDataDir = false;
@@ -750,6 +812,7 @@ function diagnosticLabel(severity: string): string {
750
812
  case 'warning': return 'WARNING';
751
813
  case 'failure': return 'FAILED';
752
814
  case 'parked': return 'PARKED';
815
+ case 'declined': return 'DECLINED';
753
816
  default: return 'REFUSED';
754
817
  }
755
818
  }