@relayflows/sdk 2.0.30 → 2.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-artifacts.d.ts.map +1 -1
- package/dist/agent-artifacts.js +2 -9
- package/dist/agent-artifacts.js.map +1 -1
- package/dist/agent-cwd.d.ts +69 -0
- package/dist/agent-cwd.d.ts.map +1 -0
- package/dist/agent-cwd.js +140 -0
- package/dist/agent-cwd.js.map +1 -0
- package/dist/artifact-scan-policy.d.ts +35 -0
- package/dist/artifact-scan-policy.d.ts.map +1 -0
- package/dist/artifact-scan-policy.js +49 -0
- package/dist/artifact-scan-policy.js.map +1 -0
- package/dist/authored-completion-record.d.ts +35 -0
- package/dist/authored-completion-record.d.ts.map +1 -0
- package/dist/authored-completion-record.js +92 -0
- package/dist/authored-completion-record.js.map +1 -0
- package/dist/authored-completion.d.ts +99 -0
- package/dist/authored-completion.d.ts.map +1 -0
- package/dist/authored-completion.js +184 -0
- package/dist/authored-completion.js.map +1 -0
- package/dist/authored-flow-error.d.ts +13 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +12 -0
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +7 -30
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +54 -47
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-human.d.ts.map +1 -1
- package/dist/authored-human.js +1 -3
- package/dist/authored-human.js.map +1 -1
- package/dist/authored-node-entry.js +4 -1
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts +9 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +92 -17
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-preflight.d.ts +4 -0
- package/dist/authored-preflight.d.ts.map +1 -0
- package/dist/authored-preflight.js +50 -0
- package/dist/authored-preflight.js.map +1 -0
- package/dist/authored-root.d.ts +5 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +20 -2
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-output.d.ts +35 -10
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +108 -40
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-verdict.d.ts +28 -0
- package/dist/authored-verdict.d.ts.map +1 -0
- package/dist/authored-verdict.js +79 -0
- package/dist/authored-verdict.js.map +1 -0
- package/dist/authored-worker-step.d.ts +5 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +51 -13
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check-worker-surface.d.ts +27 -0
- package/dist/cli/check-worker-surface.d.ts.map +1 -0
- package/dist/cli/check-worker-surface.js +63 -0
- package/dist/cli/check-worker-surface.js.map +1 -0
- package/dist/cli/check.d.ts +28 -3
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +27 -140
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +20 -0
- package/dist/cli/cli-probe.d.ts.map +1 -0
- package/dist/cli/cli-probe.js +166 -0
- package/dist/cli/cli-probe.js.map +1 -0
- package/dist/cli/cloud-format.d.ts +9 -0
- package/dist/cli/cloud-format.d.ts.map +1 -0
- package/dist/cli/cloud-format.js +77 -0
- package/dist/cli/cloud-format.js.map +1 -0
- package/dist/cli/cloud-live.d.ts +39 -0
- package/dist/cli/cloud-live.d.ts.map +1 -0
- package/dist/cli/cloud-live.js +325 -0
- package/dist/cli/cloud-live.js.map +1 -0
- package/dist/cli/cloud-mirror-session.d.ts +97 -0
- package/dist/cli/cloud-mirror-session.d.ts.map +1 -0
- package/dist/cli/cloud-mirror-session.js +264 -0
- package/dist/cli/cloud-mirror-session.js.map +1 -0
- package/dist/cli/cloud-read.d.ts +3 -7
- package/dist/cli/cloud-read.d.ts.map +1 -1
- package/dist/cli/cloud-read.js +14 -266
- package/dist/cli/cloud-read.js.map +1 -1
- package/dist/cli/cloud-refusal.d.ts +39 -0
- package/dist/cli/cloud-refusal.d.ts.map +1 -0
- package/dist/cli/cloud-refusal.js +95 -0
- package/dist/cli/cloud-refusal.js.map +1 -0
- package/dist/cli/cloud-status-view.d.ts +7 -0
- package/dist/cli/cloud-status-view.d.ts.map +1 -0
- package/dist/cli/cloud-status-view.js +214 -0
- package/dist/cli/cloud-status-view.js.map +1 -0
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +16 -2
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/hn-monitor.d.ts +0 -13
- package/dist/cli/hn-monitor.d.ts.map +1 -1
- package/dist/cli/hn-monitor.js +2 -1
- package/dist/cli/hn-monitor.js.map +1 -1
- package/dist/cli/local-agent-remedy.d.ts +80 -0
- package/dist/cli/local-agent-remedy.d.ts.map +1 -0
- package/dist/cli/local-agent-remedy.js +116 -0
- package/dist/cli/local-agent-remedy.js.map +1 -0
- package/dist/cli/observer-session.d.ts +41 -0
- package/dist/cli/observer-session.d.ts.map +1 -0
- package/dist/cli/observer-session.js +78 -0
- package/dist/cli/observer-session.js.map +1 -0
- package/dist/cli/run.d.ts +30 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +186 -24
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +9 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +31 -2
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/step-evidence.d.ts +90 -0
- package/dist/cli/step-evidence.d.ts.map +1 -0
- package/dist/cli/step-evidence.js +259 -0
- package/dist/cli/step-evidence.js.map +1 -0
- package/dist/cli/step-failure.d.ts +14 -4
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +41 -76
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +12 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +3 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +134 -57
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +1 -1
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +24 -8
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-mirror-ledger.d.ts +40 -0
- package/dist/cloud-mirror-ledger.d.ts.map +1 -0
- package/dist/cloud-mirror-ledger.js +142 -0
- package/dist/cloud-mirror-ledger.js.map +1 -0
- package/dist/cloud-mirror-step.d.ts +168 -0
- package/dist/cloud-mirror-step.d.ts.map +1 -0
- package/dist/cloud-mirror-step.js +504 -0
- package/dist/cloud-mirror-step.js.map +1 -0
- package/dist/cloud-mirror-transport.d.ts +94 -0
- package/dist/cloud-mirror-transport.d.ts.map +1 -0
- package/dist/cloud-mirror-transport.js +163 -0
- package/dist/cloud-mirror-transport.js.map +1 -0
- package/dist/cloud-mirror.d.ts +132 -0
- package/dist/cloud-mirror.d.ts.map +1 -0
- package/dist/cloud-mirror.js +556 -0
- package/dist/cloud-mirror.js.map +1 -0
- package/dist/cloud-read.d.ts +15 -0
- package/dist/cloud-read.d.ts.map +1 -1
- package/dist/cloud-read.js +20 -1
- package/dist/cloud-read.js.map +1 -1
- package/dist/cloud-run-record.d.ts +27 -0
- package/dist/cloud-run-record.d.ts.map +1 -0
- package/dist/cloud-run-record.js +56 -0
- package/dist/cloud-run-record.js.map +1 -0
- package/dist/cloud-run.d.ts +2 -9
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +3 -20
- package/dist/cloud-run.js.map +1 -1
- package/dist/communication/local.d.ts.map +1 -1
- package/dist/communication/local.js +2 -1
- package/dist/communication/local.js.map +1 -1
- package/dist/communication/worker.d.ts +1 -1
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +13 -5
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +28 -6
- package/dist/compile.js.map +1 -1
- package/dist/direct-input.d.ts.map +1 -1
- package/dist/direct-input.js +21 -1
- package/dist/direct-input.js.map +1 -1
- package/dist/failure-kinds.d.ts +96 -6
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +29 -2
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-requirements.d.ts.map +1 -1
- package/dist/flow-requirements.js +24 -9
- package/dist/flow-requirements.js.map +1 -1
- package/dist/gate-contract.d.ts +7 -0
- package/dist/gate-contract.d.ts.map +1 -1
- package/dist/gate-contract.js +6 -1
- package/dist/gate-contract.js.map +1 -1
- package/dist/helper-preflight.d.ts.map +1 -1
- package/dist/helper-preflight.js +10 -3
- package/dist/helper-preflight.js.map +1 -1
- package/dist/helper-reference.d.ts +25 -0
- package/dist/helper-reference.d.ts.map +1 -0
- package/dist/helper-reference.js +129 -0
- package/dist/helper-reference.js.map +1 -0
- package/dist/hosted-extension-runtime.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +3 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +17 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/journal-projection.d.ts +16 -0
- package/dist/journal-projection.d.ts.map +1 -0
- package/dist/journal-projection.js +108 -0
- package/dist/journal-projection.js.map +1 -0
- package/dist/llm-worker.js +1 -1
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +4 -3
- package/dist/local-agent.js.map +1 -1
- package/dist/named-gate-lowering.js +35 -5
- package/dist/named-gate-lowering.js.map +1 -1
- package/dist/named-gate-preflight.d.ts +27 -0
- package/dist/named-gate-preflight.d.ts.map +1 -0
- package/dist/named-gate-preflight.js +47 -0
- package/dist/named-gate-preflight.js.map +1 -0
- package/dist/named-gates.d.ts +1 -1
- package/dist/named-gates.d.ts.map +1 -1
- package/dist/named-gates.js +3 -0
- package/dist/named-gates.js.map +1 -1
- package/dist/observer-link.d.ts +7 -0
- package/dist/observer-link.d.ts.map +1 -1
- package/dist/observer-link.js +3 -1
- package/dist/observer-link.js.map +1 -1
- package/dist/package-version.d.ts +10 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +22 -0
- package/dist/package-version.js.map +1 -0
- package/dist/preflight.d.ts +14 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +25 -3
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/redact.d.ts +42 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +128 -0
- package/dist/redact.js.map +1 -1
- package/dist/relay-cli.d.ts.map +1 -1
- package/dist/relay-cli.js +1 -21
- package/dist/relay-cli.js.map +1 -1
- package/dist/run-projection.d.ts +83 -0
- package/dist/run-projection.d.ts.map +1 -0
- package/dist/run-projection.js +199 -0
- package/dist/run-projection.js.map +1 -0
- package/dist/shell-word.d.ts +19 -0
- package/dist/shell-word.d.ts.map +1 -0
- package/dist/shell-word.js +23 -0
- package/dist/shell-word.js.map +1 -0
- package/dist/spec.d.ts +52 -3
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-fields.d.ts +1 -1
- package/dist/step-fields.js +1 -1
- package/dist/step-fields.js.map +1 -1
- package/dist/steps-green.d.ts +27 -0
- package/dist/steps-green.d.ts.map +1 -0
- package/dist/steps-green.js +164 -0
- package/dist/steps-green.js.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +39 -8
- package/dist/validate.js.map +1 -1
- package/dist/worker-lease.d.ts +8 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +34 -4
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.d.ts +8 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +17 -9
- package/dist/worker.js.map +1 -1
- package/package.json +3 -7
- package/src/agent-artifacts.ts +2 -10
- package/src/agent-cwd.ts +139 -0
- package/src/artifact-scan-policy.ts +51 -0
- package/src/authored-completion-record.ts +119 -0
- package/src/authored-completion.ts +203 -0
- package/src/authored-flow-error.ts +13 -1
- package/src/authored-flow-executor.ts +91 -58
- package/src/authored-human.ts +1 -4
- package/src/authored-node-entry.ts +4 -1
- package/src/authored-node-runner.ts +96 -22
- package/src/authored-preflight.ts +46 -0
- package/src/authored-root.ts +20 -2
- package/src/authored-step-output.ts +139 -42
- package/src/authored-verdict.ts +87 -0
- package/src/authored-worker-step.ts +63 -14
- package/src/cli/check-worker-surface.ts +66 -0
- package/src/cli/check.ts +59 -166
- package/src/cli/cli-probe.ts +194 -0
- package/src/cli/cloud-format.ts +82 -0
- package/src/cli/cloud-live.ts +366 -0
- package/src/cli/cloud-mirror-session.ts +329 -0
- package/src/cli/cloud-read.ts +13 -282
- package/src/cli/cloud-refusal.ts +113 -0
- package/src/cli/cloud-status-view.ts +213 -0
- package/src/cli/direct-run.ts +19 -2
- package/src/cli/hn-monitor.ts +2 -1
- package/src/cli/local-agent-remedy.ts +160 -0
- package/src/cli/observer-session.ts +107 -0
- package/src/cli/run.ts +233 -28
- package/src/cli/status.ts +59 -3
- package/src/cli/step-evidence.ts +310 -0
- package/src/cli/step-failure.ts +57 -81
- package/src/cli-commands.ts +3 -0
- package/src/cli.ts +155 -60
- package/src/cloud-deploy.ts +26 -8
- package/src/cloud-mirror-ledger.ts +161 -0
- package/src/cloud-mirror-step.ts +618 -0
- package/src/cloud-mirror-transport.ts +261 -0
- package/src/cloud-mirror.ts +633 -0
- package/src/cloud-read.ts +25 -1
- package/src/cloud-run-record.ts +64 -0
- package/src/cloud-run.ts +5 -24
- package/src/communication/local.ts +2 -1
- package/src/communication/worker.ts +13 -5
- package/src/compile.ts +28 -6
- package/src/direct-input.ts +22 -1
- package/src/failure-kinds.ts +104 -2
- package/src/flow-requirements.ts +24 -9
- package/src/gate-contract.ts +13 -1
- package/src/helper-preflight.ts +10 -3
- package/src/helper-reference.ts +130 -0
- package/src/hosted-extension-runtime.ts +1 -1
- package/src/index.ts +2 -0
- package/src/journal-client.ts +19 -2
- package/src/journal-projection.ts +106 -0
- package/src/llm-worker.ts +1 -1
- package/src/local-agent.ts +4 -2
- package/src/named-gate-lowering.ts +35 -5
- package/src/named-gate-preflight.ts +47 -0
- package/src/named-gates.ts +3 -0
- package/src/observer-link.ts +10 -1
- package/src/package-version.ts +23 -0
- package/src/preflight.ts +38 -7
- package/src/protocol.ts +2 -0
- package/src/redact.ts +111 -0
- package/src/relay-cli.ts +1 -24
- package/src/run-projection.ts +247 -0
- package/src/shell-word.ts +24 -0
- package/src/spec.ts +54 -3
- package/src/step-fields.ts +1 -1
- package/src/steps-green.ts +168 -0
- package/src/validate.ts +35 -8
- package/src/worker-lease.ts +35 -5
- package/src/worker.ts +27 -11
package/src/cli/check.ts
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
import { communicationInstruction } from '../communication/spec.js';
|
|
2
2
|
import { checkCommunicationEnvironment } from '../communication/preflight.js';
|
|
3
|
-
import { agentEnvironment, brokerEnvironment } from '../communication/environment.js';
|
|
4
3
|
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
5
4
|
import { dirname, isAbsolute, join, parse as parsePath, resolve } from 'node:path';
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
7
5
|
import { parse as parseYaml } from 'yaml';
|
|
8
6
|
import { CompileError, compileSpec, kernelToAuthoring } from '../compile.js';
|
|
7
|
+
import { agentWorkerDiagnostics } from './check-worker-surface.js';
|
|
9
8
|
import { helperReady } from '../yaml-helper-effect.js';
|
|
10
9
|
import { flowRequirements, type FlowRequirements } from '../flow-requirements.js';
|
|
11
|
-
import {
|
|
12
|
-
adapterIdentification,
|
|
13
|
-
authenticationProbe,
|
|
14
|
-
cliAdapterKind,
|
|
15
|
-
displayInvocation,
|
|
16
|
-
modelReadinessProbe,
|
|
17
|
-
type CliInvocation,
|
|
18
|
-
} from '../cli-adapter.js';
|
|
19
|
-
import { MODEL_ENV } from '../worker-cli.js';
|
|
10
|
+
import { probeCli, resolveExecutable } from './cli-probe.js';
|
|
20
11
|
import { modelNameError } from '../model-name.js';
|
|
21
12
|
import type { FlowSpec } from '../spec.js';
|
|
22
13
|
import type { McpServerConfig } from '../spec.js';
|
|
@@ -25,9 +16,9 @@ import type { StepGateInspection } from '../gate-contract.js';
|
|
|
25
16
|
import type { CheckFailureKind, CheckWarningKind } from '../failure-kinds.js';
|
|
26
17
|
import {
|
|
27
18
|
preflight,
|
|
28
|
-
CliProbeError,
|
|
29
19
|
type CliResolution,
|
|
30
20
|
type CliProbeResult,
|
|
21
|
+
type CliProbeOutcome,
|
|
31
22
|
type PreflightDiagnostic,
|
|
32
23
|
type PreflightProbes,
|
|
33
24
|
} from '../preflight.js';
|
|
@@ -117,6 +108,33 @@ export interface CheckExecution {
|
|
|
117
108
|
flow?: FlowSpec;
|
|
118
109
|
}
|
|
119
110
|
|
|
111
|
+
/** Compatibility options accepted by older authored callers. */
|
|
112
|
+
export interface AuthoredCheckOptions {
|
|
113
|
+
projectConfig?: ProjectConfig;
|
|
114
|
+
probeCache?: CliProbeOutcomeMap;
|
|
115
|
+
communicationChecked?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type CliProbeOutcomeMap = Map<string, CliProbeOutcome>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Facts about the *caller*, not about the spec, that change which diagnostics
|
|
122
|
+
* apply. Preflight stays a pure function of the spec plus environment probes;
|
|
123
|
+
* anything that depends on how the flow is about to be invoked opts in here.
|
|
124
|
+
*/
|
|
125
|
+
export interface CheckInvocation {
|
|
126
|
+
/**
|
|
127
|
+
* Report `agent_worker_unresolved` when the spec has `agent` steps.
|
|
128
|
+
*
|
|
129
|
+
* Only `flows check` sets this. `flows run` attaches its own worker under
|
|
130
|
+
* `--local-agent` and knows the answer, `flows build` and `flows deploy`
|
|
131
|
+
* check a spec that will run elsewhere, and an SDK caller that reaches
|
|
132
|
+
* `checkAuthoredFlow` directly is generally running a worker already —
|
|
133
|
+
* warning any of them would be noise about a question they have answered.
|
|
134
|
+
*/
|
|
135
|
+
warnUnresolvedAgentWorker?: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
120
138
|
export class CheckFailure extends Error {
|
|
121
139
|
constructor(readonly kind: CheckFailureKind, message: string) {
|
|
122
140
|
super(message);
|
|
@@ -124,7 +142,7 @@ export class CheckFailure extends Error {
|
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
/** Validate and preflight one working-tree spec without starting a run. */
|
|
127
|
-
export function checkFlow(path: string): CheckExecution {
|
|
145
|
+
export function checkFlow(path: string, invocation: CheckInvocation = {}): CheckExecution {
|
|
128
146
|
const absolutePath = resolve(path);
|
|
129
147
|
try {
|
|
130
148
|
const source = readFlowSource(absolutePath);
|
|
@@ -134,7 +152,7 @@ export function checkFlow(path: string): CheckExecution {
|
|
|
134
152
|
: [];
|
|
135
153
|
let execution: CheckExecution;
|
|
136
154
|
try {
|
|
137
|
-
execution = checkAuthoredFlow(readFlow(source, absolutePath), path);
|
|
155
|
+
execution = checkAuthoredFlow(readFlow(source, absolutePath), path, undefined, invocation);
|
|
138
156
|
} catch (error) {
|
|
139
157
|
if (!(error instanceof CheckFailure)) throw error;
|
|
140
158
|
execution = { report: inputFailureReport(error, path) };
|
|
@@ -164,12 +182,29 @@ function safeRequirements(authoring: FlowSpec, projectCli: string | undefined):
|
|
|
164
182
|
}
|
|
165
183
|
|
|
166
184
|
/** Preflight a validated authored flow through the same path as YAML/JSON. */
|
|
167
|
-
export function checkAuthoredFlow(
|
|
185
|
+
export function checkAuthoredFlow(
|
|
186
|
+
authoring: FlowSpec,
|
|
187
|
+
path: string,
|
|
188
|
+
projectConfigOrOptions?: ProjectConfig | AuthoredCheckOptions,
|
|
189
|
+
invocation: CheckInvocation = {},
|
|
190
|
+
cliProbeCache?: Map<string, CliProbeOutcome>,
|
|
191
|
+
): CheckExecution {
|
|
168
192
|
const absolutePath = resolve(path);
|
|
169
193
|
try {
|
|
194
|
+
const options = projectConfigOrOptions !== undefined
|
|
195
|
+
&& ('projectConfig' in projectConfigOrOptions
|
|
196
|
+
|| 'probeCache' in projectConfigOrOptions
|
|
197
|
+
|| 'communicationChecked' in projectConfigOrOptions)
|
|
198
|
+
? projectConfigOrOptions
|
|
199
|
+
: undefined;
|
|
200
|
+
const projectConfig = options !== undefined
|
|
201
|
+
? options.projectConfig
|
|
202
|
+
: projectConfigOrOptions as ProjectConfig | undefined;
|
|
203
|
+
const effectiveProbeCache = cliProbeCache ?? options?.probeCache;
|
|
170
204
|
const config = projectConfig ?? readProjectConfig(dirname(absolutePath));
|
|
171
205
|
const probes = systemProbes(dirname(absolutePath), config);
|
|
172
206
|
const result = preflight(authoring, {
|
|
207
|
+
...(effectiveProbeCache === undefined ? {} : { cliProbeCache: effectiveProbeCache }),
|
|
173
208
|
projectCli: config.cli,
|
|
174
209
|
projectConfigPath: config.path,
|
|
175
210
|
projectSearchStart: dirname(absolutePath),
|
|
@@ -186,7 +221,7 @@ export function checkAuthoredFlow(authoring: FlowSpec, path: string, projectConf
|
|
|
186
221
|
)
|
|
187
222
|
: undefined;
|
|
188
223
|
if (flow?.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
|
|
189
|
-
try { checkCommunicationEnvironment(flow); }
|
|
224
|
+
try { if (options?.communicationChecked !== true) checkCommunicationEnvironment(flow); }
|
|
190
225
|
catch (error) {
|
|
191
226
|
result.ok = false;
|
|
192
227
|
result.diagnostics.push({ severity: 'refusal', kind: 'probe_failed',
|
|
@@ -195,6 +230,13 @@ export function checkAuthoredFlow(authoring: FlowSpec, path: string, projectConf
|
|
|
195
230
|
result.diagnostics.push({ severity: 'warning', kind: 'budget_unmetered',
|
|
196
231
|
message: 'Managed communication sessions do not report token or dollar usage. Budget ceilings cannot bound their spend; communication.timeoutMs bounds their duration.' });
|
|
197
232
|
}
|
|
233
|
+
// Reported whatever preflight concluded. An environment refusal (a missing
|
|
234
|
+
// CLI, an unknown model) is fixed and rerun; the worker question is still
|
|
235
|
+
// open on the next pass, and staying silent about it here is what made an
|
|
236
|
+
// author meet it one dead run at a time.
|
|
237
|
+
const workerSurface = invocation.warnUnresolvedAgentWorker === true
|
|
238
|
+
? agentWorkerDiagnostics(authoring)
|
|
239
|
+
: [];
|
|
198
240
|
return {
|
|
199
241
|
report: {
|
|
200
242
|
ok: result.ok,
|
|
@@ -202,7 +244,7 @@ export function checkAuthoredFlow(authoring: FlowSpec, path: string, projectConf
|
|
|
202
244
|
...(config.path !== undefined ? { projectConfigPath: config.path } : {}),
|
|
203
245
|
gates: result.gates,
|
|
204
246
|
resolutions: result.resolutions,
|
|
205
|
-
diagnostics: result.diagnostics,
|
|
247
|
+
diagnostics: [...result.diagnostics, ...workerSurface],
|
|
206
248
|
requirements: safeRequirements(authoring, config.cli),
|
|
207
249
|
},
|
|
208
250
|
...(result.ok && flow !== undefined ? { flow } : {}),
|
|
@@ -448,155 +490,6 @@ function canonicalCli(cli: string, directory: string): string {
|
|
|
448
490
|
return resolve(directory, cli);
|
|
449
491
|
}
|
|
450
492
|
|
|
451
|
-
function probeCli(
|
|
452
|
-
cli: string,
|
|
453
|
-
directory: string,
|
|
454
|
-
model?: string,
|
|
455
|
-
execution?: 'managed',
|
|
456
|
-
): CliProbeResult {
|
|
457
|
-
const executable = resolveExecutable(cli, directory);
|
|
458
|
-
if (executable === undefined) return { exists: false, authenticated: false };
|
|
459
|
-
const kind = cliAdapterKind(executable);
|
|
460
|
-
// Relay owns interactive CLI launch/injection. Its generic PTY path is not
|
|
461
|
-
// the headless wrapper protocol; do not demand that protocol from Gemini,
|
|
462
|
-
// Cursor, OpenCode, or other interactive tools. Never invent an auth pass.
|
|
463
|
-
if (execution === 'managed' && kind === 'relayflows-wrapper-v1') {
|
|
464
|
-
return { exists: true, supported: true, authenticated: 'unverified' };
|
|
465
|
-
}
|
|
466
|
-
const environment = execution === 'managed'
|
|
467
|
-
? { ...brokerEnvironment(process.env), ...agentEnvironment(executable) } : process.env;
|
|
468
|
-
const probe = (invocation: CliInvocation) => runProbe(executable, directory, invocation, environment);
|
|
469
|
-
const identification = adapterIdentification(kind);
|
|
470
|
-
const identified = probe(identification.invocation);
|
|
471
|
-
if (
|
|
472
|
-
identified.status !== 0
|
|
473
|
-
|| (identification.expectedStdout !== undefined
|
|
474
|
-
&& identified.stdout.trim() !== identification.expectedStdout)
|
|
475
|
-
) {
|
|
476
|
-
return { exists: true, supported: false, authenticated: false };
|
|
477
|
-
}
|
|
478
|
-
const auth = authenticationProbe(kind);
|
|
479
|
-
const authCommand = displayInvocation(cli, auth);
|
|
480
|
-
if (model === undefined) {
|
|
481
|
-
return {
|
|
482
|
-
exists: true,
|
|
483
|
-
supported: true,
|
|
484
|
-
authenticated: probe(auth).status === 0,
|
|
485
|
-
authCommand,
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const scoped = modelReadinessProbe(kind, model);
|
|
490
|
-
const modelCommand = displayInvocation(cli, scoped);
|
|
491
|
-
// A successful real provider round trip (or identified wrapper probe)
|
|
492
|
-
// proves both auth and exact-model access. On failure, run the adapter's
|
|
493
|
-
// actual auth command solely to classify auth vs model access truthfully.
|
|
494
|
-
if (probe(scoped).status === 0) {
|
|
495
|
-
return {
|
|
496
|
-
exists: true,
|
|
497
|
-
supported: true,
|
|
498
|
-
authenticated: true,
|
|
499
|
-
modelAvailable: true,
|
|
500
|
-
authCommand,
|
|
501
|
-
modelCommand,
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
const authProbe = probe(auth);
|
|
505
|
-
const authenticated = authProbe.status === 0;
|
|
506
|
-
return {
|
|
507
|
-
exists: true,
|
|
508
|
-
supported: true,
|
|
509
|
-
authenticated,
|
|
510
|
-
modelAvailable: false,
|
|
511
|
-
authCommand,
|
|
512
|
-
modelCommand,
|
|
513
|
-
// Only on failure: on success there is nothing to explain, and the output
|
|
514
|
-
// is the most identity-bearing thing this function touches.
|
|
515
|
-
...(authenticated
|
|
516
|
-
? {}
|
|
517
|
-
: {
|
|
518
|
-
authExitCode: authProbe.status,
|
|
519
|
-
authFailureDetail: redactProbeOutput(
|
|
520
|
-
`${authProbe.stderr}${authProbe.stdout}`,
|
|
521
|
-
).trim().slice(0, 500),
|
|
522
|
-
}),
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Redact anything that looks like a credential or an account identifier.
|
|
528
|
-
*
|
|
529
|
-
* `auth status` output is diagnostic, but it is also the one place an account
|
|
530
|
-
* email, org id or token fragment can appear. The point of surfacing it is to
|
|
531
|
-
* say WHY a probe failed, which survives redaction; leaking an identity into a
|
|
532
|
-
* refusal message that gets pasted into issues does not.
|
|
533
|
-
*/
|
|
534
|
-
function redactProbeOutput(text: string): string {
|
|
535
|
-
return text
|
|
536
|
-
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '<redacted-email>')
|
|
537
|
-
.replace(/\b(sk|pk|oat|rt)[-_][A-Za-z0-9._-]{8,}/gi, '<redacted-token>')
|
|
538
|
-
.replace(/\b[A-Fa-f0-9]{32,}\b/g, '<redacted-hex>');
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function runProbe(
|
|
542
|
-
executable: string,
|
|
543
|
-
directory: string,
|
|
544
|
-
invocation: CliInvocation,
|
|
545
|
-
environment: NodeJS.ProcessEnv = process.env,
|
|
546
|
-
): { status: number | null; stdout: string; stderr: string } {
|
|
547
|
-
const env = { ...environment };
|
|
548
|
-
delete env[MODEL_ENV];
|
|
549
|
-
if (invocation.modelEnv !== undefined) env[MODEL_ENV] = invocation.modelEnv;
|
|
550
|
-
const result = spawnSync(executable, invocation.args, {
|
|
551
|
-
cwd: directory,
|
|
552
|
-
encoding: 'utf8',
|
|
553
|
-
// stderr was 'ignore'. A failing `auth status` writes its reason there, so
|
|
554
|
-
// discarding it made every authentication refusal structurally
|
|
555
|
-
// undiagnosable: the refusal could say a probe exited non-zero and never
|
|
556
|
-
// what it said. Captured, then redacted at the point of use.
|
|
557
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
558
|
-
timeout: invocation.timeoutMs,
|
|
559
|
-
env,
|
|
560
|
-
});
|
|
561
|
-
const failure = classifySpawnFailure(result.error, result.signal, invocation.timeoutMs);
|
|
562
|
-
if (failure !== undefined) throw failure;
|
|
563
|
-
return {
|
|
564
|
-
status: result.status,
|
|
565
|
-
stdout: result.stdout,
|
|
566
|
-
stderr: result.stderr ?? '',
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function resolveExecutable(command: string, directory: string): string | undefined {
|
|
571
|
-
if (command.includes('/') || isAbsolute(command)) {
|
|
572
|
-
const path = isAbsolute(command) ? command : resolve(directory, command);
|
|
573
|
-
try {
|
|
574
|
-
accessSync(path, constants.X_OK);
|
|
575
|
-
return path;
|
|
576
|
-
} catch {
|
|
577
|
-
return undefined;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
const result = spawnSync('which', [command], { encoding: 'utf8', timeout: 5_000 });
|
|
581
|
-
const failure = classifySpawnFailure(result.error, result.signal, 5_000);
|
|
582
|
-
if (failure !== undefined) throw failure;
|
|
583
|
-
return result.status === 0 ? result.stdout.trim() : undefined;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function classifySpawnFailure(
|
|
587
|
-
error: Error | undefined,
|
|
588
|
-
signal: NodeJS.Signals | null,
|
|
589
|
-
timeoutMs: number,
|
|
590
|
-
): CliProbeError | undefined {
|
|
591
|
-
if (error !== undefined) {
|
|
592
|
-
const detail = (error as NodeJS.ErrnoException).code === 'ETIMEDOUT'
|
|
593
|
-
? `timeout:${timeoutMs}ms` as const
|
|
594
|
-
: 'spawn_failed' as const;
|
|
595
|
-
return new CliProbeError(detail);
|
|
596
|
-
}
|
|
597
|
-
return signal === null ? undefined : new CliProbeError(`signal:${signal}`);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
493
|
function executableExists(command: string, directory: string): boolean {
|
|
601
494
|
return resolveExecutable(command, directory) !== undefined;
|
|
602
495
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { accessSync, constants } from 'node:fs';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { execFile, spawnSync } from 'node:child_process';
|
|
4
|
+
import { agentEnvironment, brokerEnvironment } from '../communication/environment.js';
|
|
5
|
+
import { adapterIdentification, authenticationProbe, cliAdapterKind, displayInvocation,
|
|
6
|
+
modelReadinessProbe, type CliInvocation } from '../cli-adapter.js';
|
|
7
|
+
import { MODEL_ENV } from '../worker-cli.js';
|
|
8
|
+
import { CliProbeError, type CliProbeResult } from '../preflight.js';
|
|
9
|
+
|
|
10
|
+
interface ProbeRequest {
|
|
11
|
+
executable: string;
|
|
12
|
+
directory: string;
|
|
13
|
+
invocation: CliInvocation;
|
|
14
|
+
environment: NodeJS.ProcessEnv;
|
|
15
|
+
}
|
|
16
|
+
interface ProbeOutput { status: number | null; stdout: string; stderr: string }
|
|
17
|
+
|
|
18
|
+
/** One decision sequence; checking uses synchronous I/O, live flows yield the loop. */
|
|
19
|
+
export function probeCli(...args: Parameters<typeof probeSequence>): CliProbeResult {
|
|
20
|
+
return driveSync(probeSequence(...args));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function probeCliAsync(...args: Parameters<typeof probeSequence>): Promise<CliProbeResult> {
|
|
24
|
+
const sequence = probeSequence(...args);
|
|
25
|
+
let next = sequence.next();
|
|
26
|
+
while (!next.done) next = sequence.next(await runProbeAsync(next.value));
|
|
27
|
+
return next.value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveExecutable(command: string, directory: string): string | undefined {
|
|
31
|
+
return driveSync(executableSequence(command, directory));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function driveSync<T>(sequence: Generator<ProbeRequest, T, ProbeOutput>): T {
|
|
35
|
+
let next = sequence.next();
|
|
36
|
+
while (!next.done) {
|
|
37
|
+
const request = next.value;
|
|
38
|
+
const result = spawnSync(request.executable, request.invocation.args, {
|
|
39
|
+
...probeOptions(request),
|
|
40
|
+
// Preserve the old synchronous probe contract: provider CLIs must not
|
|
41
|
+
// inherit a readable stdin that can block auth/identify probes.
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
});
|
|
44
|
+
const failure = classifySpawnFailure(result.error, result.signal, request.invocation.timeoutMs);
|
|
45
|
+
if (failure !== undefined) throw failure;
|
|
46
|
+
next = sequence.next({ status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' });
|
|
47
|
+
}
|
|
48
|
+
return next.value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function probeOptions({ directory, invocation, environment }: ProbeRequest) {
|
|
52
|
+
const env = { ...environment };
|
|
53
|
+
delete env[MODEL_ENV];
|
|
54
|
+
if (invocation.modelEnv !== undefined) env[MODEL_ENV] = invocation.modelEnv;
|
|
55
|
+
return { cwd: directory, encoding: 'utf8' as const, timeout: invocation.timeoutMs,
|
|
56
|
+
maxBuffer: 1024 * 1024, env };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function runProbeAsync(request: ProbeRequest): Promise<ProbeOutput> {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const child = execFile(request.executable, request.invocation.args, probeOptions(request),
|
|
62
|
+
(error, stdout, stderr) => {
|
|
63
|
+
// Numeric exit codes are probe results; launch, timeout, signal and buffer
|
|
64
|
+
// errors are failures to collect a fact, just as in the synchronous driver.
|
|
65
|
+
if (error?.killed && typeof error.code !== 'string') return reject(new CliProbeError(`timeout:${request.invocation.timeoutMs}ms`));
|
|
66
|
+
const failure = classifySpawnFailure(
|
|
67
|
+
error !== null && typeof error.code !== 'number' && error.signal == null ? error : undefined,
|
|
68
|
+
error?.signal ?? null, request.invocation.timeoutMs);
|
|
69
|
+
if (failure !== undefined) return reject(failure);
|
|
70
|
+
resolve({ status: error === null ? 0 : typeof error.code === 'number' ? error.code : null,
|
|
71
|
+
stdout, stderr });
|
|
72
|
+
});
|
|
73
|
+
child.stdin?.end();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function* probeSequence(
|
|
78
|
+
cli: string,
|
|
79
|
+
directory: string,
|
|
80
|
+
model?: string,
|
|
81
|
+
execution?: 'managed',
|
|
82
|
+
): Generator<ProbeRequest, CliProbeResult, ProbeOutput> {
|
|
83
|
+
const executable = yield* executableSequence(cli, directory);
|
|
84
|
+
if (executable === undefined) return { exists: false, authenticated: false };
|
|
85
|
+
const kind = cliAdapterKind(executable);
|
|
86
|
+
// Relay owns interactive CLI launch/injection. Its generic PTY path is not
|
|
87
|
+
// the headless wrapper protocol; do not demand that protocol from Gemini,
|
|
88
|
+
// Cursor, OpenCode, or other interactive tools. Never invent an auth pass.
|
|
89
|
+
if (execution === 'managed' && kind === 'relayflows-wrapper-v1') {
|
|
90
|
+
return { exists: true, supported: true, authenticated: 'unverified' };
|
|
91
|
+
}
|
|
92
|
+
const environment = execution === 'managed'
|
|
93
|
+
? { ...brokerEnvironment(process.env), ...agentEnvironment(executable) } : process.env;
|
|
94
|
+
const probe = (invocation: CliInvocation): ProbeRequest => ({ executable, directory, invocation, environment });
|
|
95
|
+
const identification = adapterIdentification(kind);
|
|
96
|
+
const identified = yield probe(identification.invocation);
|
|
97
|
+
if (
|
|
98
|
+
identified.status !== 0
|
|
99
|
+
|| (identification.expectedStdout !== undefined
|
|
100
|
+
&& identified.stdout.trim() !== identification.expectedStdout)
|
|
101
|
+
) {
|
|
102
|
+
return { exists: true, supported: false, authenticated: false };
|
|
103
|
+
}
|
|
104
|
+
const auth = authenticationProbe(kind);
|
|
105
|
+
const authCommand = displayInvocation(cli, auth);
|
|
106
|
+
if (model === undefined) {
|
|
107
|
+
return {
|
|
108
|
+
exists: true,
|
|
109
|
+
supported: true,
|
|
110
|
+
authenticated: (yield probe(auth)).status === 0,
|
|
111
|
+
authCommand,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const scoped = modelReadinessProbe(kind, model);
|
|
116
|
+
const modelCommand = displayInvocation(cli, scoped);
|
|
117
|
+
// A successful real provider round trip (or identified wrapper probe)
|
|
118
|
+
// proves both auth and exact-model access. On failure, run the adapter's
|
|
119
|
+
// actual auth command solely to classify auth vs model access truthfully.
|
|
120
|
+
if ((yield probe(scoped)).status === 0) {
|
|
121
|
+
return {
|
|
122
|
+
exists: true,
|
|
123
|
+
supported: true,
|
|
124
|
+
authenticated: true,
|
|
125
|
+
modelAvailable: true,
|
|
126
|
+
authCommand,
|
|
127
|
+
modelCommand,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const authProbe = yield probe(auth);
|
|
131
|
+
const authenticated = authProbe.status === 0;
|
|
132
|
+
return {
|
|
133
|
+
exists: true,
|
|
134
|
+
supported: true,
|
|
135
|
+
authenticated,
|
|
136
|
+
modelAvailable: false,
|
|
137
|
+
authCommand,
|
|
138
|
+
modelCommand,
|
|
139
|
+
// Only on failure: on success there is nothing to explain, and the output
|
|
140
|
+
// is the most identity-bearing thing this function touches.
|
|
141
|
+
...(authenticated
|
|
142
|
+
? {}
|
|
143
|
+
: {
|
|
144
|
+
authExitCode: authProbe.status,
|
|
145
|
+
authFailureDetail: redactProbeOutput(
|
|
146
|
+
`${authProbe.stderr}${authProbe.stdout}`,
|
|
147
|
+
).trim().slice(0, 500),
|
|
148
|
+
}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Redact anything that looks like a credential or an account identifier.
|
|
154
|
+
*
|
|
155
|
+
* `auth status` output is diagnostic, but it is also the one place an account
|
|
156
|
+
* email, org id or token fragment can appear. The point of surfacing it is to
|
|
157
|
+
* say WHY a probe failed, which survives redaction; leaking an identity into a
|
|
158
|
+
* refusal message that gets pasted into issues does not.
|
|
159
|
+
*/
|
|
160
|
+
function redactProbeOutput(text: string): string {
|
|
161
|
+
return text
|
|
162
|
+
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '<redacted-email>')
|
|
163
|
+
.replace(/\b(sk|pk|oat|rt)[-_][A-Za-z0-9._-]{8,}/gi, '<redacted-token>')
|
|
164
|
+
.replace(/\b[A-Fa-f0-9]{32,}\b/g, '<redacted-hex>');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function* executableSequence(command: string, directory: string): Generator<ProbeRequest, string | undefined, ProbeOutput> {
|
|
168
|
+
if (command.includes('/') || isAbsolute(command)) {
|
|
169
|
+
const path = isAbsolute(command) ? command : resolve(directory, command);
|
|
170
|
+
try {
|
|
171
|
+
accessSync(path, constants.X_OK);
|
|
172
|
+
return path;
|
|
173
|
+
} catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const result = yield { executable: 'which', directory: process.cwd(),
|
|
178
|
+
invocation: { args: [command], timeoutMs: 5_000 }, environment: process.env };
|
|
179
|
+
return result.status === 0 ? result.stdout.trim() : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function classifySpawnFailure(
|
|
183
|
+
error: Error | undefined,
|
|
184
|
+
signal: NodeJS.Signals | null,
|
|
185
|
+
timeoutMs: number,
|
|
186
|
+
): CliProbeError | undefined {
|
|
187
|
+
if (error !== undefined) {
|
|
188
|
+
const detail = (error as NodeJS.ErrnoException).code === 'ETIMEDOUT'
|
|
189
|
+
? `timeout:${timeoutMs}ms` as const
|
|
190
|
+
: 'spawn_failed' as const;
|
|
191
|
+
return new CliProbeError(detail);
|
|
192
|
+
}
|
|
193
|
+
return signal === null ? undefined : new CliProbeError(`signal:${signal}`);
|
|
194
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// The cells every hosted-run page is built from.
|
|
2
|
+
//
|
|
3
|
+
// Moved out of `cli/cloud-read.ts` verbatim when the live views (`--watch`,
|
|
4
|
+
// `--follow`) needed the same formatting: the run list, the status page and
|
|
5
|
+
// the watched page must render a count, a cost and an instant identically, or
|
|
6
|
+
// a reader would have to learn which verb they were looking at first.
|
|
7
|
+
//
|
|
8
|
+
// Depends on nothing in `cli/` but `formatDuration`, so every other Cloud CLI
|
|
9
|
+
// module can import it without an import cycle.
|
|
10
|
+
|
|
11
|
+
import { formatDuration } from './status.js';
|
|
12
|
+
|
|
13
|
+
export function thousands(value: number): string {
|
|
14
|
+
return String(value).replace(/\B(?=(\d{3})+(?!\d))/gu, ',');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function dollars(value: number): string {
|
|
18
|
+
return `$${value.toFixed(6).replace(/0+$/u, '').replace(/\.$/u, '')}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Agent- and flow-authored names cannot inject terminal control sequences. */
|
|
22
|
+
export function safe(text: string): string {
|
|
23
|
+
return text.replace(/[\u0000-\u001F\u007F-\u009F]/gu, '?');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Render a run's `error` as readable lines rather than one control-char smear.
|
|
28
|
+
*
|
|
29
|
+
* Cloud stores the runner's terminal output in this field, newlines and all, so
|
|
30
|
+
* passing it through `safe()` alone turns a 200-line tail into a single line of
|
|
31
|
+
* `?` separators. Long runs are dominated by lease renewals — one line every
|
|
32
|
+
* 10s for the life of every agent step, differing only in the deadline — which
|
|
33
|
+
* are worth counting, not reading.
|
|
34
|
+
*
|
|
35
|
+
* Collapsing is deliberately narrow: two adjacent lines merge only when they
|
|
36
|
+
* are character-for-character identical once a trailing integer is masked. A
|
|
37
|
+
* shared prefix is NOT line identity — runner lines put the step name, reason
|
|
38
|
+
* or message after a long fixed prefix, so collapsing on a prefix would hide
|
|
39
|
+
* distinct diagnostics behind a similarity count. The final line never
|
|
40
|
+
* collapses into an earlier one, because that is where the failure is.
|
|
41
|
+
*/
|
|
42
|
+
const TRAILING_NUMBER = /\d+(?=\D{0,2}$)/u;
|
|
43
|
+
|
|
44
|
+
export function errorLines(text: string, indent: string): string[] {
|
|
45
|
+
const raw = text.split(/\r\n|\r|\n/u).map((line) => line.trimEnd()).filter((line) => line !== '');
|
|
46
|
+
if (raw.length === 0) return [];
|
|
47
|
+
|
|
48
|
+
const key = (line: string): string => line.replace(TRAILING_NUMBER, '#');
|
|
49
|
+
const collapsed: { line: string; count: number }[] = [];
|
|
50
|
+
raw.forEach((line, index) => {
|
|
51
|
+
const previous = collapsed.at(-1);
|
|
52
|
+
const isLast = index === raw.length - 1;
|
|
53
|
+
if (previous !== undefined && !isLast && key(previous.line) === key(line)) previous.count += 1;
|
|
54
|
+
else collapsed.push({ line, count: 1 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const rendered = collapsed.map(({ line, count }) =>
|
|
58
|
+
count === 1 ? safe(line) : `${safe(line)} (${count} times, differing only in a number)`);
|
|
59
|
+
|
|
60
|
+
const HEAD = 2;
|
|
61
|
+
const TAIL = 12;
|
|
62
|
+
if (rendered.length <= HEAD + TAIL + 1) return rendered.map((line) => `${indent}${line}`);
|
|
63
|
+
const elided = rendered.length - HEAD - TAIL;
|
|
64
|
+
return [
|
|
65
|
+
...rendered.slice(0, HEAD),
|
|
66
|
+
`… ${elided} more line${elided === 1 ? '' : 's'} (full text: --json)`,
|
|
67
|
+
...rendered.slice(-TAIL),
|
|
68
|
+
].map((line) => `${indent}${line}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** ISO-8601 to the second: a list column, not a timestamp to do arithmetic on. */
|
|
72
|
+
export function instant(value: string | null): string {
|
|
73
|
+
if (value === null) return 'unknown';
|
|
74
|
+
const parsed = Date.parse(value);
|
|
75
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString().replace(/\.\d{3}Z$/u, 'Z') : 'unknown';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function ago(value: string | null, now: number): string {
|
|
79
|
+
if (value === null) return 'unknown';
|
|
80
|
+
const parsed = Date.parse(value);
|
|
81
|
+
return Number.isFinite(parsed) ? `${formatDuration(now - parsed)} ago` : 'unknown';
|
|
82
|
+
}
|