@relayflows/sdk 2.0.37 → 2.0.39
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-reaper.d.ts +1 -1
- package/dist/agent-reaper.d.ts.map +1 -1
- package/dist/agent-reaper.js +6 -4
- package/dist/agent-reaper.js.map +1 -1
- package/dist/authored-budget.d.ts +26 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +72 -2
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +7 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +3 -2
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +141 -6
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-worker-step.js +1 -0
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/child-stop.d.ts +3 -1
- package/dist/child-stop.d.ts.map +1 -1
- package/dist/child-stop.js +42 -2
- package/dist/child-stop.js.map +1 -1
- package/dist/cli/add-extension.js +1 -1
- package/dist/cli/add-extension.js.map +1 -1
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +25 -7
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +1 -0
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +72 -36
- package/dist/cli/cli-probe.js.map +1 -1
- package/dist/cli/cloud-deploy.d.ts +9 -1
- package/dist/cli/cloud-deploy.d.ts.map +1 -1
- package/dist/cli/cloud-deploy.js +24 -5
- package/dist/cli/cloud-deploy.js.map +1 -1
- package/dist/cli/cloud-versions.d.ts +22 -0
- package/dist/cli/cloud-versions.d.ts.map +1 -0
- package/dist/cli/cloud-versions.js +96 -0
- package/dist/cli/cloud-versions.js.map +1 -0
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +4 -1
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/pinned-cli-alias.d.ts +13 -0
- package/dist/cli/pinned-cli-alias.d.ts.map +1 -0
- package/dist/cli/pinned-cli-alias.js +58 -0
- package/dist/cli/pinned-cli-alias.js.map +1 -0
- package/dist/cli/run.d.ts +8 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +91 -8
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +18 -0
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +27 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +18 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +17 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +17 -0
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +8 -1
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +9 -2
- package/dist/cloud-run.js.map +1 -1
- package/dist/cloud-versions-wire.d.ts +25 -0
- package/dist/cloud-versions-wire.d.ts.map +1 -0
- package/dist/cloud-versions-wire.js +39 -0
- package/dist/cloud-versions-wire.js.map +1 -0
- package/dist/cloud-versions.d.ts +47 -0
- package/dist/cloud-versions.d.ts.map +1 -0
- package/dist/cloud-versions.js +131 -0
- package/dist/cloud-versions.js.map +1 -0
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +24 -6
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +16 -7
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +9 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +6 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-extension-loader.d.ts +1 -1
- package/dist/flow-extension-loader.d.ts.map +1 -1
- package/dist/flow-extension-loader.js +143 -14
- package/dist/flow-extension-loader.js.map +1 -1
- package/dist/flow-extension-manifest.d.ts +1 -0
- package/dist/flow-extension-manifest.d.ts.map +1 -1
- package/dist/flow-extension-manifest.js +12 -3
- package/dist/flow-extension-manifest.js.map +1 -1
- package/dist/hosted-extension-manifest.d.ts.map +1 -1
- package/dist/hosted-extension-manifest.js +25 -4
- package/dist/hosted-extension-manifest.js.map +1 -1
- package/dist/hosted-extension-runtime.js +1 -1
- package/dist/hosted-extension-sandbox.js +2 -2
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +4 -2
- package/dist/llm-worker.js.map +1 -1
- package/dist/mcp-stdio.d.ts +2 -1
- package/dist/mcp-stdio.d.ts.map +1 -1
- package/dist/mcp-stdio.js +38 -21
- package/dist/mcp-stdio.js.map +1 -1
- package/dist/preflight.d.ts +2 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +9 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/resolved-cli-identity.d.ts +14 -0
- package/dist/resolved-cli-identity.d.ts.map +1 -0
- package/dist/resolved-cli-identity.js +19 -0
- package/dist/resolved-cli-identity.js.map +1 -0
- package/dist/worker-cli.d.ts +3 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +116 -40
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-lease.d.ts +3 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +47 -9
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +4 -2
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-session.d.ts +3 -1
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +93 -53
- package/dist/wrapper-session.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-reaper.ts +7 -4
- package/src/authored-budget.ts +73 -1
- package/src/authored-flow-error.ts +6 -0
- package/src/authored-node-runner.ts +3 -2
- package/src/authored-root.ts +150 -7
- package/src/authored-worker-step.ts +1 -0
- package/src/child-stop.ts +40 -1
- package/src/cli/add-extension.ts +1 -1
- package/src/cli/check.ts +25 -4
- package/src/cli/cli-probe.ts +73 -37
- package/src/cli/cloud-deploy.ts +21 -7
- package/src/cli/cloud-versions.ts +103 -0
- package/src/cli/direct-run.ts +4 -1
- package/src/cli/pinned-cli-alias.ts +74 -0
- package/src/cli/run.ts +103 -7
- package/src/cli/step-failure.ts +18 -0
- package/src/cli-commands.ts +18 -0
- package/src/cli.ts +15 -0
- package/src/cloud-deploy.ts +24 -3
- package/src/cloud-run.ts +9 -2
- package/src/cloud-versions-wire.ts +58 -0
- package/src/cloud-versions.ts +179 -0
- package/src/communication/worker.ts +24 -9
- package/src/compile.ts +28 -13
- package/src/failure-kinds.ts +14 -0
- package/src/flow-extension-loader.ts +154 -14
- package/src/flow-extension-manifest.ts +6 -3
- package/src/hosted-extension-manifest.ts +28 -5
- package/src/hosted-extension-runtime.ts +1 -1
- package/src/hosted-extension-sandbox.ts +2 -2
- package/src/llm-worker.ts +6 -4
- package/src/mcp-stdio.ts +34 -20
- package/src/preflight.ts +2 -0
- package/src/protocol.ts +9 -0
- package/src/resolved-cli-identity.ts +42 -0
- package/src/worker-cli.ts +123 -37
- package/src/worker-lease.ts +50 -9
- package/src/worker.ts +6 -4
- package/src/wrapper-session.ts +89 -56
package/src/cli/cloud-deploy.ts
CHANGED
|
@@ -4,13 +4,17 @@ import {
|
|
|
4
4
|
type FlowTriggerSource,
|
|
5
5
|
} from '../cloud-deploy.js';
|
|
6
6
|
import { describeFlowRequirements } from '../flow-requirements.js';
|
|
7
|
+
import { describeVersionChange } from '../cloud-versions-wire.js';
|
|
8
|
+
import { runCloudDeployVersionCli } from './cloud-versions.js';
|
|
7
9
|
import { cliConnectPrompt, flowRequirementsForPath, harnessRemedy } from './cloud-connect-cli.js';
|
|
8
10
|
import type { CliIo } from '../cli.js';
|
|
9
11
|
|
|
10
12
|
export interface CloudDeployArgs {
|
|
11
13
|
command: 'cloud-deploy';
|
|
12
14
|
value: string;
|
|
13
|
-
|
|
15
|
+
/** Update form: the flow (name or listener id) this source becomes the next version of. */
|
|
16
|
+
flow: string | undefined;
|
|
17
|
+
repo: string | undefined;
|
|
14
18
|
on: string[];
|
|
15
19
|
approver: string | undefined;
|
|
16
20
|
name: string | undefined;
|
|
@@ -25,6 +29,11 @@ export interface CloudDeployArgs {
|
|
|
25
29
|
/**
|
|
26
30
|
* `flows deploy <flow.ts> --repo <owner/name> --on <provider>[:k=v,…] [--on …]
|
|
27
31
|
* --approver <handle> [--name <n>] [--agents <list>] [--draft] [--no-connect] [--json]`
|
|
32
|
+
* `flows deploy <flow.ts> --flow <name-or-listener-id> [--plugin <ref>] [--no-connect] [--json]`
|
|
33
|
+
*
|
|
34
|
+
* The `--flow` form makes the source the next version of an existing flow and
|
|
35
|
+
* changes nothing else; listener settings given beside it are refused when run,
|
|
36
|
+
* by name, rather than silently ignored.
|
|
28
37
|
*
|
|
29
38
|
* Parsed here rather than in `parseDeployArgs` because the two `deploy` forms
|
|
30
39
|
* share nothing but the word: the digest form copies a sealed bundle into a
|
|
@@ -32,6 +41,7 @@ export interface CloudDeployArgs {
|
|
|
32
41
|
*/
|
|
33
42
|
export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs | undefined {
|
|
34
43
|
let value: string | undefined;
|
|
44
|
+
let flow: string | undefined;
|
|
35
45
|
let repo: string | undefined;
|
|
36
46
|
let approver: string | undefined;
|
|
37
47
|
let name: string | undefined;
|
|
@@ -72,11 +82,12 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
72
82
|
i += 1;
|
|
73
83
|
continue;
|
|
74
84
|
}
|
|
75
|
-
if (arg === '--repo' || arg === '--approver' || arg === '--name' || arg === '--on') {
|
|
85
|
+
if (arg === '--repo' || arg === '--approver' || arg === '--name' || arg === '--on' || arg === '--flow') {
|
|
76
86
|
const next = args[i + 1];
|
|
77
87
|
if (next === undefined || next.startsWith('-')) return undefined;
|
|
78
88
|
i += 1;
|
|
79
89
|
if (arg === '--on') { on.push(next); continue; }
|
|
90
|
+
if (arg === '--flow') { if (flow !== undefined) return undefined; flow = next; continue; }
|
|
80
91
|
if (arg === '--repo') { if (repo !== undefined) return undefined; repo = next; continue; }
|
|
81
92
|
if (arg === '--approver') { if (approver !== undefined) return undefined; approver = next; continue; }
|
|
82
93
|
if (name !== undefined) return undefined;
|
|
@@ -86,8 +97,9 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
86
97
|
if (arg.startsWith('-') || value !== undefined) return undefined;
|
|
87
98
|
value = arg;
|
|
88
99
|
}
|
|
89
|
-
if (value === undefined
|
|
90
|
-
|
|
100
|
+
if (value === undefined) return undefined;
|
|
101
|
+
if (flow === undefined && (repo === undefined || on.length === 0)) return undefined;
|
|
102
|
+
return { command: 'cloud-deploy', value, flow, repo, on, approver, name, agents, draft, noConnect, json, plugins };
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
function describeSource(source: FlowTriggerSource): string {
|
|
@@ -96,6 +108,7 @@ function describeSource(source: FlowTriggerSource): string {
|
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promise<0 | 1 | 2> {
|
|
111
|
+
if (args.flow !== undefined) return runCloudDeployVersionCli({ ...args, flow: args.flow }, io);
|
|
99
112
|
const agents = args.agents === undefined ? undefined : parseAgentHarnessesOr(args.agents);
|
|
100
113
|
// What the source declares, so a harness refusal names the right remedy
|
|
101
114
|
// even when `--agents` was not given; the loader's own failure is reported
|
|
@@ -111,7 +124,7 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
111
124
|
if (agents === undefined) harnesses = (await flowRequirementsForPath(args.value))?.harnesses ?? [];
|
|
112
125
|
const deployment = await deployToCloud({
|
|
113
126
|
path: args.value,
|
|
114
|
-
repository: parseRepository(args.repo),
|
|
127
|
+
repository: parseRepository(args.repo!),
|
|
115
128
|
sources: args.on.map(parseTriggerSource),
|
|
116
129
|
approver: args.approver,
|
|
117
130
|
draft: args.draft,
|
|
@@ -124,7 +137,8 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
124
137
|
io.stdout(JSON.stringify({ ok: true, ...deployment }));
|
|
125
138
|
return 0;
|
|
126
139
|
}
|
|
127
|
-
io.stdout(`${deployment.status === 'draft' ? 'SAVED' : 'DEPLOYED'} ${deployment.agentId} ${deployment.status}`
|
|
140
|
+
io.stdout(`${deployment.status === 'draft' ? 'SAVED' : 'DEPLOYED'} ${deployment.agentId} ${deployment.status}`
|
|
141
|
+
+ (deployment.version === undefined ? '' : ` · ${describeVersionChange(deployment.version)}`));
|
|
128
142
|
io.stdout(` flow: ${deployment.name} (${args.value}, sha256 ${deployment.sourceSha256.slice(0, 12)})`);
|
|
129
143
|
io.stdout(` repository: ${deployment.repository.owner}/${deployment.repository.name}`);
|
|
130
144
|
for (const source of deployment.sources) io.stdout(` on: ${describeSource(source)}`);
|
|
@@ -181,7 +195,7 @@ export async function runCloudUndeployCli({ agentId, json }: { agentId: string;
|
|
|
181
195
|
}
|
|
182
196
|
}
|
|
183
197
|
|
|
184
|
-
function reportCloudFailure(error: unknown, json: boolean, io: CliIo, harnesses: readonly string[] = []): 1 | 2 {
|
|
198
|
+
export function reportCloudFailure(error: unknown, json: boolean, io: CliIo, harnesses: readonly string[] = []): 1 | 2 {
|
|
185
199
|
const code = error instanceof CloudFlowError ? error.code : 'cloud_deploy_failed';
|
|
186
200
|
let message = error instanceof Error ? error.message : 'Cloud deploy failed.';
|
|
187
201
|
// Cloud names the missing coding-agent credential on activation; say how it is connected.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { CloudFlowError } from '../cloud-http.js';
|
|
2
|
+
import {
|
|
3
|
+
activateCloudFlowVersion, deployVersionToCloud, getCloudListener, resolveCloudFlow,
|
|
4
|
+
} from '../cloud-versions.js';
|
|
5
|
+
import { describeVersionChange } from '../cloud-versions-wire.js';
|
|
6
|
+
import { describeFlowRequirements } from '../flow-requirements.js';
|
|
7
|
+
import { cliConnectPrompt } from './cloud-connect-cli.js';
|
|
8
|
+
import { reportCloudFailure, type CloudDeployArgs } from './cloud-deploy.js';
|
|
9
|
+
import type { CliIo } from '../cli.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `flows deploy <flow.ts> --flow <name-or-id>`: the source becomes the next
|
|
13
|
+
* version of that flow. Listener settings are not part of a version, so a
|
|
14
|
+
* flag that would change one is refused by name instead of being dropped.
|
|
15
|
+
*/
|
|
16
|
+
export async function runCloudDeployVersionCli(
|
|
17
|
+
args: CloudDeployArgs & { flow: string }, io: CliIo,
|
|
18
|
+
): Promise<0 | 1 | 2> {
|
|
19
|
+
try {
|
|
20
|
+
const settings = [
|
|
21
|
+
args.repo === undefined ? undefined : '--repo',
|
|
22
|
+
args.on.length === 0 ? undefined : '--on',
|
|
23
|
+
args.approver === undefined ? undefined : '--approver',
|
|
24
|
+
args.name === undefined ? undefined : '--name',
|
|
25
|
+
args.agents === undefined ? undefined : '--agents',
|
|
26
|
+
args.draft ? '--draft' : undefined,
|
|
27
|
+
].filter((flag): flag is string => flag !== undefined);
|
|
28
|
+
if (settings.length > 0) {
|
|
29
|
+
throw new CloudFlowError('invalid_input',
|
|
30
|
+
`--flow deploys a new version of the flow's source only; ${settings.join(', ')} stay on the listener. `
|
|
31
|
+
+ 'Drop them, or change them in the Cloud dashboard.');
|
|
32
|
+
}
|
|
33
|
+
const connect = cliConnectPrompt(io, { noConnect: args.noConnect, json: args.json });
|
|
34
|
+
const deployment = await deployVersionToCloud({
|
|
35
|
+
path: args.value,
|
|
36
|
+
flow: args.flow,
|
|
37
|
+
...(connect === undefined ? {} : { connect }),
|
|
38
|
+
...(args.plugins.length === 0 ? {} : { plugins: args.plugins }),
|
|
39
|
+
});
|
|
40
|
+
if (args.json) {
|
|
41
|
+
io.stdout(JSON.stringify({ ok: true, ...deployment }));
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
io.stdout(`${deployment.status === 'draft' ? 'SAVED' : 'DEPLOYED'} ${deployment.agentId} ${deployment.status}`
|
|
45
|
+
+ ` · ${describeVersionChange(deployment.version)}`);
|
|
46
|
+
io.stdout(` flow: ${deployment.name} (${args.value}, sha256 ${deployment.sourceSha256.slice(0, 12)})`);
|
|
47
|
+
const requires = describeFlowRequirements(deployment.requirements);
|
|
48
|
+
if (requires) io.stdout(` requires: ${requires}`);
|
|
49
|
+
for (const provider of deployment.connected) io.stdout(` connected: ${provider}`);
|
|
50
|
+
io.stdout('New runs launch this version; runs already started finish on theirs. History: flows versions '
|
|
51
|
+
+ JSON.stringify(deployment.name));
|
|
52
|
+
return 0;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return reportCloudFailure(error, args.json, io);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `flows versions <name-or-id>`: newest first, the active one marked. */
|
|
59
|
+
export async function runCloudVersionsCli(
|
|
60
|
+
{ flow, json }: { flow: string; json: boolean }, io: CliIo,
|
|
61
|
+
): Promise<0 | 1 | 2> {
|
|
62
|
+
try {
|
|
63
|
+
const listener = await getCloudListener(await resolveCloudFlow(flow));
|
|
64
|
+
if (json) {
|
|
65
|
+
io.stdout(JSON.stringify({
|
|
66
|
+
ok: true, agentId: listener.agentId, name: listener.name,
|
|
67
|
+
activeVersion: listener.activeVersion?.version ?? null, versions: listener.versions,
|
|
68
|
+
}));
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (listener.versions.length === 0) {
|
|
72
|
+
io.stdout(`${listener.agentId} ${JSON.stringify(listener.name)} has no recorded versions.`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
io.stdout(`${listener.agentId} ${JSON.stringify(listener.name)} ${listener.status}`);
|
|
76
|
+
for (const version of listener.versions) {
|
|
77
|
+
const active = version.version === listener.activeVersion?.version ? ' (active)' : '';
|
|
78
|
+
io.stdout(` version ${version.version}${active} ${version.createdAt} ${version.origin} sha256 ${version.sourceSha256.slice(0, 12)}`);
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return reportCloudFailure(error, json, io);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** `flows rollback <name-or-id> <version>`: move the pointer; later versions stay recorded. */
|
|
87
|
+
export async function runCloudRollbackCli(
|
|
88
|
+
{ flow, version, json }: { flow: string; version: string; json: boolean }, io: CliIo,
|
|
89
|
+
): Promise<0 | 1 | 2> {
|
|
90
|
+
try {
|
|
91
|
+
// Cloud numbers versions up to nine digits; anything longer cannot exist.
|
|
92
|
+
if (!/^[1-9][0-9]{0,8}$/u.test(version)) {
|
|
93
|
+
throw new CloudFlowError('invalid_input', `A version is a positive whole number, got "${version}".`);
|
|
94
|
+
}
|
|
95
|
+
const result = await activateCloudFlowVersion(flow, Number(version));
|
|
96
|
+
io.stdout(json
|
|
97
|
+
? JSON.stringify({ ok: true, ...result })
|
|
98
|
+
: `ACTIVATED ${result.agentId} ${result.status} · ${describeVersionChange(result.version)}`);
|
|
99
|
+
return 0;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return reportCloudFailure(error, json, io);
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/cli/direct-run.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { authoredInput, authoredWorkerRemedy, localAgentRemedy } from './local-a
|
|
|
21
21
|
import {
|
|
22
22
|
authoredCompletion,
|
|
23
23
|
authoredHumanParked,
|
|
24
|
-
authoredStepFailure,
|
|
24
|
+
authoredStepFailure, rootLeaseLostReport,
|
|
25
25
|
connect,
|
|
26
26
|
emptyReport,
|
|
27
27
|
fromCheckReport,
|
|
@@ -212,6 +212,9 @@ export async function runDirectFlow(
|
|
|
212
212
|
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
|
|
213
213
|
return authoredStepFailure('run', base, socketPath, error);
|
|
214
214
|
}
|
|
215
|
+
if (error instanceof AuthoredFlowExecutionError && error.code === 'root_lease_lost') {
|
|
216
|
+
return rootLeaseLostReport('run', base, socketPath, error);
|
|
217
|
+
}
|
|
215
218
|
if (error instanceof AuthoredHumanParked) {
|
|
216
219
|
return authoredHumanParked('run', base, socketPath, error, { dataDir, localAgent: options.localAgent === true });
|
|
217
220
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdtempSync,
|
|
3
|
+
rmSync,
|
|
4
|
+
symlinkSync,
|
|
5
|
+
} from 'node:fs';
|
|
6
|
+
import {
|
|
7
|
+
mkdtemp,
|
|
8
|
+
rm,
|
|
9
|
+
symlink,
|
|
10
|
+
} from 'node:fs/promises';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { basename, join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export interface PinnedCliAlias {
|
|
15
|
+
executable: string;
|
|
16
|
+
release(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PinnedCliAliasSync {
|
|
20
|
+
executable: string;
|
|
21
|
+
release(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function aliasName(identity: string): string {
|
|
25
|
+
const name = basename(identity);
|
|
26
|
+
if (name.length === 0 || name === '.' || name === '..') {
|
|
27
|
+
throw new Error(`Invalid CLI identity basename: ${JSON.stringify(identity)}`);
|
|
28
|
+
}
|
|
29
|
+
return name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Execute pinned canonical bytes through the proved basename, including shebang scripts. */
|
|
33
|
+
export async function pinCliAlias(executable: string, identity: string): Promise<PinnedCliAlias> {
|
|
34
|
+
const name = aliasName(identity);
|
|
35
|
+
if (basename(executable) === name) {
|
|
36
|
+
return { executable, release() {} };
|
|
37
|
+
}
|
|
38
|
+
const directory = await mkdtemp(join(tmpdir(), 'relayflow-cli-'));
|
|
39
|
+
const alias = join(directory, name);
|
|
40
|
+
try {
|
|
41
|
+
await symlink(executable, alias, 'file');
|
|
42
|
+
} catch (error) {
|
|
43
|
+
await rm(directory, { recursive: true, force: true });
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
let released = false;
|
|
47
|
+
return { executable: alias, release: () => {
|
|
48
|
+
if (released) return;
|
|
49
|
+
rmSync(directory, { recursive: true, force: true });
|
|
50
|
+
released = true;
|
|
51
|
+
} };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Synchronous counterpart for the synchronous preflight contract. */
|
|
55
|
+
export function pinCliAliasSync(executable: string, identity: string): PinnedCliAliasSync {
|
|
56
|
+
const name = aliasName(identity);
|
|
57
|
+
if (basename(executable) === name) {
|
|
58
|
+
return { executable, release() {} };
|
|
59
|
+
}
|
|
60
|
+
const directory = mkdtempSync(join(tmpdir(), 'relayflow-cli-'));
|
|
61
|
+
const alias = join(directory, name);
|
|
62
|
+
try {
|
|
63
|
+
symlinkSync(executable, alias, 'file');
|
|
64
|
+
} catch (error) {
|
|
65
|
+
rmSync(directory, { recursive: true, force: true });
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
let released = false;
|
|
69
|
+
return { executable: alias, release: () => {
|
|
70
|
+
if (released) return;
|
|
71
|
+
rmSync(directory, { recursive: true, force: true });
|
|
72
|
+
released = true;
|
|
73
|
+
} };
|
|
74
|
+
}
|
package/src/cli/run.ts
CHANGED
|
@@ -408,6 +408,9 @@ export async function resumeFlow(
|
|
|
408
408
|
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
|
|
409
409
|
return authoredStepFailure('resume', base, socketPath, error, runId);
|
|
410
410
|
}
|
|
411
|
+
if (error instanceof AuthoredFlowExecutionError && error.code === 'root_lease_lost') {
|
|
412
|
+
return rootLeaseLostReport('resume', base, socketPath, error, runId);
|
|
413
|
+
}
|
|
411
414
|
// An authored resume that parks for want of a worker is a park, reported
|
|
412
415
|
// like the run path's — and with the same remedy, because an authored root
|
|
413
416
|
// admits its worker at run start: resuming again cannot attach one, so the
|
|
@@ -522,6 +525,38 @@ function authoredInputArgument(metadata: AuthoredRootMetadata | undefined): Auth
|
|
|
522
525
|
* drove correctly, and it produces a report with no `status`, which is the
|
|
523
526
|
* whole of what `RUN <id> unknown` ever meant.
|
|
524
527
|
*/
|
|
528
|
+
/**
|
|
529
|
+
* The authored root's lease was lost and not re-dispatched to this process
|
|
530
|
+
* (authored-root.ts driveRootFollowingRetries). Exit 1 because this CLI did
|
|
531
|
+
* not finish the run, but the report says what is true of it: still
|
|
532
|
+
* `running` in the journal, and resumable -- never `protocol_error` with the
|
|
533
|
+
* generic "worker wait canceled" and no run id, which is what it was.
|
|
534
|
+
*/
|
|
535
|
+
export function rootLeaseLostReport(
|
|
536
|
+
command: RunCommand,
|
|
537
|
+
base: CheckReport | RunReport,
|
|
538
|
+
socketPath: string,
|
|
539
|
+
error: AuthoredFlowExecutionError,
|
|
540
|
+
fallbackRunId?: string,
|
|
541
|
+
): RunExecution {
|
|
542
|
+
const rootRunId = error.rootRunId ?? fallbackRunId;
|
|
543
|
+
return {
|
|
544
|
+
exitCode: 1,
|
|
545
|
+
report: {
|
|
546
|
+
...fromBase(command, base),
|
|
547
|
+
ok: false,
|
|
548
|
+
...(rootRunId === undefined ? {} : { runId: rootRunId, rootRunId }),
|
|
549
|
+
socketPath,
|
|
550
|
+
status: 'running',
|
|
551
|
+
diagnostics: [...base.diagnostics, {
|
|
552
|
+
severity: 'failure',
|
|
553
|
+
kind: 'root_lease_lost',
|
|
554
|
+
message: error.message.replace(/^root_lease_lost: /, ''),
|
|
555
|
+
}],
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
525
560
|
export function authoredStepFailure(
|
|
526
561
|
command: RunCommand,
|
|
527
562
|
base: CheckReport | RunReport,
|
|
@@ -798,13 +833,27 @@ export async function classifyOutcome(
|
|
|
798
833
|
let parkedStep: ParkedStep | undefined;
|
|
799
834
|
let needsHuman = false;
|
|
800
835
|
let unclassifiedPolls = 0;
|
|
801
|
-
|
|
836
|
+
// `running` too: an authored body re-driven after a lost root lease re-admits
|
|
837
|
+
// its child by admission key and gets the EXISTING run back, mid-attempt.
|
|
838
|
+
// Only `parked` was followed, so that child -- on the attempt 2 the kernel
|
|
839
|
+
// retried it to -- fell straight through to "status running without a
|
|
840
|
+
// classifiable completion" and failed the flow (customer rw_3a0fcb71).
|
|
841
|
+
while (current.status === 'parked' || current.status === 'running') {
|
|
802
842
|
const inspection = await inspectOutOfBandStep(client, current.run_id);
|
|
803
843
|
// A runnable sibling may only be waiting for capacity held by a live
|
|
804
|
-
// attempt. Keep driving until that
|
|
805
|
-
|
|
844
|
+
// attempt or by a retry in backoff. Keep driving until that active work
|
|
845
|
+
// settles before declaring a park.
|
|
846
|
+
if (inspection?.parkedStep !== undefined && (inspection.needsHuman
|
|
847
|
+
|| (inspection.runningStep === undefined && inspection.backoffStep === undefined))) {
|
|
806
848
|
parkedStep = inspection.parkedStep;
|
|
807
849
|
needsHuman = inspection.needsHuman;
|
|
850
|
+
// `run.get` reports the live snapshot as `running` for an ordinary
|
|
851
|
+
// runnable worker wait, even when `run.start`/`run.resume` already
|
|
852
|
+
// classified the outcome as parked. Conversely, a re-admitted run can
|
|
853
|
+
// arrive here with a stale `running` outcome after its worker vanished.
|
|
854
|
+
// The step-level inspection is authoritative that this is a park; do
|
|
855
|
+
// not copy its broader run status and lose that classification.
|
|
856
|
+
current = { ...current, status: 'parked' };
|
|
808
857
|
break;
|
|
809
858
|
}
|
|
810
859
|
if (inspection?.runningStep !== undefined) {
|
|
@@ -812,6 +861,20 @@ export async function classifyOutcome(
|
|
|
812
861
|
current = await client.runResume(current.run_id, command === 'run' || options.allowHumanInfluenced);
|
|
813
862
|
continue;
|
|
814
863
|
}
|
|
864
|
+
// A step in `backoff` is between attempts: the kernel has scheduled the
|
|
865
|
+
// next one (a dead worker's retry is due at once, but waits for the
|
|
866
|
+
// worker slot the dead attempt held; a failed attempt waits out the
|
|
867
|
+
// retry policy's delay, bounded by its max_backoff_ms). That is progress,
|
|
868
|
+
// not an unclassifiable state -- treating it as one gave up after 2s on a
|
|
869
|
+
// child whose attempt 2 then succeeded (customer rw_3a0fcb71). Once due,
|
|
870
|
+
// the step is dispatched (`running`, followed above) or, with no worker,
|
|
871
|
+
// left `runnable` and reported as a park.
|
|
872
|
+
if (inspection?.backoffStep !== undefined && inspection.runningStep === undefined) {
|
|
873
|
+
throwIfCanceled(options.signal, inspection.backoffStep.id);
|
|
874
|
+
await delay(EXPIRED_LEASE_POLL_MS, options.signal);
|
|
875
|
+
current = await client.runResume(current.run_id, command === 'run' || options.allowHumanInfluenced);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
815
878
|
if (inspection?.status === 'completed' || inspection?.status === 'failed') {
|
|
816
879
|
current = await client.runResume(current.run_id, command === 'run' || options.allowHumanInfluenced);
|
|
817
880
|
continue;
|
|
@@ -954,6 +1017,8 @@ interface OutOfBandInspection {
|
|
|
954
1017
|
parkedStep?: ParkedStep;
|
|
955
1018
|
needsHuman: boolean;
|
|
956
1019
|
runningStep?: RunningStep;
|
|
1020
|
+
/** A step waiting out a retry backoff: its next attempt is scheduled. */
|
|
1021
|
+
backoffStep?: ParkedStep;
|
|
957
1022
|
}
|
|
958
1023
|
|
|
959
1024
|
interface RunningStep extends ParkedStep {
|
|
@@ -981,6 +1046,9 @@ async function inspectOutOfBandStep(
|
|
|
981
1046
|
const runningEntry = entries.find(([, step]) =>
|
|
982
1047
|
step.type !== 'deterministic' && step.state === 'running',
|
|
983
1048
|
);
|
|
1049
|
+
const backoffEntry = entries.find(([, step]) =>
|
|
1050
|
+
step.type !== 'deterministic' && step.state === 'backoff',
|
|
1051
|
+
);
|
|
984
1052
|
const parkedEntry = humanEntry ?? runnableEntry;
|
|
985
1053
|
const parkedStep = parkedEntry === undefined ? undefined : {
|
|
986
1054
|
id: parkedEntry[0],
|
|
@@ -996,11 +1064,21 @@ async function inspectOutOfBandStep(
|
|
|
996
1064
|
needsHuman: humanEntry !== undefined,
|
|
997
1065
|
...(parkedStep !== undefined ? { parkedStep } : {}),
|
|
998
1066
|
...(runningStep !== undefined ? { runningStep } : {}),
|
|
1067
|
+
...(backoffEntry !== undefined ? { backoffStep: {
|
|
1068
|
+
id: backoffEntry[0],
|
|
1069
|
+
type: backoffEntry[1].type as Extract<StepType, 'llm' | 'agent'>,
|
|
1070
|
+
} } : {}),
|
|
999
1071
|
};
|
|
1000
1072
|
}
|
|
1001
1073
|
|
|
1002
1074
|
// Match kernel/relayflowd/src/server/client.rs: allow the lease sweep to dispatch a retry.
|
|
1003
1075
|
export const LEASE_SWEEP_GRACE_MS = 5_000;
|
|
1076
|
+
// How often to re-ask the daemon about a step whose lease looks expired here.
|
|
1077
|
+
const EXPIRED_LEASE_POLL_MS = 250;
|
|
1078
|
+
// A deadline unchanged for a whole lease (relayflowd LEASE_RENEWAL_MS, 30s)
|
|
1079
|
+
// plus the sweep grace was renewed by nobody: the attempt is dead and the
|
|
1080
|
+
// daemon never swept it. Fail rather than hang.
|
|
1081
|
+
const STALE_LEASE_MS = 30_000 + LEASE_SWEEP_GRACE_MS;
|
|
1004
1082
|
|
|
1005
1083
|
async function waitForRunningStep(
|
|
1006
1084
|
client: JournalClient,
|
|
@@ -1018,20 +1096,29 @@ async function waitForRunningStep(
|
|
|
1018
1096
|
stepType: runningStep.type,
|
|
1019
1097
|
leaseDeadlineMs,
|
|
1020
1098
|
});
|
|
1099
|
+
// When the lease deadline last changed, on this process's monotonic clock.
|
|
1100
|
+
// A live worker renews every ~10s, moving the deadline; a dead lease the
|
|
1101
|
+
// daemon has not swept keeps it. That -- not a comparison of the daemon's
|
|
1102
|
+
// deadline with our wall clock -- is what separates the two under skew.
|
|
1103
|
+
let deadlineSeenAt = performance.now();
|
|
1021
1104
|
while (true) {
|
|
1022
1105
|
throwIfCanceled(options.signal, runningStep.id);
|
|
1023
1106
|
const remainingMs = leaseDeadlineMs + LEASE_SWEEP_GRACE_MS - Date.now();
|
|
1024
|
-
if (
|
|
1107
|
+
if (performance.now() - deadlineSeenAt > STALE_LEASE_MS) {
|
|
1025
1108
|
throw new Error(
|
|
1026
1109
|
`worker lease for step "${runningStep.id}" expired at ${leaseDeadlineMs} without completion`,
|
|
1027
1110
|
);
|
|
1028
1111
|
}
|
|
1029
|
-
|
|
1112
|
+
// Past the deadline by THIS clock alone is not an expiry: the daemon may
|
|
1113
|
+
// still hold the lease (skew) or be sweeping it into a retry. Keep
|
|
1114
|
+
// following the step, at a slower poll, until the daemon says otherwise.
|
|
1115
|
+
await delay(remainingMs <= 0 ? EXPIRED_LEASE_POLL_MS : Math.min(50, remainingMs), options.signal);
|
|
1030
1116
|
const snapshot = await client.runGet(runId);
|
|
1031
1117
|
const step = snapshot.steps[runningStep.id];
|
|
1032
1118
|
if (step?.state !== 'running') return;
|
|
1033
1119
|
if (step.lease_deadline_ms !== undefined && step.lease_deadline_ms !== leaseDeadlineMs) {
|
|
1034
1120
|
leaseDeadlineMs = step.lease_deadline_ms;
|
|
1121
|
+
deadlineSeenAt = performance.now();
|
|
1035
1122
|
options.onWait?.({
|
|
1036
1123
|
runId,
|
|
1037
1124
|
stepId: runningStep.id,
|
|
@@ -1098,7 +1185,14 @@ function errorMessage(error: unknown): string {
|
|
|
1098
1185
|
}
|
|
1099
1186
|
|
|
1100
1187
|
function throwIfCanceled(signal: AbortSignal | undefined, stepId: string): void {
|
|
1101
|
-
if (signal?.aborted === true)
|
|
1188
|
+
if (signal?.aborted === true) {
|
|
1189
|
+
throw new Error(`waiting for running step "${stepId}" was canceled${cancelCause(signal)}`, { cause: signal.reason });
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/** Why a wait was canceled, when whoever aborted it said (a lost lease does). */
|
|
1194
|
+
function cancelCause(signal: AbortSignal): string {
|
|
1195
|
+
return signal.reason instanceof Error && signal.reason.name !== 'AbortError' ? `: ${signal.reason.message}` : '';
|
|
1102
1196
|
}
|
|
1103
1197
|
|
|
1104
1198
|
export function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
@@ -1112,7 +1206,9 @@ export function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
1112
1206
|
const cancel = (): void => {
|
|
1113
1207
|
clearTimeout(timer);
|
|
1114
1208
|
signal.removeEventListener('abort', cancel);
|
|
1115
|
-
|
|
1209
|
+
// Carries the abort reason: a bare "worker wait canceled" was all a
|
|
1210
|
+
// lost root lease ever reported (customer rw_3a0fcb71).
|
|
1211
|
+
rejectDelay(new Error(`worker wait canceled${cancelCause(signal)}`, { cause: signal.reason }));
|
|
1116
1212
|
};
|
|
1117
1213
|
if (signal.aborted) cancel();
|
|
1118
1214
|
else signal.addEventListener('abort', cancel, { once: true });
|
package/src/cli/step-failure.ts
CHANGED
|
@@ -63,6 +63,8 @@ export async function stepFailureDetails(
|
|
|
63
63
|
// Keyed by step, so interleaved steps never pool their attempts and a page
|
|
64
64
|
// boundary never splits one step's history.
|
|
65
65
|
const histories = new Map<string, AttemptHistory>();
|
|
66
|
+
// Attempts the kernel itself declared dead (see `unchargedAttempts`).
|
|
67
|
+
const uncharged = new Map<string, number>();
|
|
66
68
|
while (true) {
|
|
67
69
|
const { entries } = await client.journalRead(runId, fromSeq, 100);
|
|
68
70
|
if (entries.length === 0) break;
|
|
@@ -93,6 +95,7 @@ export async function stepFailureDetails(
|
|
|
93
95
|
// holds every entry; only this diagnostic's candidate is cleared.
|
|
94
96
|
if (payload === undefined || typeof completionReason !== 'string' || completionReason === 'success') {
|
|
95
97
|
histories.delete(stepId);
|
|
98
|
+
uncharged.delete(stepId);
|
|
96
99
|
continue;
|
|
97
100
|
}
|
|
98
101
|
const history = histories.get(stepId) ?? { records: [], causes: [] };
|
|
@@ -101,6 +104,10 @@ export async function stepFailureDetails(
|
|
|
101
104
|
history.records.push(attemptFailure(entry['attempt'], completionReason, payload['disposition'], payload));
|
|
102
105
|
history.causes.push(failureCause(completionReason, payload));
|
|
103
106
|
histories.set(stepId, history);
|
|
107
|
+
if (payload['disposition'] !== 'step_done' && payload['completed_by'] === 'kernel'
|
|
108
|
+
&& (completionReason === 'crashed' || completionReason === 'lease_expired')) {
|
|
109
|
+
uncharged.set(stepId, (uncharged.get(stepId) ?? 0) + 1);
|
|
110
|
+
}
|
|
104
111
|
// A terminal completion that is not a success is the failure, whatever
|
|
105
112
|
// its step type. The old predicate also demanded a non-zero `exit_code`,
|
|
106
113
|
// which no agent completion carries and which a deterministic step that
|
|
@@ -116,6 +123,7 @@ export async function stepFailureDetails(
|
|
|
116
123
|
...(stepType === undefined ? {} : { stepType }),
|
|
117
124
|
...(typeof attempt === 'number' && Number.isSafeInteger(attempt) && attempt > 0 ? { attempt } : {}),
|
|
118
125
|
...(maxIterations === undefined ? {} : { maxIterations }),
|
|
126
|
+
...((uncharged.get(stepId) ?? 0) > 0 ? { unchargedAttempts: uncharged.get(stepId) } : {}),
|
|
119
127
|
...terminalEvidence(payload),
|
|
120
128
|
// A single failed attempt is already fully described by the scalars
|
|
121
129
|
// above; repeating it as a one-element history would add a clause to
|
|
@@ -169,6 +177,16 @@ export function renderStepEvidence(details: StepFailedDetails): string {
|
|
|
169
177
|
|
|
170
178
|
function renderAttempt(details: StepFailedDetails): string {
|
|
171
179
|
if (details.attempt === undefined) return '';
|
|
180
|
+
const uncharged = details.unchargedAttempts ?? 0;
|
|
181
|
+
// `attempt=N/M` compares like with like only when every attempt was charged.
|
|
182
|
+
// A worker that died (crashed / lease_expired) costs no iteration, so a
|
|
183
|
+
// one-iteration step can legitimately run attempt 2 -- printed as
|
|
184
|
+
// `attempt=2/1` it read as the budget being overrun. Say which is which.
|
|
185
|
+
if (uncharged > 0 && uncharged < details.attempt) {
|
|
186
|
+
return ` attempt=${details.attempt}`
|
|
187
|
+
+ ` (iteration ${details.attempt - uncharged}${details.maxIterations === undefined ? '' : `/${details.maxIterations}`};`
|
|
188
|
+
+ ` ${uncharged} earlier attempt${uncharged === 1 ? '' : 's'} lost to a dead worker, not charged)`;
|
|
189
|
+
}
|
|
172
190
|
return ` attempt=${details.attempt}`
|
|
173
191
|
+ (details.maxIterations === undefined ? '' : `/${details.maxIterations}`);
|
|
174
192
|
}
|
package/src/cli-commands.ts
CHANGED
|
@@ -163,11 +163,29 @@ export const CLI_VERBS = [
|
|
|
163
163
|
{ flags: '--name <name>', description: 'Name for the hosted listener' },
|
|
164
164
|
{ flags: '--draft', description: 'Create the listener without activating it' },
|
|
165
165
|
{ flags: '--plugin <ref>', description: 'Send-only GitHub flow-extension ref; repeatable. Does not write flows.json' },
|
|
166
|
+
{ flags: '--flow <name|listener-id>', description: 'Deploy the source as the next version of this flow; its settings stay unchanged' },
|
|
166
167
|
NO_CONNECT_OPTION,
|
|
167
168
|
JSON_OPTION,
|
|
168
169
|
],
|
|
169
170
|
variants: ['deploy', 'cloud-deploy'],
|
|
170
171
|
},
|
|
172
|
+
{
|
|
173
|
+
name: 'versions',
|
|
174
|
+
description: 'List a hosted flow’s source versions, the active one marked',
|
|
175
|
+
args: [{ name: 'flow', description: 'Flow name or listener id', required: true }],
|
|
176
|
+
options: [JSON_OPTION],
|
|
177
|
+
variants: ['versions'],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'rollback',
|
|
181
|
+
description: 'Make an earlier (or later) recorded version of a hosted flow the active one',
|
|
182
|
+
args: [
|
|
183
|
+
{ name: 'flow', description: 'Flow name or listener id', required: true },
|
|
184
|
+
{ name: 'version', description: 'Version number to activate', required: true },
|
|
185
|
+
],
|
|
186
|
+
options: [JSON_OPTION],
|
|
187
|
+
variants: ['rollback'],
|
|
188
|
+
},
|
|
171
189
|
{
|
|
172
190
|
name: 'deployments',
|
|
173
191
|
description: 'List this workspace’s hosted trigger listeners',
|
package/src/cli.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { checkTypeScriptFlow } from './cli/check-typescript.js';
|
|
|
44
44
|
import { runCloudCli } from './cli/cloud-run.js';
|
|
45
45
|
import { runCloudSyncCli } from './cli/cloud-sync.js';
|
|
46
46
|
import { parseCloudDeployArgs, runCloudDeployCli, runCloudDeploymentsCli, runCloudUndeployCli, type CloudDeployArgs } from './cli/cloud-deploy.js';
|
|
47
|
+
import { runCloudRollbackCli, runCloudVersionsCli } from './cli/cloud-versions.js';
|
|
47
48
|
import { parseCloudScheduleArgs, runCloudScheduleCli, runCloudSchedulesCli, runCloudUnscheduleCli, type CloudScheduleArgs } from './cli/cloud-schedule.js';
|
|
48
49
|
import { isAuthoredFlowPath } from './direct-input.js';
|
|
49
50
|
import { parseDeployArgs, runDeploy, type DeployArgs } from './cli/deploy.js';
|
|
@@ -97,6 +98,8 @@ export type ParsedArgs =
|
|
|
97
98
|
| CloudDeployArgs
|
|
98
99
|
| { command: 'deployments'; json: boolean }
|
|
99
100
|
| { command: 'undeploy'; agentId: string; json: boolean }
|
|
101
|
+
| { command: 'versions'; flow: string; json: boolean }
|
|
102
|
+
| { command: 'rollback'; flow: string; version: string; json: boolean }
|
|
100
103
|
| CloudScheduleArgs
|
|
101
104
|
| { command: 'schedules'; json: boolean }
|
|
102
105
|
| { command: 'unschedule'; scheduleId: string; json: boolean }
|
|
@@ -123,7 +126,10 @@ const USAGE = [
|
|
|
123
126
|
'flows build [--out <dir>] <flow.yaml|flow.ts>',
|
|
124
127
|
'flows build --verify <bundle-dir>',
|
|
125
128
|
'flows deploy <flow.ts> --repo <owner/name> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--plugin <ref>] [--no-connect] [--json]',
|
|
129
|
+
'flows deploy <flow.ts> --flow <name|listener-id> [--plugin <ref>] [--no-connect] [--json]',
|
|
126
130
|
'flows deployments [--json]',
|
|
131
|
+
'flows versions [--json] <name|listener-id>',
|
|
132
|
+
'flows rollback [--json] <name|listener-id> <version>',
|
|
127
133
|
'flows undeploy [--json] <deployment-id>',
|
|
128
134
|
'flows schedule <flow.yaml|flow.ts> [--cron "<expr>" | --every <n><s|m|h|d>] [--tz <IANA>] [--input <inline-json-or-file>] [--name <name>] [--no-connect] [--json]',
|
|
129
135
|
'flows schedules [--json]',
|
|
@@ -244,6 +250,8 @@ export async function runCli(
|
|
|
244
250
|
if (parsed.command === 'cloud-deploy') return runCloudDeployCli(parsed, io);
|
|
245
251
|
if (parsed.command === 'deployments') return runCloudDeploymentsCli(parsed, io);
|
|
246
252
|
if (parsed.command === 'undeploy') return runCloudUndeployCli(parsed, io);
|
|
253
|
+
if (parsed.command === 'versions') return runCloudVersionsCli(parsed, io);
|
|
254
|
+
if (parsed.command === 'rollback') return runCloudRollbackCli(parsed, io);
|
|
247
255
|
if (parsed.command === 'schedule') return runCloudScheduleCli(parsed, io);
|
|
248
256
|
if (parsed.command === 'schedules') return runCloudSchedulesCli(parsed, io);
|
|
249
257
|
if (parsed.command === 'unschedule') return runCloudUnscheduleCli(parsed, io);
|
|
@@ -631,6 +639,13 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
631
639
|
if (json > 1 || rest.length !== 1 || rest[0]!.startsWith('-')) return undefined;
|
|
632
640
|
return { command: 'undeploy', agentId: rest[0]!, json: json === 1 };
|
|
633
641
|
}
|
|
642
|
+
if (command === 'versions' || command === 'rollback') {
|
|
643
|
+
const rest = args.slice(1).filter(a => a !== '--json');
|
|
644
|
+
const json = args.length - 1 - rest.length;
|
|
645
|
+
if (json > 1 || rest.some(a => a.startsWith('-'))) return undefined;
|
|
646
|
+
if (command === 'versions') return rest.length === 1 ? { command, flow: rest[0]!, json: json === 1 } : undefined;
|
|
647
|
+
return rest.length === 2 ? { command, flow: rest[0]!, version: rest[1]!, json: json === 1 } : undefined;
|
|
648
|
+
}
|
|
634
649
|
if (command === 'deployments') {
|
|
635
650
|
const rest = args.slice(1);
|
|
636
651
|
if (rest.length > 1 || (rest.length === 1 && rest[0] !== '--json')) return undefined;
|