@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.
- package/dist/agent-artifacts.d.ts +20 -0
- package/dist/agent-artifacts.d.ts.map +1 -0
- package/dist/agent-artifacts.js +81 -0
- package/dist/agent-artifacts.js.map +1 -0
- package/dist/authored-flow-executor.d.ts +3 -3
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +7 -7
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-step-output.d.ts +2 -0
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +5 -1
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +19 -1
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check.js +6 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-deploy.d.ts +30 -0
- package/dist/cli/cloud-deploy.d.ts.map +1 -0
- package/dist/cli/cloud-deploy.js +162 -0
- package/dist/cli/cloud-deploy.js.map +1 -0
- package/dist/cli/cloud-run.d.ts +3 -1
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +37 -5
- package/dist/cli/cloud-run.js.map +1 -1
- package/dist/cli/cloud-sync.d.ts +13 -0
- package/dist/cli/cloud-sync.d.ts.map +1 -0
- package/dist/cli/cloud-sync.js +39 -0
- package/dist/cli/cloud-sync.js.map +1 -0
- package/dist/cli/run.d.ts +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +12 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +82 -12
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +70 -0
- package/dist/cloud-deploy.d.ts.map +1 -0
- package/dist/cloud-deploy.js +181 -0
- package/dist/cloud-deploy.js.map +1 -0
- package/dist/cloud-http.d.ts +37 -2
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +98 -8
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-run.d.ts +23 -0
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +30 -1
- package/dist/cloud-run.js.map +1 -1
- package/dist/cloud-sync.d.ts +66 -0
- package/dist/cloud-sync.d.ts.map +1 -0
- package/dist/cloud-sync.js +287 -0
- package/dist/cloud-sync.js.map +1 -0
- package/dist/failure-kinds.d.ts +8 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +8 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-artifacts.ts +76 -0
- package/src/authored-flow-executor.ts +8 -8
- package/src/authored-step-output.ts +6 -1
- package/src/authored-worker-step.ts +19 -1
- package/src/cli/check.ts +6 -1
- package/src/cli/cloud-deploy.ts +157 -0
- package/src/cli/cloud-run.ts +37 -6
- package/src/cli/cloud-sync.ts +37 -0
- package/src/cli/run.ts +13 -2
- package/src/cli.ts +75 -12
- package/src/cloud-deploy.ts +236 -0
- package/src/cloud-http.ts +129 -9
- package/src/cloud-run.ts +47 -1
- package/src/cloud-sync.ts +315 -0
- package/src/failure-kinds.ts +8 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/cli/cloud-run.ts
CHANGED
|
@@ -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 }: {
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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
|
|
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')
|
|
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 --
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
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
|
}
|