@relayflows/sdk 2.0.17 → 2.0.19
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.d.ts +9 -0
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +26 -7
- 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 +9 -2
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +27 -15
- 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/cloud-sync.d.ts +8 -1
- package/dist/cli/cloud-sync.d.ts.map +1 -1
- package/dist/cli/cloud-sync.js +79 -8
- package/dist/cli/cloud-sync.js.map +1 -1
- package/dist/cli/deploy.d.ts +6 -0
- package/dist/cli/deploy.d.ts.map +1 -1
- package/dist/cli/deploy.js +29 -4
- package/dist/cli/deploy.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/serve-webhook.d.ts +7 -1
- package/dist/cli/serve-webhook.d.ts.map +1 -1
- package/dist/cli/serve-webhook.js +19 -9
- package/dist/cli/serve-webhook.js.map +1 -1
- package/dist/cli-commands.d.ts +398 -0
- package/dist/cli-commands.d.ts.map +1 -0
- package/dist/cli-commands.js +254 -0
- package/dist/cli-commands.js.map +1 -0
- package/dist/cli-watch.d.ts +3 -1
- package/dist/cli-watch.d.ts.map +1 -1
- package/dist/cli-watch.js +4 -10
- package/dist/cli-watch.js.map +1 -1
- package/dist/cli.d.ts +127 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +195 -57
- 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/cloud-sync.d.ts +85 -2
- package/dist/cloud-sync.d.ts.map +1 -1
- package/dist/cloud-sync.js +123 -10
- package/dist/cloud-sync.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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- 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/dist/relay-cli.d.ts +50 -0
- package/dist/relay-cli.d.ts.map +1 -0
- package/dist/relay-cli.js +64 -0
- package/dist/relay-cli.js.map +1 -0
- package/package.json +7 -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 +21 -7
- 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 +26 -15
- package/src/cli/cloud-schedule.ts +31 -6
- package/src/cli/cloud-sync.ts +85 -8
- package/src/cli/deploy.ts +27 -5
- package/src/cli/direct-run.ts +5 -0
- package/src/cli/run.ts +48 -2
- package/src/cli/serve-webhook.ts +15 -8
- package/src/cli-commands.ts +339 -0
- package/src/cli-watch.ts +8 -9
- package/src/cli.ts +206 -54
- 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/cloud-sync.ts +164 -11
- package/src/failure-kinds.ts +2 -0
- package/src/flow-requirements.ts +201 -0
- package/src/index.ts +12 -2
- package/src/journal-client.ts +20 -0
- package/src/progress.ts +11 -4
- package/src/protocol.ts +23 -0
- package/src/relay-cli.ts +117 -0
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { addPlugin } from './cli/add.js';
|
|
3
3
|
import { watchCheck } from './cli-watch.js';
|
|
4
4
|
import { checkHelperBody } from './cli/check-helper-body.js';
|
|
5
|
+
import { describeFlowRequirements } from './flow-requirements.js';
|
|
5
6
|
|
|
6
7
|
import { renderProgress, type ProgressEvent } from './progress.js';
|
|
7
8
|
import { realpathSync } from 'node:fs';
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type RunProgress,
|
|
19
20
|
type RunReport,
|
|
20
21
|
} from './cli/run.js';
|
|
22
|
+
import { answerFlow } from './cli/answer.js';
|
|
21
23
|
import { checkAuthoredTriggers } from './cli/check-triggers.js';
|
|
22
24
|
import { parseWebhookArgs, runServeWebhook } from './cli/serve-webhook.js';
|
|
23
25
|
import { runDirectFlow } from './cli/direct-run.js';
|
|
@@ -34,6 +36,7 @@ import { parseBuildArgs, runBuild, type BuildArgs } from './cli/build.js';
|
|
|
34
36
|
import { runHnMonitor } from './cli/hn-monitor.js';
|
|
35
37
|
import { runTickRunner } from './cli/tick-runner.js';
|
|
36
38
|
import { DEFAULT_DATA_DIR } from './daemon-connection.js';
|
|
39
|
+
import { CLI_VERB_NAMES } from './cli-commands.js';
|
|
37
40
|
import {
|
|
38
41
|
mintObserverUrl,
|
|
39
42
|
resolveObserverLinkEnv,
|
|
@@ -48,14 +51,19 @@ export interface CliIo {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
type CliExitCode = 0 | 1 | 2 | 3;
|
|
51
|
-
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Every shape `parseArgs` can produce. Exported for `cli-commands.ts`, whose
|
|
57
|
+
* table must claim each variant or fail to compile.
|
|
58
|
+
*/
|
|
59
|
+
export type ParsedArgs =
|
|
52
60
|
| { command: 'add'; value: string }
|
|
53
61
|
| ReplayArgs
|
|
54
62
|
| BuildArgs
|
|
55
63
|
| DeployArgs
|
|
56
64
|
| { command: 'serve-webhook'; dataDir: string; port: number; admitted?: readonly string[] }
|
|
57
|
-
| { command: 'cloud-run'; value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean }
|
|
58
|
-
| { command: 'sync'; runId: string; json: boolean; root: string }
|
|
65
|
+
| { command: 'cloud-run'; value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean; noConnect: boolean }
|
|
66
|
+
| { command: 'sync'; runId: string; json: boolean; root: string; dryRun: boolean }
|
|
59
67
|
| CloudDeployArgs
|
|
60
68
|
| { command: 'deployments'; json: boolean }
|
|
61
69
|
| { command: 'undeploy'; agentId: string; json: boolean }
|
|
@@ -65,6 +73,7 @@ type ParsedArgs =
|
|
|
65
73
|
| { command: 'check'; json: boolean; watch: boolean; value: string }
|
|
66
74
|
| { 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 }
|
|
67
75
|
| { command: 'resume'; localAgent: boolean; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
|
|
76
|
+
| { command: 'answer'; dataDir: string; json: boolean; spawn: boolean; note: string | undefined; by: string | undefined; runId: string; waitId: string; answer: boolean }
|
|
68
77
|
| { command: 'observer'; dataDir: string }
|
|
69
78
|
| { command: 'hn-monitor'; sub: 'start'; dataDir: string; specPath: string; pollIntervalMs: number | undefined }
|
|
70
79
|
| { command: 'tick'; sub: 'start'; dataDir: string; specPath: string; scheduleId: string;
|
|
@@ -76,10 +85,10 @@ const USAGE = [
|
|
|
76
85
|
'flows add <helper-name|@flows/helper-name>',
|
|
77
86
|
'flows build [--out <dir>] <flow.yaml|flow.ts>',
|
|
78
87
|
'flows build --verify <bundle-dir>',
|
|
79
|
-
'flows deploy <flow.ts> --repo <owner/name> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--json]',
|
|
88
|
+
'flows deploy <flow.ts> --repo <owner/name> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--no-connect] [--json]',
|
|
80
89
|
'flows deployments [--json]',
|
|
81
90
|
'flows undeploy [--json] <deployment-id>',
|
|
82
|
-
'flows schedule <flow.yaml|flow.ts> [--cron "<expr>" | --every <n><s|m|h|d>] [--tz <IANA>] [--input <inline-json-or-file>] [--name <name>] [--json]',
|
|
91
|
+
'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]',
|
|
83
92
|
'flows schedules [--json]',
|
|
84
93
|
'flows unschedule [--json] <schedule-id>',
|
|
85
94
|
'flows deploy <flow>@sha256:<digest> --to <file-bucket-uri>',
|
|
@@ -87,12 +96,13 @@ const USAGE = [
|
|
|
87
96
|
'flows check [--watch] [--json] <flow.ts|flow.yaml|spec.json>',
|
|
88
97
|
'flows serve-webhook --data-dir <dir> --port <p> [--allow <name>[,<name>]]',
|
|
89
98
|
'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] [--reuse-from <run-id>] <flow.yaml|spec.json>',
|
|
90
|
-
'flows run --cloud [--json] [--wait] [--sync-code] <flow.yaml|spec.json>',
|
|
91
|
-
'flows run --cloud [--json] [--wait] [--sync-code] <flow.ts> --input <inline-json-or-file>',
|
|
92
|
-
'flows sync [--json] [--dir <path>] <run-id>',
|
|
99
|
+
'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] <flow.yaml|spec.json>',
|
|
100
|
+
'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] <flow.ts> --input <inline-json-or-file>',
|
|
101
|
+
'flows sync [--json] [--dry-run] [--dir <path>] <run-id>',
|
|
93
102
|
'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <flow.ts> --input <inline-json-or-file>',
|
|
94
103
|
'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>',
|
|
95
104
|
'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <run-id>',
|
|
105
|
+
'flows answer [--json] [--no-spawn] [--data-dir <dir>] [--note <text>] [--by <identity>] <run-id> <wait-id> <yes|no>',
|
|
96
106
|
'flows replay [--allow-human-influenced] [--json] [--data-dir <dir>] <run-id> [--at <step-id>]',
|
|
97
107
|
'flows observer [--data-dir <dir>]',
|
|
98
108
|
'flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>',
|
|
@@ -113,9 +123,48 @@ const PROCESS_IO: CliIo = {
|
|
|
113
123
|
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
114
124
|
};
|
|
115
125
|
|
|
126
|
+
/** Optional knobs for an embedded caller. `bin/flows.js` passes none. */
|
|
127
|
+
export interface RunCliOptions {
|
|
128
|
+
/**
|
|
129
|
+
* Cancellation for the long-running verbs (`run --cloud`, `check --watch`,
|
|
130
|
+
* `serve-webhook`, `hn-monitor start`, `tick start`).
|
|
131
|
+
*
|
|
132
|
+
* Supply one and `runCli` installs **no** process signal handlers -- required
|
|
133
|
+
* of a CLI surface mounted into another host, which owns SIGINT itself.
|
|
134
|
+
* Omit it and the standalone `flows` binary keeps today's behaviour exactly:
|
|
135
|
+
* SIGINT/SIGTERM are handled here, for the duration of that verb only.
|
|
136
|
+
*/
|
|
137
|
+
signal?: AbortSignal;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run one long-running verb under a cancellation signal.
|
|
142
|
+
*
|
|
143
|
+
* With a caller-supplied signal this installs nothing. Without one it owns
|
|
144
|
+
* SIGINT/SIGTERM for the duration of `body` and removes the handlers after --
|
|
145
|
+
* the pre-existing standalone behaviour, unchanged.
|
|
146
|
+
*/
|
|
147
|
+
async function withInterrupt<T>(
|
|
148
|
+
provided: AbortSignal | undefined,
|
|
149
|
+
body: (signal: AbortSignal) => Promise<T>,
|
|
150
|
+
): Promise<T> {
|
|
151
|
+
if (provided !== undefined) return body(provided);
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const onSignal = (): void => controller.abort();
|
|
154
|
+
process.once('SIGINT', onSignal);
|
|
155
|
+
process.once('SIGTERM', onSignal);
|
|
156
|
+
try {
|
|
157
|
+
return await body(controller.signal);
|
|
158
|
+
} finally {
|
|
159
|
+
process.off('SIGINT', onSignal);
|
|
160
|
+
process.off('SIGTERM', onSignal);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
116
164
|
export async function runCli(
|
|
117
165
|
args: readonly string[],
|
|
118
166
|
io: CliIo = PROCESS_IO,
|
|
167
|
+
options: RunCliOptions = {},
|
|
119
168
|
): Promise<CliExitCode> {
|
|
120
169
|
if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
|
|
121
170
|
io.stdout(USAGE);
|
|
@@ -131,9 +180,13 @@ export async function runCli(
|
|
|
131
180
|
|
|
132
181
|
if (parsed.command === 'add') return addPlugin(parsed.value, io);
|
|
133
182
|
|
|
134
|
-
if (parsed.command === 'serve-webhook')
|
|
183
|
+
if (parsed.command === 'serve-webhook') {
|
|
184
|
+
return withInterrupt(options.signal, (signal) => runServeWebhook(parsed, io, signal));
|
|
185
|
+
}
|
|
135
186
|
|
|
136
|
-
if (parsed.command === 'cloud-run')
|
|
187
|
+
if (parsed.command === 'cloud-run') {
|
|
188
|
+
return withInterrupt(options.signal, (signal) => runCloudCli(parsed, io, signal));
|
|
189
|
+
}
|
|
137
190
|
if (parsed.command === 'sync') return runCloudSyncCli(parsed, io);
|
|
138
191
|
if (parsed.command === 'cloud-deploy') return runCloudDeployCli(parsed, io);
|
|
139
192
|
if (parsed.command === 'deployments') return runCloudDeploymentsCli(parsed, io);
|
|
@@ -142,11 +195,22 @@ export async function runCli(
|
|
|
142
195
|
if (parsed.command === 'schedules') return runCloudSchedulesCli(parsed, io);
|
|
143
196
|
if (parsed.command === 'unschedule') return runCloudUnscheduleCli(parsed, io);
|
|
144
197
|
if (parsed.command === 'replay') return replayJournal(parsed, io);
|
|
198
|
+
if (parsed.command === 'answer') {
|
|
199
|
+
const execution = await answerFlow(parsed.runId, parsed.waitId, parsed.answer, parsed.dataDir, {
|
|
200
|
+
...(parsed.note === undefined ? {} : { note: parsed.note }),
|
|
201
|
+
...(parsed.by === undefined ? {} : { answeredBy: parsed.by }),
|
|
202
|
+
daemon: { spawn: parsed.spawn && spawnAllowedByEnv() },
|
|
203
|
+
});
|
|
204
|
+
emitRunReport(execution, parsed.json, io);
|
|
205
|
+
return execution.exitCode;
|
|
206
|
+
}
|
|
145
207
|
if (parsed.command === 'build') return runBuild(parsed, io);
|
|
146
208
|
if (parsed.command === 'deploy') return runDeploy(parsed, io);
|
|
147
209
|
|
|
148
210
|
if (parsed.command === 'check') {
|
|
149
|
-
if (parsed.watch)
|
|
211
|
+
if (parsed.watch) {
|
|
212
|
+
return withInterrupt(options.signal, (signal) => watchCheck(parsed.value, parsed.json, io, signal));
|
|
213
|
+
}
|
|
150
214
|
// Deliberately daemon-free (kernel/DAEMON-LIFECYCLE.md §4). `checkFlow` is
|
|
151
215
|
// a compile-and-preflight that opens no daemon socket, and the parser
|
|
152
216
|
// refuses `--data-dir` on `check`, so there is no data dir to attach to.
|
|
@@ -161,45 +225,27 @@ export async function runCli(
|
|
|
161
225
|
if (parsed.command === 'observer') return runObserverCommand(io);
|
|
162
226
|
|
|
163
227
|
if (parsed.command === 'hn-monitor') {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
dataDir: parsed.dataDir,
|
|
171
|
-
specPath: parsed.specPath,
|
|
172
|
-
pollIntervalMs: parsed.pollIntervalMs,
|
|
173
|
-
signal: controller.signal,
|
|
174
|
-
}, io);
|
|
175
|
-
} finally {
|
|
176
|
-
process.off('SIGINT', onSignal);
|
|
177
|
-
process.off('SIGTERM', onSignal);
|
|
178
|
-
}
|
|
228
|
+
return withInterrupt(options.signal, (signal) => runHnMonitor({
|
|
229
|
+
dataDir: parsed.dataDir,
|
|
230
|
+
specPath: parsed.specPath,
|
|
231
|
+
pollIntervalMs: parsed.pollIntervalMs,
|
|
232
|
+
signal,
|
|
233
|
+
}, io));
|
|
179
234
|
}
|
|
180
235
|
|
|
181
236
|
if (parsed.command === 'tick') {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
...(parsed.maxCatchUp === undefined ? {} : { maxCatchUp: parsed.maxCatchUp }),
|
|
195
|
-
},
|
|
196
|
-
pollIntervalMs: parsed.pollIntervalMs,
|
|
197
|
-
signal: controller.signal,
|
|
198
|
-
}, io) as CliExitCode;
|
|
199
|
-
} finally {
|
|
200
|
-
process.off('SIGINT', onSignal);
|
|
201
|
-
process.off('SIGTERM', onSignal);
|
|
202
|
-
}
|
|
237
|
+
return withInterrupt(options.signal, async (signal) => await runTickRunner({
|
|
238
|
+
dataDir: parsed.dataDir,
|
|
239
|
+
specPath: parsed.specPath,
|
|
240
|
+
schedule: {
|
|
241
|
+
scheduleId: parsed.scheduleId,
|
|
242
|
+
intervalMs: parsed.intervalMs,
|
|
243
|
+
...(parsed.epochMs === undefined ? {} : { epochMs: parsed.epochMs }),
|
|
244
|
+
...(parsed.maxCatchUp === undefined ? {} : { maxCatchUp: parsed.maxCatchUp }),
|
|
245
|
+
},
|
|
246
|
+
pollIntervalMs: parsed.pollIntervalMs,
|
|
247
|
+
signal,
|
|
248
|
+
}, io) as CliExitCode);
|
|
203
249
|
}
|
|
204
250
|
|
|
205
251
|
// Attach-or-spawn runs inside `runFlow`/`resumeFlow`/`runDirectFlow`, at the
|
|
@@ -277,6 +323,9 @@ async function checkAuthoredFlowComposed(path: string): Promise<{ report: CheckR
|
|
|
277
323
|
report: {
|
|
278
324
|
...mcp.report,
|
|
279
325
|
...(triggers?.report.schedules === undefined ? {} : { schedules: triggers.report.schedules }),
|
|
326
|
+
// The authored definition sees helper flags, body use and `cli:`
|
|
327
|
+
// declarations; the compiled view underneath knows only its steps.
|
|
328
|
+
...(triggers?.report.requirements === undefined ? {} : { requirements: triggers.report.requirements }),
|
|
280
329
|
diagnostics: [...helper.report.diagnostics, ...mcp.report.diagnostics, ...triggerDiagnostics],
|
|
281
330
|
ok: helper.report.ok && mcp.report.ok && triggerOk,
|
|
282
331
|
},
|
|
@@ -442,6 +491,11 @@ function emitWait(
|
|
|
442
491
|
|
|
443
492
|
function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
444
493
|
const command = args[0];
|
|
494
|
+
// The verb set lives in exactly one place -- `CLI_VERBS` in cli-commands.ts --
|
|
495
|
+
// which is also what `createRelayCliSurface` projects into `commands`. Gating
|
|
496
|
+
// dispatch on it means a token the surface does not declare can never reach a
|
|
497
|
+
// parser, so the declared tree and the dispatched tree cannot drift apart.
|
|
498
|
+
if (command === undefined || !CLI_VERB_NAMES.has(command)) return undefined;
|
|
445
499
|
if (command === 'add') return args.length === 2 ? { command: 'add', value: args[1]! } : undefined;
|
|
446
500
|
if (command === 'replay') return parseReplayArgs(args.slice(1));
|
|
447
501
|
if (command === 'build') return parseBuildArgs(args.slice(1));
|
|
@@ -479,6 +533,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
479
533
|
if (command === 'tick') return parseTickArgs(args.slice(1));
|
|
480
534
|
if (command === 'observer') return parseObserverArgs(args.slice(1));
|
|
481
535
|
if (command === 'sync') return parseSyncArgs(args.slice(1));
|
|
536
|
+
if (command === 'answer') return parseAnswerArgs(args.slice(1));
|
|
482
537
|
if (command !== 'check' && command !== 'run' && command !== 'resume') return undefined;
|
|
483
538
|
|
|
484
539
|
let json = false;
|
|
@@ -486,6 +541,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
486
541
|
let cloud = false;
|
|
487
542
|
let wait = false;
|
|
488
543
|
let syncCode = false;
|
|
544
|
+
let noConnect = false;
|
|
489
545
|
let localAgent = false;
|
|
490
546
|
let allowHumanInfluenced = false;
|
|
491
547
|
let dataDir = DEFAULT_DATA_DIR;
|
|
@@ -499,11 +555,13 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
499
555
|
const positionals: string[] = [];
|
|
500
556
|
for (let index = 1; index < args.length; index += 1) {
|
|
501
557
|
const argument = args[index]!;
|
|
502
|
-
if (argument === '--cloud' || argument === '--wait' || argument === '--sync-code') {
|
|
503
|
-
if (command !== 'run'
|
|
558
|
+
if (argument === '--cloud' || argument === '--wait' || argument === '--sync-code' || argument === '--no-connect') {
|
|
559
|
+
if (command !== 'run') return undefined;
|
|
560
|
+
if (argument === '--cloud' ? cloud : argument === '--wait' ? wait : argument === '--sync-code' ? syncCode : noConnect) return undefined;
|
|
504
561
|
if (argument === '--cloud') cloud = true;
|
|
505
562
|
else if (argument === '--wait') wait = true;
|
|
506
|
-
else syncCode = true;
|
|
563
|
+
else if (argument === '--sync-code') syncCode = true;
|
|
564
|
+
else noConnect = true;
|
|
507
565
|
continue;
|
|
508
566
|
}
|
|
509
567
|
if (argument === '--allow-human-influenced') {
|
|
@@ -583,9 +641,9 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
583
641
|
// the source, so it is accepted exactly where a local run accepts it.
|
|
584
642
|
if (allowHumanInfluenced || sawDataDir || !spawn || localAgent || noObserverLink || reuseFromRunId !== undefined) return undefined;
|
|
585
643
|
if (sawInput && !isAuthoredFlowPath(positionals[0]!)) return undefined;
|
|
586
|
-
return { command: 'cloud-run', value: positionals[0]!, json, wait, input, syncCode };
|
|
644
|
+
return { command: 'cloud-run', value: positionals[0]!, json, wait, input, syncCode, noConnect };
|
|
587
645
|
}
|
|
588
|
-
if (wait || syncCode) return undefined;
|
|
646
|
+
if (wait || syncCode || noConnect) return undefined;
|
|
589
647
|
if (reuseFromRunId !== undefined && isAuthoredFlowPath(positionals[0]!)) return undefined;
|
|
590
648
|
|
|
591
649
|
if (command === 'run' && input !== undefined && !isAuthoredFlowPath(positionals[0]!)) return undefined;
|
|
@@ -596,6 +654,66 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
596
654
|
: { command, localAgent, dataDir, json, spawn, noObserverLink, allowHumanInfluenced, value: positionals[0]! };
|
|
597
655
|
}
|
|
598
656
|
|
|
657
|
+
/**
|
|
658
|
+
* `flows answer [--json] [--no-spawn] [--data-dir <dir>] [--note <text>] [--by <identity>] <run-id> <wait-id> <yes|no>`.
|
|
659
|
+
* The answer is a literal `yes`/`no` (also `true`/`false`) so a shell cannot
|
|
660
|
+
* hand the kernel an ambiguous word as a decision. `--by` records who answered
|
|
661
|
+
* when the invoker is relaying a person's decision (Cloud's answer route runs
|
|
662
|
+
* this inside the resumed sandbox with the caller's identity); it defaults to
|
|
663
|
+
* the OS user.
|
|
664
|
+
*/
|
|
665
|
+
function parseAnswerArgs(rest: readonly string[]): ParsedArgs | undefined {
|
|
666
|
+
let json = false;
|
|
667
|
+
let spawn = true;
|
|
668
|
+
let dataDir = DEFAULT_DATA_DIR;
|
|
669
|
+
let sawDataDir = false;
|
|
670
|
+
let note: string | undefined;
|
|
671
|
+
let by: string | undefined;
|
|
672
|
+
const positionals: string[] = [];
|
|
673
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
674
|
+
const argument = rest[index]!;
|
|
675
|
+
if (argument === '--json') {
|
|
676
|
+
if (json) return undefined;
|
|
677
|
+
json = true;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (argument === '--no-spawn') {
|
|
681
|
+
if (!spawn) return undefined;
|
|
682
|
+
spawn = false;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (argument === '--by') {
|
|
686
|
+
const value = rest[index + 1];
|
|
687
|
+
if (by !== undefined || value === undefined || value.startsWith('-') || value.trim() === '') return undefined;
|
|
688
|
+
by = value;
|
|
689
|
+
index += 1;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (argument === '--data-dir') {
|
|
693
|
+
const value = rest[index + 1];
|
|
694
|
+
if (sawDataDir || value === undefined || value.startsWith('-')) return undefined;
|
|
695
|
+
dataDir = value;
|
|
696
|
+
sawDataDir = true;
|
|
697
|
+
index += 1;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
if (argument === '--note') {
|
|
701
|
+
const value = rest[index + 1];
|
|
702
|
+
if (note !== undefined || value === undefined || value.startsWith('--')) return undefined;
|
|
703
|
+
note = value;
|
|
704
|
+
index += 1;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (argument.startsWith('-')) return undefined;
|
|
708
|
+
positionals.push(argument);
|
|
709
|
+
}
|
|
710
|
+
if (positionals.length !== 3) return undefined;
|
|
711
|
+
const [runId, waitId, word] = positionals as [string, string, string];
|
|
712
|
+
const answer = word === 'yes' || word === 'true' ? true : word === 'no' || word === 'false' ? false : undefined;
|
|
713
|
+
if (answer === undefined) return undefined;
|
|
714
|
+
return { command: 'answer', dataDir, json, spawn, note, by, runId, waitId, answer };
|
|
715
|
+
}
|
|
716
|
+
|
|
599
717
|
function parseHnMonitorArgs(rest: readonly string[]): ParsedArgs | undefined {
|
|
600
718
|
const sub = rest[0];
|
|
601
719
|
if (sub !== 'start') return undefined;
|
|
@@ -638,9 +756,13 @@ function parseHnMonitorArgs(rest: readonly string[]): ParsedArgs | undefined {
|
|
|
638
756
|
* directory at all -- the mint is a pure Relaycast API round-trip. No
|
|
639
757
|
* positional argument, no other flags.
|
|
640
758
|
*/
|
|
641
|
-
/**
|
|
759
|
+
/**
|
|
760
|
+
* `flows sync [--json] [--dry-run] [--dir <path>] <run-id>`: apply a hosted
|
|
761
|
+
* run's patch to a local tree, or with `--dry-run` print it and apply nothing.
|
|
762
|
+
*/
|
|
642
763
|
function parseSyncArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
643
764
|
let json = false;
|
|
765
|
+
let dryRun = false;
|
|
644
766
|
let root: string | undefined;
|
|
645
767
|
const positionals: string[] = [];
|
|
646
768
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -650,6 +772,11 @@ function parseSyncArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
650
772
|
json = true;
|
|
651
773
|
continue;
|
|
652
774
|
}
|
|
775
|
+
if (argument === '--dry-run') {
|
|
776
|
+
if (dryRun) return undefined;
|
|
777
|
+
dryRun = true;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
653
780
|
if (argument === '--dir') {
|
|
654
781
|
const value = args[index + 1];
|
|
655
782
|
if (root !== undefined || value === undefined || value.startsWith('-')) return undefined;
|
|
@@ -661,7 +788,7 @@ function parseSyncArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
661
788
|
positionals.push(argument);
|
|
662
789
|
}
|
|
663
790
|
if (positionals.length !== 1) return undefined;
|
|
664
|
-
return { command: 'sync', runId: positionals[0]!, json, root: root ?? '.' };
|
|
791
|
+
return { command: 'sync', runId: positionals[0]!, json, dryRun, root: root ?? '.' };
|
|
665
792
|
}
|
|
666
793
|
|
|
667
794
|
function parseObserverArgs(rest: readonly string[]): ParsedArgs | undefined {
|
|
@@ -790,6 +917,10 @@ function emitCheckReport(report: CheckReport, json: boolean, io: CliIo): void {
|
|
|
790
917
|
const model = resolution.model === undefined ? '' : ` model "${resolution.model}"`;
|
|
791
918
|
io.stdout(`RESOLVED step "${resolution.stepId}" cli "${resolution.cli}"${model} from ${resolution.source}${config}`);
|
|
792
919
|
}
|
|
920
|
+
// What the workspace must have connected before this flow can run there;
|
|
921
|
+
// the hosted verbs check the same list against Cloud before submitting.
|
|
922
|
+
const requires = report.requirements === undefined ? '' : describeFlowRequirements(report.requirements);
|
|
923
|
+
if (requires) io.stdout(`REQUIRES ${requires}`);
|
|
793
924
|
if (report.ok) io.stdout(`CHECK PASSED ${report.path ?? ''}`.trimEnd());
|
|
794
925
|
}
|
|
795
926
|
|
|
@@ -810,6 +941,15 @@ function emitRunReport(
|
|
|
810
941
|
return;
|
|
811
942
|
}
|
|
812
943
|
if (report.runId === undefined) return;
|
|
944
|
+
if (report.answer !== undefined) {
|
|
945
|
+
io.stdout(`ANSWERED ${report.runId} ${report.answer.waitId} ${report.answer.answer ? 'yes' : 'no'}`
|
|
946
|
+
+ (report.answer.note === undefined ? '' : ` (${report.answer.note})`));
|
|
947
|
+
if (report.next !== undefined) io.stdout(`Continue with: ${report.next}`);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
// A refused answer changed nothing about the run, so there is no run
|
|
951
|
+
// outcome to summarize; the refusal above is the whole report.
|
|
952
|
+
if (report.command === 'answer') return;
|
|
813
953
|
const completed = report.completedSteps === undefined ? '' : ` (${report.completedSteps} ${report.completedSteps === 1 ? 'step' : 'steps'})`;
|
|
814
954
|
const reason = report.completionReason === undefined
|
|
815
955
|
? ''
|
|
@@ -863,3 +1003,15 @@ if (isDirectInvocation(process.argv[1])) {
|
|
|
863
1003
|
process.exitCode = exitCode;
|
|
864
1004
|
});
|
|
865
1005
|
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* The argv parser, exported for the CLI-surface drift test.
|
|
1009
|
+
*
|
|
1010
|
+
* The drift test must prove that every command `cli-commands.ts` declares
|
|
1011
|
+
* actually routes to a `ParsedArgs` variant, and that every variant is
|
|
1012
|
+
* reachable from some declared command. Observing that through `runCli` would
|
|
1013
|
+
* mean executing the commands. Not part of the package's public API --
|
|
1014
|
+
* `@relayflows/sdk/cli` exports `runCli`, and `@relayflows/sdk/relay-cli`
|
|
1015
|
+
* exports the surface.
|
|
1016
|
+
*/
|
|
1017
|
+
export { parseArgs as parseCliArgs };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { CloudFlowError, cloudFetch, cloudRequest, isCloudRecord, type CloudConnectionOptions } from './cloud-http.js';
|
|
3
|
+
import type { FlowIntegrationRequirement, FlowRequirements } from './flow-requirements.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Integration readiness before a hosted submission — the CLI form of the
|
|
7
|
+
* deploy wizard's "Connect integrations" step, and the same relayfile
|
|
8
|
+
* connect-session flow `agentworkforce deploy` runs for a proactive agent:
|
|
9
|
+
*
|
|
10
|
+
* GET /api/v1/workspaces/<id>/integrations/<provider>/status?scope=workspace
|
|
11
|
+
* POST /api/v1/workspaces/<id>/integrations/connect-session
|
|
12
|
+
* { allowedIntegrations: [<provider>], scope: { kind: "workspace" } }
|
|
13
|
+
* → { connectLink } (older: sessionUrl / url), opened in the browser,
|
|
14
|
+
* then the status is polled until it reports ready.
|
|
15
|
+
*
|
|
16
|
+
* Only `workspace` scope is used: a flow deployment runs as the workspace,
|
|
17
|
+
* which is the scope `checkFlowConnections` reads on the server. Nothing here
|
|
18
|
+
* stores a credential — Cloud's connect page does — and a refusal names the
|
|
19
|
+
* provider and the two ways to connect it (this prompt, or the dashboard).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface ConnectPrompt {
|
|
23
|
+
/** Asked once per missing provider before a browser opens; `false` refuses the submission. */
|
|
24
|
+
confirm(question: string): Promise<boolean>;
|
|
25
|
+
info(line: string): void;
|
|
26
|
+
/** Defaults to the platform opener (`open`, `xdg-open`, `start`); a failure only means the URL was printed. */
|
|
27
|
+
openUrl?(url: string): Promise<void> | void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EnsureConnectionsOptions extends CloudConnectionOptions {
|
|
31
|
+
workspaceId: string;
|
|
32
|
+
/** Omitted (`--no-connect`, or no TTY) means a missing integration is a refusal, never a prompt. */
|
|
33
|
+
prompt?: ConnectPrompt;
|
|
34
|
+
/** Status poll cadence while the browser connect is pending. Default 2 seconds. */
|
|
35
|
+
pollIntervalMs?: number;
|
|
36
|
+
/** How long one connect may stay pending. Default 5 minutes. */
|
|
37
|
+
connectTimeoutMs?: number;
|
|
38
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ConnectionsOutcome {
|
|
42
|
+
/** Providers the workspace already had. */
|
|
43
|
+
ready: string[];
|
|
44
|
+
/** Providers connected through this prompt, in order. */
|
|
45
|
+
connected: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const WORKSPACE_ID = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
49
|
+
const PROVIDER_ID = /^[a-z0-9][a-z0-9-]{0,63}$/u;
|
|
50
|
+
|
|
51
|
+
/** Whether the workspace has a ready connection for one provider. */
|
|
52
|
+
export async function integrationConnected(
|
|
53
|
+
workspaceId: string, provider: string, options: CloudConnectionOptions,
|
|
54
|
+
): Promise<boolean> {
|
|
55
|
+
assertIds(workspaceId, provider);
|
|
56
|
+
const base = `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/integrations`;
|
|
57
|
+
try {
|
|
58
|
+
const status = await cloudFetch(`${base}/${encodeURIComponent(provider)}/status?scope=workspace`, options,
|
|
59
|
+
{ method: 'GET', detail: true });
|
|
60
|
+
return isReady(status);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (!(error instanceof CloudFlowError) || error.code !== 'http_error') throw error;
|
|
63
|
+
if (error.refusal?.code === 'unknown_provider') {
|
|
64
|
+
throw new CloudFlowError('integration_not_connected',
|
|
65
|
+
`"${provider}" is not an integration this Cloud can connect; the flow declares it, so rename the declaration to a provider Cloud lists.`,
|
|
66
|
+
error.status, error.refusal);
|
|
67
|
+
}
|
|
68
|
+
// An older Cloud without the per-provider status route: the list still says which are connected.
|
|
69
|
+
if (error.status !== 404 && error.status !== 405) throw error;
|
|
70
|
+
const list = await cloudRequest(base, options);
|
|
71
|
+
const rows = Array.isArray(list) ? list : isCloudRecord(list) && Array.isArray(list.integrations) ? list.integrations : [];
|
|
72
|
+
return rows.some(row => isCloudRecord(row) && row.provider === provider && isReady(row));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Every integration the requirements name is ready when this resolves; each
|
|
78
|
+
* missing one was connected through the prompt, or the whole submission is
|
|
79
|
+
* refused with `integration_not_connected` naming the first missing provider.
|
|
80
|
+
*/
|
|
81
|
+
export async function ensureIntegrationsConnected(
|
|
82
|
+
requirements: Pick<FlowRequirements, 'integrations'>, options: EnsureConnectionsOptions,
|
|
83
|
+
): Promise<ConnectionsOutcome> {
|
|
84
|
+
const outcome: ConnectionsOutcome = { ready: [], connected: [] };
|
|
85
|
+
for (const integration of requirements.integrations) {
|
|
86
|
+
options.signal?.throwIfAborted();
|
|
87
|
+
if (await integrationConnected(options.workspaceId, integration.provider, options)) {
|
|
88
|
+
outcome.ready.push(integration.provider);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (options.prompt === undefined) throw notConnected(integration, options);
|
|
92
|
+
const label = providerLabel(integration.provider);
|
|
93
|
+
options.prompt.info(`This flow needs ${label} (${integration.detail}), which is not connected to this workspace.`);
|
|
94
|
+
if (!await options.prompt.confirm(`Connect ${label} now? (opens browser) [Y/n] `)) {
|
|
95
|
+
throw notConnected(integration, options);
|
|
96
|
+
}
|
|
97
|
+
await connectIntegration(integration.provider, options, options.prompt);
|
|
98
|
+
outcome.connected.push(integration.provider);
|
|
99
|
+
}
|
|
100
|
+
return outcome;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function connectIntegration(provider: string, options: EnsureConnectionsOptions, prompt: ConnectPrompt): Promise<void> {
|
|
104
|
+
const base = `/api/v1/workspaces/${encodeURIComponent(options.workspaceId)}/integrations`;
|
|
105
|
+
const session = await cloudFetch(`${base}/connect-session`, options, {
|
|
106
|
+
method: 'POST', detail: true,
|
|
107
|
+
body: JSON.stringify({ allowedIntegrations: [provider], scope: { kind: 'workspace' } }),
|
|
108
|
+
});
|
|
109
|
+
const url = isCloudRecord(session)
|
|
110
|
+
? [session.connectLink, session.sessionUrl, session.url].find((value): value is string => typeof value === 'string' && value.length > 0)
|
|
111
|
+
: undefined;
|
|
112
|
+
if (url === undefined || !/^https:\/\//u.test(url)) {
|
|
113
|
+
throw new CloudFlowError('invalid_response', 'Cloud did not return a connect link.');
|
|
114
|
+
}
|
|
115
|
+
prompt.info(`Opening ${url}`);
|
|
116
|
+
prompt.info(`If the browser did not open, visit that link, then return here; waiting for ${providerLabel(provider)} to connect…`);
|
|
117
|
+
try {
|
|
118
|
+
await (prompt.openUrl ?? openInBrowser)(url);
|
|
119
|
+
} catch {
|
|
120
|
+
// The link was printed; the poll below is what decides.
|
|
121
|
+
}
|
|
122
|
+
const interval = options.pollIntervalMs ?? 2_000;
|
|
123
|
+
const deadline = Date.now() + (options.connectTimeoutMs ?? 300_000);
|
|
124
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
125
|
+
while (Date.now() < deadline) {
|
|
126
|
+
await sleep(interval, options.signal);
|
|
127
|
+
options.signal?.throwIfAborted();
|
|
128
|
+
if (await integrationConnected(options.workspaceId, provider, options)) {
|
|
129
|
+
prompt.info(`${providerLabel(provider)} connected.`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw new CloudFlowError('integration_not_connected',
|
|
134
|
+
`${providerLabel(provider)} was not connected before the wait ran out. Finish at ${url}, then run the command again.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function notConnected(integration: FlowIntegrationRequirement, options: EnsureConnectionsOptions): CloudFlowError {
|
|
138
|
+
const label = providerLabel(integration.provider);
|
|
139
|
+
return new CloudFlowError('integration_not_connected',
|
|
140
|
+
`${label} is not connected to this workspace, and this flow needs it (${integration.detail}). `
|
|
141
|
+
+ `Connect it from the dashboard, or run again without --no-connect in a terminal to connect it now `
|
|
142
|
+
+ `(a browser opens a ${label} connect page for workspace ${options.workspaceId}).`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isReady(status: unknown): boolean {
|
|
146
|
+
if (!isCloudRecord(status)) return false;
|
|
147
|
+
return status.ready === true || status.state === 'ready' || status.status === 'ready' || status.status === 'connected';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function assertIds(workspaceId: string, provider: string): void {
|
|
151
|
+
if (!WORKSPACE_ID.test(workspaceId)) throw new CloudFlowError('invalid_response', 'Cloud reported an unusable workspace id.');
|
|
152
|
+
if (!PROVIDER_ID.test(provider)) throw new CloudFlowError('invalid_input', `"${provider}" is not an integration provider id.`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** `google-mail` → `Google Mail`, `github` → `GitHub`; presentation only. */
|
|
156
|
+
export function providerLabel(provider: string): string {
|
|
157
|
+
const known: Record<string, string> = { github: 'GitHub', gitlab: 'GitLab', hubspot: 'HubSpot', clickup: 'ClickUp' };
|
|
158
|
+
return known[provider] ?? provider.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function defaultSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
if (signal?.aborted) { reject(signal.reason); return; }
|
|
164
|
+
const timer = setTimeout(() => { signal?.removeEventListener('abort', onAbort); resolve(); }, ms);
|
|
165
|
+
function onAbort(): void { clearTimeout(timer); reject(signal!.reason); }
|
|
166
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Detached so a browser that outlives the CLI never holds its stdio; every
|
|
172
|
+
* failure is ignored. `FLOWS_NO_BROWSER=1` (an SSH session, CI) leaves the
|
|
173
|
+
* printed link as the only opener.
|
|
174
|
+
*/
|
|
175
|
+
function openInBrowser(url: string): void {
|
|
176
|
+
if (process.env['FLOWS_NO_BROWSER'] === '1') return;
|
|
177
|
+
const [command, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
178
|
+
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
|
|
179
|
+
: ['xdg-open', [url]];
|
|
180
|
+
try {
|
|
181
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
182
|
+
child.on('error', () => {});
|
|
183
|
+
child.unref();
|
|
184
|
+
} catch {
|
|
185
|
+
// Printed URL is the fallback.
|
|
186
|
+
}
|
|
187
|
+
}
|