@relayflows/sdk 2.0.17 → 2.0.18
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/authored-flow-error.d.ts +21 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +14 -0
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +47 -3
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-human.d.ts +49 -0
- package/dist/authored-human.d.ts.map +1 -0
- package/dist/authored-human.js +97 -0
- package/dist/authored-human.js.map +1 -0
- package/dist/authored-node-entry.js +3 -2
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +15 -4
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +29 -1
- package/dist/authored-root.js.map +1 -1
- package/dist/cli/answer.d.ts +17 -0
- package/dist/cli/answer.d.ts.map +1 -0
- package/dist/cli/answer.js +89 -0
- package/dist/cli/answer.js.map +1 -0
- package/dist/cli/build.js +1 -1
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/check-triggers.d.ts.map +1 -1
- package/dist/cli/check-triggers.js +2 -0
- package/dist/cli/check-triggers.js.map +1 -1
- package/dist/cli/check.d.ts +11 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +14 -2
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-connect-cli.d.ts +54 -0
- package/dist/cli/cloud-connect-cli.d.ts.map +1 -0
- package/dist/cli/cloud-connect-cli.js +109 -0
- package/dist/cli/cloud-connect-cli.js.map +1 -0
- package/dist/cli/cloud-deploy.d.ts +3 -1
- package/dist/cli/cloud-deploy.d.ts.map +1 -1
- package/dist/cli/cloud-deploy.js +44 -6
- package/dist/cli/cloud-deploy.js.map +1 -1
- package/dist/cli/cloud-run.d.ts +2 -1
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +17 -3
- package/dist/cli/cloud-run.js.map +1 -1
- package/dist/cli/cloud-schedule.d.ts +3 -1
- package/dist/cli/cloud-schedule.d.ts.map +1 -1
- package/dist/cli/cloud-schedule.js +31 -6
- package/dist/cli/cloud-schedule.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +5 -1
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +22 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +34 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +114 -9
- package/dist/cli.js.map +1 -1
- package/dist/cloud-connect.d.ts +52 -0
- package/dist/cloud-connect.d.ts.map +1 -0
- package/dist/cloud-connect.js +138 -0
- package/dist/cloud-connect.js.map +1 -0
- package/dist/cloud-deploy.d.ts +19 -1
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +40 -1
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-http.d.ts +2 -2
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-schedule.d.ts +7 -0
- package/dist/cloud-schedule.d.ts.map +1 -1
- package/dist/cloud-schedule.js +22 -1
- package/dist/cloud-schedule.js.map +1 -1
- package/dist/failure-kinds.d.ts +1 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +2 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-requirements.d.ts +68 -0
- package/dist/flow-requirements.d.ts.map +1 -0
- package/dist/flow-requirements.js +152 -0
- package/dist/flow-requirements.js.map +1 -0
- 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/dist/journal-client.d.ts +11 -0
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +13 -0
- package/dist/journal-client.js.map +1 -1
- package/dist/progress.d.ts +2 -1
- package/dist/progress.d.ts.map +1 -1
- package/dist/progress.js +9 -3
- package/dist/progress.js.map +1 -1
- package/dist/protocol.d.ts +25 -1
- package/dist/protocol.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/authored-flow-error.ts +29 -0
- package/src/authored-flow-executor.ts +54 -2
- package/src/authored-human.ts +150 -0
- package/src/authored-node-entry.ts +3 -2
- package/src/authored-node-runner.ts +20 -5
- package/src/authored-root.ts +28 -1
- package/src/cli/answer.ts +98 -0
- package/src/cli/build.ts +1 -1
- package/src/cli/check-triggers.ts +2 -0
- package/src/cli/check.ts +24 -2
- package/src/cli/cloud-connect-cli.ts +130 -0
- package/src/cli/cloud-deploy.ts +40 -6
- package/src/cli/cloud-run.ts +16 -4
- package/src/cli/cloud-schedule.ts +31 -6
- package/src/cli/direct-run.ts +5 -0
- package/src/cli/run.ts +48 -2
- package/src/cli.ts +103 -10
- package/src/cloud-connect.ts +187 -0
- package/src/cloud-deploy.ts +57 -2
- package/src/cloud-http.ts +1 -1
- package/src/cloud-schedule.ts +20 -3
- package/src/failure-kinds.ts +2 -0
- package/src/flow-requirements.ts +201 -0
- package/src/index.ts +8 -0
- package/src/journal-client.ts +20 -0
- package/src/progress.ts +11 -4
- package/src/protocol.ts +23 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
5
|
+
import { loadAuthoredFlow } from '../authored-flow-loader.js';
|
|
6
|
+
import { ensureIntegrationsConnected, type ConnectPrompt, type ConnectionsOutcome } from '../cloud-connect.js';
|
|
7
|
+
import { CloudFlowError, cloudRequest, isCloudRecord, type CloudConnectionOptions } from '../cloud-http.js';
|
|
8
|
+
import { CompileError, compileSpec, kernelToAuthoring } from '../compile.js';
|
|
9
|
+
import { FLOW_HARNESSES, flowRequirements, type FlowRequirements, type FlowRequirementsContext } from '../flow-requirements.js';
|
|
10
|
+
import { readProjectConfig } from './check.js';
|
|
11
|
+
import type { CliIo } from '../cli.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The hosted verbs' shared "are the integrations connected?" step. It runs
|
|
15
|
+
* before `flows deploy`, `flows schedule` and `flows run --cloud` submit
|
|
16
|
+
* anything, so a flow that would fail at its first `f.slack.post` with
|
|
17
|
+
* `helper_slack.credential_missing` is connected — or refused — first.
|
|
18
|
+
*
|
|
19
|
+
* The flow is read the same way the submission reads it (authored loader or
|
|
20
|
+
* YAML compile); a source that does not load is left for the submission to
|
|
21
|
+
* refuse with its own, better diagnostic, so this step never masks one.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export interface FlowConnectionsInput extends FlowRequirementsContext {
|
|
25
|
+
path: string;
|
|
26
|
+
/** `undefined` refuses a missing integration instead of prompting (`--no-connect`, `--json`, no TTY). */
|
|
27
|
+
prompt: ConnectPrompt | undefined;
|
|
28
|
+
/** Known already (deploy resolved it); otherwise `whoami` supplies it. */
|
|
29
|
+
workspaceId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function flowRequirementsForPath(
|
|
33
|
+
path: string, context: FlowRequirementsContext = {},
|
|
34
|
+
): Promise<FlowRequirements | undefined> {
|
|
35
|
+
const projectCli = context.projectCli ?? projectCliFor(path);
|
|
36
|
+
const withCli = { ...context, ...(projectCli === undefined ? {} : { projectCli }) };
|
|
37
|
+
try {
|
|
38
|
+
if (/\.flow\.ts$/iu.test(path)) {
|
|
39
|
+
const loaded = await loadAuthoredFlow(path);
|
|
40
|
+
return flowRequirements(loaded.getDefinition(loaded.handle), withCli);
|
|
41
|
+
}
|
|
42
|
+
if (!/\.(?:ya?ml|json)$/iu.test(path)) return undefined;
|
|
43
|
+
const parsed: unknown = parseYaml(await readFile(path, 'utf8'));
|
|
44
|
+
let spec;
|
|
45
|
+
try {
|
|
46
|
+
spec = compileSpec(parsed);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (!(error instanceof CompileError)) throw error;
|
|
49
|
+
spec = compileSpec(kernelToAuthoring(parsed));
|
|
50
|
+
}
|
|
51
|
+
return flowRequirements(spec, withCli);
|
|
52
|
+
} catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function projectCliFor(path: string): string | undefined {
|
|
58
|
+
try {
|
|
59
|
+
return readProjectConfig(dirname(resolve(path))).cli;
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `whoami`'s current workspace: the one every hosted verb submits into. */
|
|
66
|
+
export async function currentWorkspaceId(options: CloudConnectionOptions): Promise<string> {
|
|
67
|
+
const whoami = await cloudRequest('/api/v1/auth/whoami', options);
|
|
68
|
+
const workspace = isCloudRecord(whoami) && isCloudRecord(whoami.currentWorkspace) ? whoami.currentWorkspace : undefined;
|
|
69
|
+
if (workspace === undefined || typeof workspace.id !== 'string' || !workspace.id) {
|
|
70
|
+
throw new CloudFlowError('invalid_response', 'Cloud did not report a current workspace for this credential.');
|
|
71
|
+
}
|
|
72
|
+
return workspace.id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Connects (or refuses on) every integration `path` requires. Nothing is
|
|
77
|
+
* contacted when the flow requires no integration, so a flow with no
|
|
78
|
+
* helpers, sources or repository submits exactly as before; the derived
|
|
79
|
+
* requirements still come back so a later harness refusal can name its remedy.
|
|
80
|
+
* `undefined` only when the source does not load — the submission says why.
|
|
81
|
+
*/
|
|
82
|
+
export async function ensureFlowConnections(
|
|
83
|
+
input: FlowConnectionsInput, options: CloudConnectionOptions = {},
|
|
84
|
+
): Promise<{ requirements: FlowRequirements; outcome: ConnectionsOutcome } | undefined> {
|
|
85
|
+
const requirements = await flowRequirementsForPath(input.path, input);
|
|
86
|
+
if (requirements === undefined) return undefined;
|
|
87
|
+
if (requirements.integrations.length === 0) return { requirements, outcome: { ready: [], connected: [] } };
|
|
88
|
+
const workspaceId = input.workspaceId ?? await currentWorkspaceId(options);
|
|
89
|
+
const outcome = await ensureIntegrationsConnected(requirements, {
|
|
90
|
+
...options, workspaceId, ...(input.prompt === undefined ? {} : { prompt: input.prompt }),
|
|
91
|
+
});
|
|
92
|
+
return { requirements, outcome };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A terminal prompt, or nothing. `--json` output is for a machine, and a
|
|
97
|
+
* pipe cannot answer a question, so both refuse instead of asking; `--no-connect`
|
|
98
|
+
* is the explicit form of the same choice.
|
|
99
|
+
*/
|
|
100
|
+
export function cliConnectPrompt(
|
|
101
|
+
io: CliIo, { noConnect, json }: { noConnect: boolean; json: boolean },
|
|
102
|
+
streams: { stdin: NodeJS.ReadStream; stdout: NodeJS.WriteStream } = { stdin: process.stdin, stdout: process.stdout },
|
|
103
|
+
): ConnectPrompt | undefined {
|
|
104
|
+
if (noConnect || json || !streams.stdin.isTTY || !streams.stdout.isTTY) return undefined;
|
|
105
|
+
return {
|
|
106
|
+
info: line => io.stderr(line),
|
|
107
|
+
confirm: question => new Promise(resolveAnswer => {
|
|
108
|
+
const rl = createInterface({ input: streams.stdin, output: streams.stdout });
|
|
109
|
+
rl.question(question, answer => {
|
|
110
|
+
rl.close();
|
|
111
|
+
resolveAnswer(/^\s*(?:y(?:es)?)?\s*$/iu.test(answer));
|
|
112
|
+
});
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Cloud refuses a run or activation whose coding-agent credential is missing
|
|
119
|
+
* (`flow_model_not_connected`, `cli_credentials_missing`); there is no status
|
|
120
|
+
* route to ask first, so the refusal is where the remedy is named.
|
|
121
|
+
*/
|
|
122
|
+
export function harnessRemedy(error: unknown, harnesses: readonly string[]): string {
|
|
123
|
+
if (!(error instanceof CloudFlowError) || error.refusal === undefined) return '';
|
|
124
|
+
if (!['flow_model_not_connected', 'cli_credentials_missing'].includes(error.refusal.code)) return '';
|
|
125
|
+
if (/agent-relay cloud connect/u.test(error.refusal.error)) return '';
|
|
126
|
+
const message = error.refusal.error.toLowerCase();
|
|
127
|
+
const named = FLOW_HARNESSES.filter(harness => message.includes(harness));
|
|
128
|
+
const targets = named.length > 0 ? named : harnesses.length > 0 ? harnesses : ['claude'];
|
|
129
|
+
return ` Connect it with: ${targets.map(harness => `agent-relay cloud connect ${harness}`).join('; ')}.`;
|
|
130
|
+
}
|
package/src/cli/cloud-deploy.ts
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
deployToCloud, listCloudDeployments, parseAgentHarnesses, parseRepository, parseTriggerSource, undeployFromCloud,
|
|
4
4
|
type FlowTriggerSource,
|
|
5
5
|
} from '../cloud-deploy.js';
|
|
6
|
+
import { describeFlowRequirements } from '../flow-requirements.js';
|
|
7
|
+
import { cliConnectPrompt, flowRequirementsForPath, harnessRemedy } from './cloud-connect-cli.js';
|
|
6
8
|
import type { CliIo } from '../cli.js';
|
|
7
9
|
|
|
8
10
|
export interface CloudDeployArgs {
|
|
@@ -14,12 +16,14 @@ export interface CloudDeployArgs {
|
|
|
14
16
|
name: string | undefined;
|
|
15
17
|
agents: string | undefined;
|
|
16
18
|
draft: boolean;
|
|
19
|
+
/** Refuse a missing integration instead of offering to connect it. */
|
|
20
|
+
noConnect: boolean;
|
|
17
21
|
json: boolean;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* `flows deploy <flow.ts> --repo <owner/name> --on <provider>[:k=v,…] [--on …]
|
|
22
|
-
* --approver <handle> [--name <n>] [--json]`
|
|
26
|
+
* --approver <handle> [--name <n>] [--agents <list>] [--draft] [--no-connect] [--json]`
|
|
23
27
|
*
|
|
24
28
|
* Parsed here rather than in `parseDeployArgs` because the two `deploy` forms
|
|
25
29
|
* share nothing but the word: the digest form copies a sealed bundle into a
|
|
@@ -32,6 +36,7 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
32
36
|
let name: string | undefined;
|
|
33
37
|
let agents: string | undefined;
|
|
34
38
|
let draft = false;
|
|
39
|
+
let noConnect = false;
|
|
35
40
|
let json = false;
|
|
36
41
|
const on: string[] = [];
|
|
37
42
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -46,6 +51,11 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
46
51
|
draft = true;
|
|
47
52
|
continue;
|
|
48
53
|
}
|
|
54
|
+
if (arg === '--no-connect') {
|
|
55
|
+
if (noConnect) return undefined;
|
|
56
|
+
noConnect = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
49
59
|
if (arg === '--agents') {
|
|
50
60
|
const next = args[i + 1];
|
|
51
61
|
if (agents !== undefined || next === undefined || next.startsWith('-')) return undefined;
|
|
@@ -68,7 +78,7 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
68
78
|
value = arg;
|
|
69
79
|
}
|
|
70
80
|
if (value === undefined || repo === undefined || on.length === 0) return undefined;
|
|
71
|
-
return { command: 'cloud-deploy', value, repo, on, approver, name, agents, draft, json };
|
|
81
|
+
return { command: 'cloud-deploy', value, repo, on, approver, name, agents, draft, noConnect, json };
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
function describeSource(source: FlowTriggerSource): string {
|
|
@@ -77,11 +87,19 @@ function describeSource(source: FlowTriggerSource): string {
|
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promise<0 | 1 | 2> {
|
|
90
|
+
const agents = args.agents === undefined ? undefined : parseAgentHarnessesOr(args.agents);
|
|
91
|
+
// What the source declares, so a harness refusal names the right remedy
|
|
92
|
+
// even when `--agents` was not given; the loader's own failure is reported
|
|
93
|
+
// by deployToCloud, not here.
|
|
94
|
+
let harnesses: readonly string[] = agents instanceof CloudFlowError ? [] : agents ?? [];
|
|
80
95
|
try {
|
|
81
96
|
if (args.approver === undefined) {
|
|
82
97
|
throw new CloudFlowError('invalid_input',
|
|
83
98
|
'--approver <handle> is required: every launched run receives it as input.approver for f.human.');
|
|
84
99
|
}
|
|
100
|
+
if (agents instanceof CloudFlowError) throw agents;
|
|
101
|
+
const connect = cliConnectPrompt(io, { noConnect: args.noConnect, json: args.json });
|
|
102
|
+
if (agents === undefined) harnesses = (await flowRequirementsForPath(args.value))?.harnesses ?? [];
|
|
85
103
|
const deployment = await deployToCloud({
|
|
86
104
|
path: args.value,
|
|
87
105
|
repository: parseRepository(args.repo),
|
|
@@ -89,7 +107,8 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
89
107
|
approver: args.approver,
|
|
90
108
|
draft: args.draft,
|
|
91
109
|
...(args.name === undefined ? {} : { name: args.name }),
|
|
92
|
-
...(
|
|
110
|
+
...(agents === undefined ? {} : { agents }),
|
|
111
|
+
...(connect === undefined ? {} : { connect }),
|
|
93
112
|
});
|
|
94
113
|
if (args.json) {
|
|
95
114
|
io.stdout(JSON.stringify({ ok: true, ...deployment }));
|
|
@@ -99,12 +118,24 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
99
118
|
io.stdout(` flow: ${deployment.name} (${args.value}, sha256 ${deployment.sourceSha256.slice(0, 12)})`);
|
|
100
119
|
io.stdout(` repository: ${deployment.repository.owner}/${deployment.repository.name}`);
|
|
101
120
|
for (const source of deployment.sources) io.stdout(` on: ${describeSource(source)}`);
|
|
121
|
+
const requires = describeFlowRequirements(deployment.requirements);
|
|
122
|
+
if (requires) io.stdout(` requires: ${requires}`);
|
|
123
|
+
for (const provider of deployment.connected) io.stdout(` connected: ${provider}`);
|
|
102
124
|
io.stdout(deployment.status === 'draft'
|
|
103
125
|
? 'Saved without activating; activate it from the Cloud dashboard, or redeploy without --draft.'
|
|
104
126
|
: 'Each matching ticket launches a run of this source in a fresh branch; list with: flows deployments');
|
|
105
127
|
return 0;
|
|
106
128
|
} catch (error) {
|
|
107
|
-
return reportCloudFailure(error, args.json, io);
|
|
129
|
+
return reportCloudFailure(error, args.json, io, harnesses);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseAgentHarnessesOr(value: string): ReturnType<typeof parseAgentHarnesses> | CloudFlowError {
|
|
134
|
+
try {
|
|
135
|
+
return parseAgentHarnesses(value);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error instanceof CloudFlowError) return error;
|
|
138
|
+
throw error;
|
|
108
139
|
}
|
|
109
140
|
}
|
|
110
141
|
|
|
@@ -140,9 +171,11 @@ export async function runCloudUndeployCli({ agentId, json }: { agentId: string;
|
|
|
140
171
|
}
|
|
141
172
|
}
|
|
142
173
|
|
|
143
|
-
function reportCloudFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
|
|
174
|
+
function reportCloudFailure(error: unknown, json: boolean, io: CliIo, harnesses: readonly string[] = []): 1 | 2 {
|
|
144
175
|
const code = error instanceof CloudFlowError ? error.code : 'cloud_deploy_failed';
|
|
145
176
|
let message = error instanceof Error ? error.message : 'Cloud deploy failed.';
|
|
177
|
+
// Cloud names the missing coding-agent credential on activation; say how it is connected.
|
|
178
|
+
message += harnessRemedy(error, harnesses);
|
|
146
179
|
// The deploy routes take a browser session or a `cli:auth` token. A
|
|
147
180
|
// deployment (CI) token gets 403 `session_required`; say what fixes it.
|
|
148
181
|
if (error instanceof CloudFlowError && error.status === 403) {
|
|
@@ -152,6 +185,7 @@ function reportCloudFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
|
|
|
152
185
|
if (json) io.stdout(JSON.stringify({ ok: false, code, message }));
|
|
153
186
|
else io.stderr(`${code}: ${message}`);
|
|
154
187
|
return error instanceof CloudFlowError
|
|
155
|
-
&& (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code)
|
|
188
|
+
&& (['configuration', 'unsupported_source', 'invalid_input', 'integration_not_connected'].includes(error.code)
|
|
189
|
+
|| error.status === 403 || error.status === 401)
|
|
156
190
|
? 2 : 1;
|
|
157
191
|
}
|
package/src/cli/cloud-run.ts
CHANGED
|
@@ -2,12 +2,13 @@ import { CloudFlowError } from '../cloud-http.js';
|
|
|
2
2
|
import { runInCloud, waitForCloudFlowRun, type RunInCloudOptions } from '../cloud-run.js';
|
|
3
3
|
import { DirectInputError, isAuthoredFlowPath, parseDirectInput } from '../direct-input.js';
|
|
4
4
|
import { snapshotJsonValue } from '../json-value.js';
|
|
5
|
+
import { cliConnectPrompt, ensureFlowConnections, harnessRemedy } from './cloud-connect-cli.js';
|
|
5
6
|
import type { CliIo } from '../cli.js';
|
|
6
7
|
|
|
7
8
|
/** Presentation only: the central CLI parser owns argv; the SDK owns the lifecycle. */
|
|
8
9
|
export async function runCloudCli(
|
|
9
|
-
{ value: path, json, wait, input, syncCode }: {
|
|
10
|
-
value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean;
|
|
10
|
+
{ value: path, json, wait, input, syncCode, noConnect = false }: {
|
|
11
|
+
value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean; noConnect?: boolean;
|
|
11
12
|
},
|
|
12
13
|
io: CliIo,
|
|
13
14
|
): Promise<0 | 1 | 2> {
|
|
@@ -20,6 +21,7 @@ export async function runCloudCli(
|
|
|
20
21
|
// packing or upload — admitted nothing and is safe to retry; only an
|
|
21
22
|
// interrupted submission has unknown admission.
|
|
22
23
|
let submitting = false;
|
|
24
|
+
let harnesses: readonly string[] = [];
|
|
23
25
|
try {
|
|
24
26
|
const options: RunInCloudOptions = { signal: controller.signal, onSubmit: () => { submitting = true; } };
|
|
25
27
|
if (isAuthoredFlowPath(path)) {
|
|
@@ -32,6 +34,15 @@ export async function runCloudCli(
|
|
|
32
34
|
throw error;
|
|
33
35
|
}
|
|
34
36
|
}
|
|
37
|
+
// After local validation, before anything is packed or uploaded: a helper
|
|
38
|
+
// the run would call against an unconnected integration is connected
|
|
39
|
+
// here, or refused here. A command that is going to be refused for its
|
|
40
|
+
// arguments must not first open a browser.
|
|
41
|
+
const connections = await ensureFlowConnections({
|
|
42
|
+
path, prompt: cliConnectPrompt(io, { noConnect, json }),
|
|
43
|
+
}, { signal: controller.signal });
|
|
44
|
+
harnesses = connections?.requirements.harnesses ?? [];
|
|
45
|
+
for (const provider of connections?.outcome.connected ?? []) if (!json) io.stdout(`CONNECTED ${provider}`);
|
|
35
46
|
// The tree is the invoking directory, as with v1: the flow path is where
|
|
36
47
|
// the body lives, not the boundary of what the run may read.
|
|
37
48
|
if (syncCode) options.syncCode = { root: process.cwd() };
|
|
@@ -65,11 +76,12 @@ export async function runCloudCli(
|
|
|
65
76
|
: submitting
|
|
66
77
|
? 'Submission interrupted before a receipt was received. Admission is unknown; Cloud may have started the run. Do not resubmit blindly.'
|
|
67
78
|
: 'Interrupted before the run was submitted; nothing was admitted. Safe to run again.'
|
|
68
|
-
: error instanceof Error ? error.message : 'Cloud run failed.';
|
|
79
|
+
: (error instanceof Error ? error.message : 'Cloud run failed.') + harnessRemedy(error, harnesses);
|
|
69
80
|
if (json) io.stdout(JSON.stringify({ ok: false, code, message, ...(runId ? { runId } : {}) }));
|
|
70
81
|
else io.stderr(`${code}: ${message}${runId ? ` (run ${runId})` : ''}`);
|
|
71
82
|
return error instanceof CloudFlowError
|
|
72
|
-
&& (['configuration', 'unsupported_source', 'invalid_input', 'unsupported_storage_backend', 'sync_too_large', 'sync_unsupported'
|
|
83
|
+
&& (['configuration', 'unsupported_source', 'invalid_input', 'unsupported_storage_backend', 'sync_too_large', 'sync_unsupported',
|
|
84
|
+
'integration_not_connected'].includes(error.code)
|
|
73
85
|
|| (runId === undefined && error.code === 'http_error' && [401, 403].includes(error.status ?? 0))) ? 2 : 1;
|
|
74
86
|
} finally {
|
|
75
87
|
process.off('SIGINT', abort);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CloudFlowError } from '../cloud-http.js';
|
|
2
|
-
import { listCloudSchedules, scheduleInCloud, unscheduleInCloud, type CloudSchedule } from '../cloud-schedule.js';
|
|
2
|
+
import { listCloudSchedules, scheduleInCloud, unscheduleInCloud, validateScheduleArgs, type CloudSchedule } from '../cloud-schedule.js';
|
|
3
3
|
import { DirectInputError, isAuthoredFlowPath, parseDirectInput } from '../direct-input.js';
|
|
4
4
|
import { snapshotJsonValue, type JsonValue } from '../json-value.js';
|
|
5
|
+
import { cliConnectPrompt, ensureFlowConnections, harnessRemedy } from './cloud-connect-cli.js';
|
|
5
6
|
import type { CliIo } from '../cli.js';
|
|
6
7
|
|
|
7
8
|
export interface CloudScheduleArgs {
|
|
@@ -12,12 +13,14 @@ export interface CloudScheduleArgs {
|
|
|
12
13
|
tz: string | undefined;
|
|
13
14
|
input: string | undefined;
|
|
14
15
|
name: string | undefined;
|
|
16
|
+
/** Refuse a missing integration instead of offering to connect it. */
|
|
17
|
+
noConnect: boolean;
|
|
15
18
|
json: boolean;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* `flows schedule <flow.yaml|flow.ts> [--cron "<expr>" | --every <n><s|m|h|d>]
|
|
20
|
-
* [--tz <IANA>] [--input <json|file>] [--name <n>] [--json]`
|
|
23
|
+
* [--tz <IANA>] [--input <json|file>] [--name <n>] [--no-connect] [--json]`
|
|
21
24
|
*
|
|
22
25
|
* With neither `--cron` nor `--every`, the flow's own `schedule.*` handler
|
|
23
26
|
* supplies the cron, so a declared schedule and its hosted registration
|
|
@@ -29,6 +32,7 @@ export function parseCloudScheduleArgs(args: readonly string[]): CloudScheduleAr
|
|
|
29
32
|
cron: undefined, every: undefined, tz: undefined, input: undefined, name: undefined,
|
|
30
33
|
};
|
|
31
34
|
let json = false;
|
|
35
|
+
let noConnect = false;
|
|
32
36
|
for (let i = 0; i < args.length; i++) {
|
|
33
37
|
const arg = args[i]!;
|
|
34
38
|
if (arg === '--json') {
|
|
@@ -36,6 +40,11 @@ export function parseCloudScheduleArgs(args: readonly string[]): CloudScheduleAr
|
|
|
36
40
|
json = true;
|
|
37
41
|
continue;
|
|
38
42
|
}
|
|
43
|
+
if (arg === '--no-connect') {
|
|
44
|
+
if (noConnect) return undefined;
|
|
45
|
+
noConnect = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
39
48
|
if (arg === '--cron' || arg === '--every' || arg === '--tz' || arg === '--input' || arg === '--name') {
|
|
40
49
|
const key = arg.slice(2) as keyof typeof flags;
|
|
41
50
|
const next = args[i + 1];
|
|
@@ -51,7 +60,7 @@ export function parseCloudScheduleArgs(args: readonly string[]): CloudScheduleAr
|
|
|
51
60
|
if (value === undefined) return undefined;
|
|
52
61
|
if (flags.cron !== undefined && flags.every !== undefined) return undefined;
|
|
53
62
|
if (flags.input !== undefined && !isAuthoredFlowPath(value)) return undefined;
|
|
54
|
-
return { command: 'schedule', value, ...flags, json };
|
|
63
|
+
return { command: 'schedule', value, ...flags, noConnect, json };
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
function describe(schedule: CloudSchedule): string {
|
|
@@ -61,6 +70,7 @@ function describe(schedule: CloudSchedule): string {
|
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
export async function runCloudScheduleCli(args: CloudScheduleArgs, io: CliIo): Promise<0 | 1 | 2> {
|
|
73
|
+
let harnesses: readonly string[] = [];
|
|
64
74
|
try {
|
|
65
75
|
let input: JsonValue | undefined;
|
|
66
76
|
let inputPresent = false;
|
|
@@ -73,6 +83,18 @@ export async function runCloudScheduleCli(args: CloudScheduleArgs, io: CliIo): P
|
|
|
73
83
|
throw error;
|
|
74
84
|
}
|
|
75
85
|
}
|
|
86
|
+
validateScheduleArgs({
|
|
87
|
+
...(args.cron === undefined ? {} : { cron: args.cron }),
|
|
88
|
+
...(args.every === undefined ? {} : { every: args.every }),
|
|
89
|
+
...(args.tz === undefined ? {} : { tz: args.tz }),
|
|
90
|
+
});
|
|
91
|
+
// Each fire runs the flow as this workspace, so what it needs connected
|
|
92
|
+
// is checked once here — after the arguments are known to be good, so a
|
|
93
|
+
// refused command never opens a browser first — and before the schedule exists.
|
|
94
|
+
const connections = await ensureFlowConnections({
|
|
95
|
+
path: args.value, prompt: cliConnectPrompt(io, { noConnect: args.noConnect, json: args.json }),
|
|
96
|
+
});
|
|
97
|
+
harnesses = connections?.requirements.harnesses ?? [];
|
|
76
98
|
const schedule = await scheduleInCloud({
|
|
77
99
|
flow: { path: args.value },
|
|
78
100
|
...(args.cron === undefined ? {} : { cron: args.cron }),
|
|
@@ -86,10 +108,11 @@ export async function runCloudScheduleCli(args: CloudScheduleArgs, io: CliIo): P
|
|
|
86
108
|
return 0;
|
|
87
109
|
}
|
|
88
110
|
io.stdout(`SCHEDULED ${describe(schedule)}`);
|
|
111
|
+
for (const provider of connections?.outcome.connected ?? []) io.stdout(` connected: ${provider}`);
|
|
89
112
|
io.stdout('Each fire submits this exact source through the same admission as flows run --cloud; list with: flows schedules');
|
|
90
113
|
return 0;
|
|
91
114
|
} catch (error) {
|
|
92
|
-
return reportFailure(error, args.json, io);
|
|
115
|
+
return reportFailure(error, args.json, io, harnesses);
|
|
93
116
|
}
|
|
94
117
|
}
|
|
95
118
|
|
|
@@ -120,9 +143,10 @@ export async function runCloudUnscheduleCli(
|
|
|
120
143
|
}
|
|
121
144
|
}
|
|
122
145
|
|
|
123
|
-
function reportFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
|
|
146
|
+
function reportFailure(error: unknown, json: boolean, io: CliIo, harnesses: readonly string[] = []): 1 | 2 {
|
|
124
147
|
const code = error instanceof CloudFlowError ? error.code : 'cloud_schedule_failed';
|
|
125
148
|
let message = error instanceof Error ? error.message : 'Cloud schedule failed.';
|
|
149
|
+
message += harnessRemedy(error, harnesses);
|
|
126
150
|
// The schedule routes take a browser session or a `cli:auth` token.
|
|
127
151
|
if (error instanceof CloudFlowError && error.status === 403) {
|
|
128
152
|
message += ' Scheduling needs an interactive `cli:auth` credential: run `agent-relay cloud login`.';
|
|
@@ -130,6 +154,7 @@ function reportFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
|
|
|
130
154
|
if (json) io.stdout(JSON.stringify({ ok: false, code, message }));
|
|
131
155
|
else io.stderr(`${code}: ${message}`);
|
|
132
156
|
return error instanceof CloudFlowError
|
|
133
|
-
&& (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code)
|
|
157
|
+
&& (['configuration', 'unsupported_source', 'invalid_input', 'integration_not_connected'].includes(error.code)
|
|
158
|
+
|| error.status === 403 || error.status === 401)
|
|
134
159
|
? 2 : 1;
|
|
135
160
|
}
|
package/src/cli/direct-run.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
AuthoredFlowExecutionError,
|
|
9
9
|
} from '../authored-flow-executor.js';
|
|
10
10
|
import { executeDurableAuthoredFlow } from '../authored-root.js';
|
|
11
|
+
import { AuthoredHumanParked } from '../authored-flow-error.js';
|
|
11
12
|
import { AuthoredFlowLoadError } from '../authored-flow-loader.js';
|
|
12
13
|
import { DirectInputError, parseDirectInput } from '../direct-input.js';
|
|
13
14
|
import { JournalClient } from '../journal-client.js';
|
|
@@ -15,6 +16,7 @@ import { inputFailureReport } from './check.js';
|
|
|
15
16
|
import { checkAuthoredTriggers } from './check-triggers.js';
|
|
16
17
|
import {
|
|
17
18
|
authoredCompletion,
|
|
19
|
+
authoredHumanParked,
|
|
18
20
|
authoredStepFailure,
|
|
19
21
|
connect,
|
|
20
22
|
emptyReport,
|
|
@@ -175,6 +177,9 @@ export async function runDirectFlow(
|
|
|
175
177
|
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
|
|
176
178
|
return authoredStepFailure('run', base, socketPath, error);
|
|
177
179
|
}
|
|
180
|
+
if (error instanceof AuthoredHumanParked) {
|
|
181
|
+
return authoredHumanParked('run', base, socketPath, error, { dataDir, localAgent: options.localAgent === true });
|
|
182
|
+
}
|
|
178
183
|
const runId = error instanceof AuthoredFlowExecutionError ? error.runId : undefined;
|
|
179
184
|
return protocolFailure('run', base, socketPath, error, runId);
|
|
180
185
|
} finally {
|
package/src/cli/run.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { parseDigestReference } from '../bundle-transport.js';
|
|
|
2
2
|
import { prepareDigestRun } from './run-digest.js';
|
|
3
3
|
import { reuseSummary } from './reuse.js';
|
|
4
4
|
import { resumeHelperEffect } from '../authored-helper-effect.js';
|
|
5
|
-
import { AuthoredFlowExecutionError } from '../authored-flow-error.js';
|
|
5
|
+
import { AuthoredFlowExecutionError, AuthoredHumanParked, type AuthoredHumanWait } from '../authored-flow-error.js';
|
|
6
|
+
import { answerCommand, resumeCommand } from '../authored-human.js';
|
|
6
7
|
import { join, resolve } from 'node:path';
|
|
7
8
|
import type { ProgressEvent } from '../progress.js';
|
|
8
9
|
import { toKernelSpec } from '../compile.js';
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
} from './check.js';
|
|
30
31
|
|
|
31
32
|
export type RunExitCode = 0 | 1 | 2 | 3;
|
|
32
|
-
export type RunCommand = 'run' | 'resume';
|
|
33
|
+
export type RunCommand = 'run' | 'resume' | 'answer';
|
|
33
34
|
|
|
34
35
|
export interface ParkedStep {
|
|
35
36
|
id: string;
|
|
@@ -53,6 +54,12 @@ export interface RunReport {
|
|
|
53
54
|
completedSteps?: number;
|
|
54
55
|
reuse?: { fromRunId: string; reusedSteps: number; executedSteps: number };
|
|
55
56
|
parkedStep?: ParkedStep;
|
|
57
|
+
/** The open `f.human` question a parked authored run is waiting on. */
|
|
58
|
+
humanWait?: AuthoredHumanWait;
|
|
59
|
+
/** `flows answer` only: the answer it recorded. */
|
|
60
|
+
answer?: { waitId: string; answer: boolean; note?: string };
|
|
61
|
+
/** The invocation that continues this run, when one is known. */
|
|
62
|
+
next?: string;
|
|
56
63
|
projectConfigPath?: string;
|
|
57
64
|
resolutions: CheckReport['resolutions'];
|
|
58
65
|
diagnostics: Array<CheckReport['diagnostics'][number] | RunDiagnostic>;
|
|
@@ -226,6 +233,9 @@ export async function resumeFlow(
|
|
|
226
233
|
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
|
|
227
234
|
return authoredStepFailure('resume', base, socketPath, error, runId);
|
|
228
235
|
}
|
|
236
|
+
if (error instanceof AuthoredHumanParked) {
|
|
237
|
+
return authoredHumanParked('resume', base, socketPath, error, { dataDir, localAgent: options.localAgent === true });
|
|
238
|
+
}
|
|
229
239
|
if (!(error instanceof JournalProtocolError) || error.code !== 'run_not_found') {
|
|
230
240
|
return protocolFailure('resume', base, socketPath, error, runId);
|
|
231
241
|
}
|
|
@@ -298,6 +308,42 @@ export function authoredStepFailure(
|
|
|
298
308
|
};
|
|
299
309
|
}
|
|
300
310
|
|
|
311
|
+
/**
|
|
312
|
+
* An authored body parked on an unanswered `f.human`: exit 3, like every
|
|
313
|
+
* other park, but the diagnostic names the question, who it is for, and the
|
|
314
|
+
* exact `flows answer` invocation — the run is waiting on a person, not on a
|
|
315
|
+
* worker. Shared by `run` and `resume` so the two never drift.
|
|
316
|
+
*/
|
|
317
|
+
export function authoredHumanParked(
|
|
318
|
+
command: RunCommand,
|
|
319
|
+
base: CheckReport | RunReport,
|
|
320
|
+
socketPath: string,
|
|
321
|
+
error: AuthoredHumanParked,
|
|
322
|
+
where: { dataDir?: string; localAgent: boolean },
|
|
323
|
+
): RunExecution {
|
|
324
|
+
const runId = error.runId!;
|
|
325
|
+
return {
|
|
326
|
+
exitCode: 3,
|
|
327
|
+
report: {
|
|
328
|
+
...fromBase(command, base),
|
|
329
|
+
ok: false,
|
|
330
|
+
runId,
|
|
331
|
+
socketPath,
|
|
332
|
+
status: 'parked',
|
|
333
|
+
parkedStep: { id: 'authored-root', type: 'agent' },
|
|
334
|
+
humanWait: error.wait,
|
|
335
|
+
diagnostics: [...base.diagnostics, {
|
|
336
|
+
severity: 'parked',
|
|
337
|
+
kind: 'run_parked',
|
|
338
|
+
message: `Run "${runId}" is waiting for ${error.wait.to} to answer ${error.wait.waitId}: `
|
|
339
|
+
+ `${JSON.stringify(error.wait.question)}\n`
|
|
340
|
+
+ `Answer with: ${answerCommand(runId, error.wait.waitId, where.dataDir)}\n`
|
|
341
|
+
+ `Then continue with: ${resumeCommand(runId, where.dataDir, where.localAgent)}`,
|
|
342
|
+
}],
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
301
347
|
/**
|
|
302
348
|
* An authored body that returned its own terminal verdict, reported as that verdict.
|
|
303
349
|
*
|