@deksden-com/dd-flow-cli 0.9.0-beta.135 → 0.9.0-beta.136
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/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +1 -0
- package/dist/cli/command-metadata.js +144 -0
- package/dist/cli/command-option-descriptions.js +186 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +71 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +69 -0
- package/dist/cli/run-cli.js +5 -0
- package/dist/cli/setup.js +147 -0
- package/dist/services/cli-operation-classifier.js +4 -0
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +1 -1
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hybrid-contracts.js +455 -0
- package/dist/services/installation-lock.js +92 -0
- package/dist/services/managed-processes.js +30 -0
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/run-controller.js +1 -0
- package/dist/services/run-health.js +149 -0
- package/dist/services/runs.js +61 -13
- package/dist/services/setup.js +377 -0
- package/dist/storage/database.js +18 -3
- package/dist/storage/writer-contract.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { prepareOperatorProfile, prepareProfileCreation, readOperatorProfileInput } from "../services/operator-profiles.js";
|
|
4
|
+
/** One explicit choice; prompts go to stderr so stdout remains machine-readable. */
|
|
5
|
+
export async function askExecutorChoice(settings, io) {
|
|
6
|
+
const ids = settings.profiles.map(entry => entry.id);
|
|
7
|
+
io.stderr.write(`Executor profiles: ${ids.length ? ids.join(", ") : "none configured"}\n`);
|
|
8
|
+
io.stderr.write("Choose a profile ID/alias, file:/absolute/profile.json, create, or skip. No model is selected automatically.\n");
|
|
9
|
+
const terminal = createInterface({ input: io.stdin, output: io.stderr });
|
|
10
|
+
const lines = terminal[Symbol.asyncIterator]();
|
|
11
|
+
const cancelled = () => new AppError("onboarding_cancelled", "Executor onboarding was cancelled", 1, { effect: "no_effect" });
|
|
12
|
+
terminal.on("SIGINT", () => terminal.close());
|
|
13
|
+
const ask = async (prompt) => {
|
|
14
|
+
io.stderr.write(prompt);
|
|
15
|
+
const line = await lines.next();
|
|
16
|
+
if (line.done)
|
|
17
|
+
throw cancelled();
|
|
18
|
+
const answer = line.value.trim();
|
|
19
|
+
if (answer.length > 4096)
|
|
20
|
+
throw new AppError("usage", "Executor choice exceeds the input limit", 2, { effect: "no_effect" });
|
|
21
|
+
return answer;
|
|
22
|
+
};
|
|
23
|
+
try {
|
|
24
|
+
const answer = await ask("Executor: ");
|
|
25
|
+
if (answer === "skip")
|
|
26
|
+
return { skip: true };
|
|
27
|
+
if (answer === "create") {
|
|
28
|
+
io.stderr.write("Enter explicit profile settings; this does not probe providers or verify model access. Nothing is saved by this dialog.\n");
|
|
29
|
+
const input = { schema_id: "dd-flow/agent-profile@1" };
|
|
30
|
+
for (const [key, prompt] of [
|
|
31
|
+
["id", "Profile ID: "], ["harness", "Harness (codex/zcode/agy/opencode/grok/droid): "],
|
|
32
|
+
["provider", "Provider ID: "], ["model", "Model ID: "], ["reasoning", "Reasoning setting: "],
|
|
33
|
+
["mode", "Mode setting: "], ["permission", "Permission (allow/deny): "]
|
|
34
|
+
])
|
|
35
|
+
input[key] = await ask(prompt);
|
|
36
|
+
const profile = prepareOperatorProfile(input);
|
|
37
|
+
if (settings.profiles.some(entry => entry.id === profile.id) || Object.hasOwn(settings.aliases, profile.id)) {
|
|
38
|
+
throw new AppError("profile_id_conflict", "New profile ID collides with an existing profile or alias", 2, { effect: "no_effect" });
|
|
39
|
+
}
|
|
40
|
+
if (await ask("Use this profile for this operation? Type yes: ") !== "yes")
|
|
41
|
+
throw cancelled();
|
|
42
|
+
const save = await ask("Save this profile globally during installation? Type yes or no: ");
|
|
43
|
+
if (save !== "yes" && save !== "no")
|
|
44
|
+
throw cancelled();
|
|
45
|
+
if (save === "yes") {
|
|
46
|
+
const defaultChoice = await ask("Set it as global default? Type yes or no: ");
|
|
47
|
+
if (defaultChoice !== "yes" && defaultChoice !== "no")
|
|
48
|
+
throw cancelled();
|
|
49
|
+
return { profile, profileCreation: prepareProfileCreation(settings, profile, defaultChoice === "yes") };
|
|
50
|
+
}
|
|
51
|
+
return { profile };
|
|
52
|
+
}
|
|
53
|
+
if (answer.startsWith("file:")) {
|
|
54
|
+
const file = answer.slice(5);
|
|
55
|
+
if (!file.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(file))
|
|
56
|
+
throw new AppError("usage", "Profile file must be an absolute path", 2, { effect: "no_effect" });
|
|
57
|
+
return { profile: readOperatorProfileInput(file) };
|
|
58
|
+
}
|
|
59
|
+
if (!answer)
|
|
60
|
+
throw new AppError("agent_profile_missing", "An explicit executor choice is required", 1, { effect: "no_effect" });
|
|
61
|
+
return { profileId: answer };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof AppError)
|
|
65
|
+
throw error;
|
|
66
|
+
throw cancelled();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
terminal.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { prepareCommandRoute } from "./command-inputs.js";
|
|
2
|
+
import { writePreparationError } from "./error-output.js";
|
|
3
|
+
import { parseOutputOptions } from "./output-options.js";
|
|
4
|
+
import { optionalOption, optionalPositiveNumber } from "./argument-values.js";
|
|
5
|
+
import { discoverHarnesses, harnessCatalog } from "../services/harness-discovery.js";
|
|
6
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
7
|
+
import { probeHarnessVersion } from "../services/harness-version-probe.js";
|
|
8
|
+
import { probeHarnessDoctor } from "../services/harness-doctor.js";
|
|
9
|
+
import { prepareResponseDestination, writeResponseFile } from "./response-file.js";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { validateModelScope } from "../services/model-discovery.js";
|
|
12
|
+
/** Home-free ingress: process diagnostics require explicit effect admission. */
|
|
13
|
+
export async function runHarnessesCli(args, env, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
14
|
+
let json = false;
|
|
15
|
+
try {
|
|
16
|
+
const output = parseOutputOptions(args);
|
|
17
|
+
json = output.json;
|
|
18
|
+
if (output.responseFile)
|
|
19
|
+
prepareResponseDestination(output.responseFile);
|
|
20
|
+
const route = prepareCommandRoute(output.args);
|
|
21
|
+
const family = route.key === "harnesses show" ? route.parsed.positional[0] : optionalOption(route.parsed, "harness");
|
|
22
|
+
if (family && !harnessCatalog.entries.some(entry => entry.id === family))
|
|
23
|
+
throw new AppError("usage", "Unknown harness; see dd-flow harnesses --help", 2, { effect: "no_effect" });
|
|
24
|
+
const provider = optionalOption(route.parsed, "provider"), account = optionalOption(route.parsed, "account-sha256");
|
|
25
|
+
const observationFile = optionalOption(route.parsed, "model-observation-file");
|
|
26
|
+
let modelScope;
|
|
27
|
+
if (provider !== undefined || account !== undefined || observationFile !== undefined) {
|
|
28
|
+
if (!provider || !account)
|
|
29
|
+
throw new AppError("usage", "Doctor model observations require both --provider and --account-sha256", 2, { effect: "no_effect", help: "harnesses doctor" });
|
|
30
|
+
modelScope = { harness: family, provider, account_sha256: account };
|
|
31
|
+
validateModelScope(modelScope);
|
|
32
|
+
if (observationFile) {
|
|
33
|
+
if (output.responseFile && path.resolve(observationFile) === path.resolve(output.responseFile))
|
|
34
|
+
throw new AppError("usage", "Model observation and command response destinations must differ", 2, { effect: "no_effect" });
|
|
35
|
+
prepareResponseDestination(observationFile);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const discovery = discoverHarnesses({ env, ...(family ? { harness: family } : {}),
|
|
39
|
+
...(optionalOption(route.parsed, "executable") ? { executable: optionalOption(route.parsed, "executable") } : {}) });
|
|
40
|
+
let probe;
|
|
41
|
+
let modelObservation;
|
|
42
|
+
let observationPublication;
|
|
43
|
+
if (route.key === "harnesses doctor") {
|
|
44
|
+
const selected = discovery.entries[0]?.candidates.find(candidate => candidate.selected);
|
|
45
|
+
if (!selected)
|
|
46
|
+
throw new AppError("harness_selection_required", "Doctor requires one installed executable; use --executable for ambiguous installations", 1, { effect: "no_effect" });
|
|
47
|
+
const stateDir = optionalOption(route.parsed, "state-dir"), nativeExecutable = optionalOption(route.parsed, "native-executable");
|
|
48
|
+
const timeoutMs = optionalPositiveNumber(route.parsed, "timeout-ms", true);
|
|
49
|
+
probe = await probeHarnessDoctor({ harness: family, executable: selected.path, env,
|
|
50
|
+
...(stateDir !== undefined ? { stateDir } : {}), ...(nativeExecutable !== undefined ? { nativeExecutable } : {}),
|
|
51
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
52
|
+
scope: { processes: route.parsed.options.has("allow-processes"), services: route.parsed.options.has("allow-services"),
|
|
53
|
+
credentials: route.parsed.options.has("allow-credentials"), writes: route.parsed.options.has("allow-writes") } });
|
|
54
|
+
if (modelScope) {
|
|
55
|
+
modelObservation = { scope: modelScope, observed_at: new Date().toISOString(),
|
|
56
|
+
status: probe.reported_models.length ? "listed" : "unavailable", complete: false, models: [...probe.reported_models] };
|
|
57
|
+
if (observationFile) {
|
|
58
|
+
const published = writeResponseFile(modelObservation, { responseFile: observationFile });
|
|
59
|
+
observationPublication = { path: observationFile, status: published.response_file_error ? "unknown" : "published",
|
|
60
|
+
...(published.response_file_error ? { error: published.response_file_error } : {}) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (route.key === "harnesses probe-version") {
|
|
65
|
+
const selected = discovery.entries[0]?.candidates.find(candidate => candidate.selected);
|
|
66
|
+
if (!selected)
|
|
67
|
+
throw new AppError("harness_selection_required", "Version probe requires one installed executable; use --executable for ambiguous installations", 1, { effect: "no_effect" });
|
|
68
|
+
probe = await probeHarnessVersion({ harness: family, executable: selected.path, env,
|
|
69
|
+
...(optionalPositiveNumber(route.parsed, "timeout-ms", true) === undefined ? {} : { timeoutMs: optionalPositiveNumber(route.parsed, "timeout-ms", true) }),
|
|
70
|
+
scope: { processes: route.parsed.options.has("allow-processes"), services: false,
|
|
71
|
+
credentials: route.parsed.options.has("allow-credentials"), writes: route.parsed.options.has("allow-writes") } });
|
|
72
|
+
}
|
|
73
|
+
const result = { ok: true, action: route.key.replaceAll(" ", "_"), catalog: harnessCatalog, discovery, ...(probe ? { probe } : {}),
|
|
74
|
+
...(modelObservation ? { model_observation: modelObservation, observation_scope_binding: "operator_declared_unverified" } : {}),
|
|
75
|
+
...(observationPublication ? { model_observation_publication: observationPublication } : {}) };
|
|
76
|
+
io.stdout.write(`${JSON.stringify(writeResponseFile(result, output), null, json ? undefined : 2)}\n`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const record = errorRecord(error);
|
|
81
|
+
writePreparationError(io, args, record);
|
|
82
|
+
return record.exitCode;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { prepareCommandRoute } from "./command-inputs.js";
|
|
2
|
+
import { optionalOption } from "./argument-values.js";
|
|
3
|
+
import { parseOutputOptions } from "./output-options.js";
|
|
4
|
+
import { prepareResponseDestination, writeResponseFile } from "./response-file.js";
|
|
5
|
+
import { listOperatorModels } from "../services/model-catalog.js";
|
|
6
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
7
|
+
import { writePreparationError } from "./error-output.js";
|
|
8
|
+
/** Model listing never opens an AppContext, starts a probe or reads credentials. */
|
|
9
|
+
export function runModelsCli(args, io = process) {
|
|
10
|
+
let json = false;
|
|
11
|
+
try {
|
|
12
|
+
const output = parseOutputOptions(args);
|
|
13
|
+
json = output.json;
|
|
14
|
+
if (output.responseFile)
|
|
15
|
+
prepareResponseDestination(output.responseFile);
|
|
16
|
+
const route = prepareCommandRoute(output.args);
|
|
17
|
+
if (!["models list", "models show"].includes(route.key))
|
|
18
|
+
throw new AppError("usage", "Unknown models command; see dd-flow models --help", 2, { effect: "no_effect" });
|
|
19
|
+
const input = {};
|
|
20
|
+
for (const [option, field] of [["harness", "harness"], ["provider", "provider"], ["account-sha256", "accountSha256"],
|
|
21
|
+
["seed-file", "seedFile"], ["observation-file", "observationFile"]]) {
|
|
22
|
+
const value = optionalOption(route.parsed, option);
|
|
23
|
+
if (value !== undefined)
|
|
24
|
+
input[field] = value;
|
|
25
|
+
}
|
|
26
|
+
if (route.key === "models show")
|
|
27
|
+
input.model = route.parsed.positional[0];
|
|
28
|
+
const models = listOperatorModels(input);
|
|
29
|
+
if (route.key === "models show" && models.entries.length === 0)
|
|
30
|
+
throw new AppError("model_not_found", "Model is absent from the selected seed and scoped observation", 1, { effect: "no_effect", help: "models" });
|
|
31
|
+
io.stdout.write(`${JSON.stringify(writeResponseFile({ ok: true, action: route.key.replaceAll(" ", "_"), ...models }, output), null, json ? undefined : 2)}\n`);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const record = errorRecord(error);
|
|
36
|
+
writePreparationError(io, args, record);
|
|
37
|
+
return record.exitCode;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { requiredOption, optionalOption } from "./argument-values.js";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { errorRecord } from "../shared/errors.js";
|
|
4
|
+
import { prepareCommandRoute } from "./command-inputs.js";
|
|
5
|
+
import { parseOutputOptions } from "./output-options.js";
|
|
6
|
+
import { writePreparationError } from "./error-output.js";
|
|
7
|
+
import { resolveDdFlowHome } from "../storage/paths.js";
|
|
8
|
+
import { prepareResponseDestination, writeResponseFile } from "./response-file.js";
|
|
9
|
+
import { readOperatorProfiles, resolveOperatorProfile, mutateOperatorProfiles, readOperatorProfileInput } from "../services/operator-profiles.js";
|
|
10
|
+
/** Home-only entrypoint; does not create AppContext, project registration or runtime DB. */
|
|
11
|
+
export function dispatchOperatorProfiles(home, route) {
|
|
12
|
+
const action = route.command, parsed = route.parsed;
|
|
13
|
+
if (action === "list")
|
|
14
|
+
return { ok: true, action: "profiles_list", settings: readOperatorProfiles(home) };
|
|
15
|
+
if (action === "show") {
|
|
16
|
+
const settings = readOperatorProfiles(home), id = parsed.positional[0];
|
|
17
|
+
return { ok: true, action: "profiles_show", selected: id, revision: settings.revision, profile: resolveOperatorProfile(settings, id) };
|
|
18
|
+
}
|
|
19
|
+
let mutation;
|
|
20
|
+
if (action === "create")
|
|
21
|
+
mutation = { action, profile: readOperatorProfileInput(requiredOption(parsed, "file")), setDefault: parsed.options.has("set-default") };
|
|
22
|
+
else if (action === "edit")
|
|
23
|
+
mutation = { action, profile: readOperatorProfileInput(requiredOption(parsed, "file")) };
|
|
24
|
+
else if (action === "remove")
|
|
25
|
+
mutation = { action, id: parsed.positional[0] };
|
|
26
|
+
else if (action === "alias") {
|
|
27
|
+
const target = optionalOption(parsed, "target"), remove = parsed.options.has("remove");
|
|
28
|
+
if (Boolean(target) === remove)
|
|
29
|
+
throw new AppError("usage", "Use exactly one of --target ID or --remove; see dd-flow profiles alias --help", 2, { effect: "no_effect" });
|
|
30
|
+
mutation = { action, alias: parsed.positional[0], target: target ?? null };
|
|
31
|
+
}
|
|
32
|
+
else if (action === "set-default") {
|
|
33
|
+
const id = parsed.positional[0], clear = parsed.options.has("clear");
|
|
34
|
+
if (Boolean(id) === clear)
|
|
35
|
+
throw new AppError("usage", "Select one profile ID or --clear; see dd-flow profiles set-default --help", 2, { effect: "no_effect" });
|
|
36
|
+
mutation = { action, id: id ?? null };
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
throw new AppError("usage", "Unknown profiles command; use dd-flow profiles --help", 2, { effect: "no_effect" });
|
|
40
|
+
const settings = mutateOperatorProfiles(home, requiredOption(parsed, "expected-revision"), mutation);
|
|
41
|
+
return { ok: true, action: `profiles_${action}`, effect: "committed", settings };
|
|
42
|
+
}
|
|
43
|
+
/** Lightweight executable ingress; never imports the project/runtime business router. */
|
|
44
|
+
export function runOperatorProfilesCli(args, env, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
45
|
+
let json = false;
|
|
46
|
+
try {
|
|
47
|
+
const output = parseOutputOptions(args);
|
|
48
|
+
json = output.json;
|
|
49
|
+
if (output.responseFile)
|
|
50
|
+
prepareResponseDestination(output.responseFile);
|
|
51
|
+
const route = prepareCommandRoute(output.args);
|
|
52
|
+
const result = dispatchOperatorProfiles(resolveDdFlowHome(env), route);
|
|
53
|
+
io.stdout.write(`${JSON.stringify(writeResponseFile(result, output), null, json ? undefined : 2)}\n`);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const record = errorRecord(error);
|
|
58
|
+
writePreparationError(io, args, record);
|
|
59
|
+
return record.exitCode;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
/** Shared execution transport grammar. Help resolves before this function. */
|
|
3
|
+
export function parseOutputOptions(args) {
|
|
4
|
+
const stripped = [];
|
|
5
|
+
let json = false, progressJsonl = false, responseFile = null;
|
|
6
|
+
for (let index = 0; index < args.length; index++) {
|
|
7
|
+
const arg = args[index];
|
|
8
|
+
if (arg === "--") {
|
|
9
|
+
stripped.push(...args.slice(index));
|
|
10
|
+
break;
|
|
11
|
+
}
|
|
12
|
+
if (arg === "--json") {
|
|
13
|
+
json = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (arg === "--progress-jsonl") {
|
|
17
|
+
progressJsonl = true;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (arg === "--response-file" || arg.startsWith("--response-file=")) {
|
|
21
|
+
if (responseFile !== null)
|
|
22
|
+
throw new AppError("usage", "--response-file may be supplied only once", 2);
|
|
23
|
+
const file = arg === "--response-file" ? args[++index] : arg.slice("--response-file=".length);
|
|
24
|
+
if (!file?.trim() || file.startsWith("--"))
|
|
25
|
+
throw new AppError("usage", "--response-file requires a path", 2);
|
|
26
|
+
responseFile = file;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
stripped.push(arg);
|
|
30
|
+
}
|
|
31
|
+
if (responseFile && !json)
|
|
32
|
+
throw new AppError("usage", "--response-file requires --json", 2);
|
|
33
|
+
const hookJsonOnly = stripped[0] === "codex" && stripped[1] === "hook" && stripped[2] === "handle";
|
|
34
|
+
return { json: json || hookJsonOnly, progressJsonl, args: stripped, hookJsonOnly, responseFile };
|
|
35
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
/** Preflight does not create a parent, destination or runtime home. */
|
|
6
|
+
export function prepareResponseDestination(value) {
|
|
7
|
+
const file = path.resolve(value);
|
|
8
|
+
try {
|
|
9
|
+
const inspect = (target) => {
|
|
10
|
+
try {
|
|
11
|
+
return fs.lstatSync(target);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code === "ENOENT")
|
|
15
|
+
return null;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
let parent = path.dirname(file);
|
|
20
|
+
let parentStat = inspect(parent);
|
|
21
|
+
while (!parentStat) {
|
|
22
|
+
const next = path.dirname(parent);
|
|
23
|
+
if (next === parent)
|
|
24
|
+
break;
|
|
25
|
+
parent = next;
|
|
26
|
+
parentStat = inspect(parent);
|
|
27
|
+
}
|
|
28
|
+
const targetStat = inspect(file);
|
|
29
|
+
if (!parentStat?.isDirectory() || targetStat && !targetStat.isFile())
|
|
30
|
+
throw new Error();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new AppError("response_file_invalid", "Response destination must be a regular file under a directory", 2, { parameter: "response-file", effect: "no_effect" });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Optional publication failure never changes the accepted operation outcome. */
|
|
37
|
+
export function writeResponseFile(payload, output) {
|
|
38
|
+
if (!output.responseFile)
|
|
39
|
+
return payload;
|
|
40
|
+
const file = path.resolve(output.responseFile), temporary = `${file}.tmp-${randomUUID()}`;
|
|
41
|
+
const visible = payload && typeof payload === "object" && !Array.isArray(payload) ? { ...payload } : { ok: true, result: payload };
|
|
42
|
+
let ownsTemporary = false;
|
|
43
|
+
try {
|
|
44
|
+
prepareResponseDestination(file);
|
|
45
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
46
|
+
const fd = fs.openSync(temporary, "wx", 0o600);
|
|
47
|
+
ownsTemporary = true;
|
|
48
|
+
try {
|
|
49
|
+
fs.writeFileSync(fd, `${JSON.stringify(payload, null, 2)}\n`);
|
|
50
|
+
fs.fsyncSync(fd);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
fs.closeSync(fd);
|
|
54
|
+
}
|
|
55
|
+
fs.renameSync(temporary, file);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { ...visible, response_file: file, response_file_error: "Response publication failed; reconcile the accepted operation before retrying" };
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
try {
|
|
62
|
+
if (ownsTemporary && fs.existsSync(temporary))
|
|
63
|
+
fs.unlinkSync(temporary);
|
|
64
|
+
}
|
|
65
|
+
catch { /* Own temporary only; never mask the operation outcome. */ }
|
|
66
|
+
}
|
|
67
|
+
delete visible.worker_prompt_markdown;
|
|
68
|
+
return { ...visible, response_file: file };
|
|
69
|
+
}
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -70,6 +70,7 @@ import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
|
70
70
|
import { prepareCommandRoute } from "./command-inputs.js";
|
|
71
71
|
import { parseLifecycleCommand } from "../services/lifecycle-command.js";
|
|
72
72
|
import { dispatchRuntimeProcess, prepareRuntimeProcess } from "./runtime-process.js";
|
|
73
|
+
import { reconcileFlowRunHealth } from "../services/run-health.js";
|
|
73
74
|
import { fenceRuntimeBudget, runtimeBudgetStatus, prepareRuntimeScopeControl, prepareRuntimeScopeFence } from "../services/runtime-budget.js";
|
|
74
75
|
import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
|
|
75
76
|
import { launchRuntimeScopeControl, launchRuntimeScopeResume, prepareRuntimeScopeReconciliation, prepareRuntimeScopeServing, reconcileRuntimeScopeControl, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
|
|
@@ -505,6 +506,8 @@ function resolveProjectRootBeforeRouting(args, env, deadlineAt) {
|
|
|
505
506
|
function databaseModeForCommand(args, classification) {
|
|
506
507
|
if (isNativeHookIngress(args))
|
|
507
508
|
return "hook";
|
|
509
|
+
if (args[0] === "run" && args[1] === "reconcile")
|
|
510
|
+
return args.includes("--apply") ? "reconcile" : "read_existing";
|
|
508
511
|
if (isRuntimeMaintenance(args))
|
|
509
512
|
return "read_existing";
|
|
510
513
|
const [family, command] = args;
|
|
@@ -2474,6 +2477,8 @@ function recoveryResumeInput(parsed) {
|
|
|
2474
2477
|
return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), recoveryId: requiredOption(parsed, "recovery-id"), daemonId: requiredOption(parsed, "daemon-id"), harness: requiredOption(parsed, "harness"), nativeSessionId: requiredOption(parsed, "native-session-id") };
|
|
2475
2478
|
}
|
|
2476
2479
|
function dispatchRun(context, command, parsed, prepared = {}) {
|
|
2480
|
+
if (command === "reconcile")
|
|
2481
|
+
return reconcileFlowRunHealth(context, { runId: parsed.positional[0], projectRoot: optionalOption(parsed, "project-root"), apply: hasOption(parsed, "apply"), noProgressMs: optionalNumber(parsed, "no-progress-ms") });
|
|
2477
2482
|
if (command === "recovery") {
|
|
2478
2483
|
const action = requiredPosition(parsed, 0, "recovery action");
|
|
2479
2484
|
const projectRoot = requiredOption(parsed, "project-root");
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { prepareCommandRoute } from "./command-inputs.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseOutputOptions } from "./output-options.js";
|
|
4
|
+
import { resolveDdFlowHome } from "../storage/paths.js";
|
|
5
|
+
import { readSetupStatus, installAuthenticatedRelease, readRetainedSetupExecutor, hasDeferredSetupOnboarding, readSetupOnboarding, commitSetupOnboarding } from "../services/setup.js";
|
|
6
|
+
import { readTrustedReleaseProducers, readReleaseBundle } from "../services/release-source.js";
|
|
7
|
+
import { createReleaseCompatibility, installedCatalogVersions } from "../services/release-compatibility.js";
|
|
8
|
+
import { getCliVersionReport } from "../services/build-info.js";
|
|
9
|
+
import { readOperatorProfiles, readProfileCreationIntent } from "../services/operator-profiles.js";
|
|
10
|
+
import { onboardExecutor, selectOnboardingExecutor } from "../services/executor-onboarding.js";
|
|
11
|
+
import { askExecutorChoice } from "./executor-onboarding.js";
|
|
12
|
+
import { requiredOption, optionalOption } from "./argument-values.js";
|
|
13
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
14
|
+
import { hybridDigest } from "../services/hybrid-contracts.js";
|
|
15
|
+
import { prepareResponseDestination, writeResponseFile } from "./response-file.js";
|
|
16
|
+
import { readRegistryRelease } from "../services/release-registry.js";
|
|
17
|
+
import { writePreparationError } from "./error-output.js";
|
|
18
|
+
/** Lightweight global diagnostic ingress; never creates a runtime context. */
|
|
19
|
+
export async function runSetupCli(args, env, io = { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin }) {
|
|
20
|
+
let json = false;
|
|
21
|
+
try {
|
|
22
|
+
const output = parseOutputOptions(args);
|
|
23
|
+
json = output.json;
|
|
24
|
+
if (output.responseFile)
|
|
25
|
+
prepareResponseDestination(output.responseFile);
|
|
26
|
+
const route = prepareCommandRoute(output.args);
|
|
27
|
+
const humanProgress = !output.json && !output.progressJsonl;
|
|
28
|
+
const reportHumanProgress = (message) => {
|
|
29
|
+
// Diagnostic delivery cannot turn a completed installation into failure.
|
|
30
|
+
try {
|
|
31
|
+
io.stderr.write(`setup: ${message}\n`);
|
|
32
|
+
}
|
|
33
|
+
catch { /* Best-effort human progress only. */ }
|
|
34
|
+
};
|
|
35
|
+
const home = resolveDdFlowHome(env);
|
|
36
|
+
let result;
|
|
37
|
+
if (route.key === "setup status")
|
|
38
|
+
result = { ok: true, action: "setup_status", observed_cli: getCliVersionReport().cli, ...readSetupStatus(home) };
|
|
39
|
+
else if (route.key === "setup" || route.key === "setup update") {
|
|
40
|
+
const parsed = route.parsed;
|
|
41
|
+
const source = optionalOption(parsed, "canon-source"), version = optionalOption(parsed, "canon-version");
|
|
42
|
+
const registryFile = optionalOption(parsed, "release-registry");
|
|
43
|
+
if (source && (version || registryFile))
|
|
44
|
+
throw new AppError("usage", "--canon-source is exclusive with --canon-version and --release-registry; see dd-flow setup --help", 2, { effect: "no_effect" });
|
|
45
|
+
const channel = (optionalOption(parsed, "channel") ?? "stable");
|
|
46
|
+
const selection = source ? null : readRegistryRelease(registryFile ?? env.DD_FLOW_RELEASE_REGISTRY ?? path.join(home, "release-registry.json"), channel, version);
|
|
47
|
+
const profile = optionalOption(parsed, "profile"), skip = parsed.options.has("skip-agent-setup");
|
|
48
|
+
const intentFile = optionalOption(parsed, "save-profile-intent");
|
|
49
|
+
if (intentFile && skip)
|
|
50
|
+
throw new AppError("usage", "--save-profile-intent conflicts with --skip-agent-setup", 2, { effect: "no_effect" });
|
|
51
|
+
let profileCreation = intentFile ? readProfileCreationIntent(intentFile) : undefined;
|
|
52
|
+
const stdin = io.stdin ?? process.stdin;
|
|
53
|
+
const nonInteractive = parsed.options.has("non-interactive") || output.json || output.progressJsonl
|
|
54
|
+
|| !stdin.isTTY || !io.stderr.isTTY;
|
|
55
|
+
const profileFile = optionalOption(parsed, "profile-file");
|
|
56
|
+
const requestId = requiredOption(parsed, "request-id");
|
|
57
|
+
const retained = profile === undefined && profileFile === undefined && !intentFile && !skip ? readRetainedSetupExecutor(home, requestId) : null;
|
|
58
|
+
if (retained?.profileCreation)
|
|
59
|
+
profileCreation = retained.profileCreation;
|
|
60
|
+
const choice = { ...(profile !== undefined ? { profileId: profile } : {}),
|
|
61
|
+
...(profileFile !== undefined ? { profileFile } : {}),
|
|
62
|
+
...(retained?.executor ? { profile: retained.executor } : profileCreation && profile === undefined && profileFile === undefined ? { profile: profileCreation.profile } : {}),
|
|
63
|
+
skip: skip || retained !== null && retained.executor === null };
|
|
64
|
+
let deferred = hasDeferredSetupOnboarding(home, requestId);
|
|
65
|
+
let selected;
|
|
66
|
+
if (!deferred) {
|
|
67
|
+
try {
|
|
68
|
+
selected = selectOnboardingExecutor(choice, () => readOperatorProfiles(home), { allowSkip: true });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!(error instanceof AppError) || error.code !== "agent_profile_missing" || nonInteractive
|
|
72
|
+
|| profile !== undefined || profileFile !== undefined || intentFile || skip)
|
|
73
|
+
throw error;
|
|
74
|
+
deferred = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (profile !== undefined || profileFile !== undefined || intentFile || skip) {
|
|
78
|
+
// Explicit continuation inputs still fail before downloading or replaying.
|
|
79
|
+
selected = selectOnboardingExecutor(choice, () => readOperatorProfiles(home), { allowSkip: true });
|
|
80
|
+
}
|
|
81
|
+
const observedCli = getCliVersionReport().cli;
|
|
82
|
+
const compatibility = createReleaseCompatibility({ channel,
|
|
83
|
+
catalogVersions: installedCatalogVersions, routerVersion: observedCli.version });
|
|
84
|
+
const producers = readTrustedReleaseProducers(requiredOption(parsed, "trust-file"));
|
|
85
|
+
const bundle = await readReleaseBundle(source ?? selection.envelope_url, producers, compatibility, {
|
|
86
|
+
...(selection ? { expectedVersion: selection.version } : {}),
|
|
87
|
+
...((output.progressJsonl || humanProgress) ? { onProgress: (progress) => {
|
|
88
|
+
if (output.progressJsonl)
|
|
89
|
+
io.stdout.write(`${JSON.stringify({ type: "setup_download_progress", request_id: requestId, ...progress })}\n`);
|
|
90
|
+
else if (progress.received_bytes === progress.expected_bytes)
|
|
91
|
+
reportHumanProgress(`${progress.artifact} archive received (${progress.received_bytes} bytes); integrity validation follows`);
|
|
92
|
+
} } : {})
|
|
93
|
+
});
|
|
94
|
+
if (selection && bundle.authenticated.manifest.version !== selection.version)
|
|
95
|
+
throw new AppError("release_registry", "Authenticated release differs from selected registry version", 1, { reason: "selected_version_mismatch", phase: "release_selection", effect: "no_effect", help: "setup" });
|
|
96
|
+
const installed = installAuthenticatedRelease(home, { requestId, action: route.key === "setup update" ? "update" : "install",
|
|
97
|
+
envelope: bundle.envelope, trustedProducers: producers, compatibility, canonArchive: bundle.canonArchive, engineArchive: bundle.engineArchive,
|
|
98
|
+
...(deferred ? { deferredOnboarding: true } : { executor: selected.profile, ...(profileCreation ? { profileCreation } : {}) }),
|
|
99
|
+
...((output.progressJsonl || humanProgress) ? { onCheckpoint: phase => {
|
|
100
|
+
if (output.progressJsonl)
|
|
101
|
+
io.stdout.write(`${JSON.stringify({ type: "setup_progress", request_id: requestId, phase })}\n`);
|
|
102
|
+
else
|
|
103
|
+
reportHumanProgress({ prepared: "verified release request retained", canon_cached: "canon cache verified", engine_cached: "engine cache verified",
|
|
104
|
+
profiles_saved: "profile settings saved", committed: "installation committed; runtime qualification remains unknown" }[phase]);
|
|
105
|
+
} } : {}) });
|
|
106
|
+
if (humanProgress && installed.replayed)
|
|
107
|
+
reportHumanProgress("exact request replay verified; retained selection was not reactivated");
|
|
108
|
+
if (deferred) {
|
|
109
|
+
const saved = readSetupOnboarding(home, requestId);
|
|
110
|
+
const explicit = profile !== undefined || profileFile !== undefined || intentFile !== undefined || skip;
|
|
111
|
+
const deferredChoice = saved && !explicit ? { ...(saved.executor ? { profile: saved.executor } : { skip: true }),
|
|
112
|
+
...(saved.profileCreation ? { profileCreation: saved.profileCreation } : {}) } : choice;
|
|
113
|
+
try {
|
|
114
|
+
selected ??= await onboardExecutor(deferredChoice, () => readOperatorProfiles(home), { allowSkip: true, nonInteractive }, settings => askExecutorChoice(settings, { stdin, stderr: io.stderr }));
|
|
115
|
+
const consent = commitSetupOnboarding(home, requestId, { executor: selected.profile,
|
|
116
|
+
...(profileCreation ?? selected.profileCreation ? { profileCreation: profileCreation ?? selected.profileCreation } : {}) });
|
|
117
|
+
installed.executor = consent.executor;
|
|
118
|
+
if (humanProgress)
|
|
119
|
+
reportHumanProgress("executor onboarding committed after installation");
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error instanceof AppError) {
|
|
123
|
+
error.details.installation = "committed";
|
|
124
|
+
error.details.request_id = requestId;
|
|
125
|
+
error.details.next_action = "Replay the same setup request to resume executor onboarding; installation remains retained";
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
result = { ...installed, action: route.key === "setup update" ? "setup_update" : "setup_install", observed_cli: observedCli,
|
|
131
|
+
executor: installed.executor ? { profile_id: installed.executor.id, revision: hybridDigest(installed.executor), readiness: "unknown_requires_qualification" } : { profile_id: null, readiness: "not_configured" },
|
|
132
|
+
...readSetupStatus(home) };
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
throw new AppError("usage", "Unknown setup command; see dd-flow setup --help", 2, { effect: "no_effect" });
|
|
136
|
+
io.stdout.write(`${JSON.stringify(writeResponseFile(result, output), null, json ? undefined : 2)}\n`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const record = errorRecord(error);
|
|
141
|
+
writePreparationError(io, args, record);
|
|
142
|
+
if (!json && error instanceof AppError && error.details.installation === "committed") {
|
|
143
|
+
io.stderr.write("setup: installation remains committed; replay the same request to resume executor onboarding\n");
|
|
144
|
+
}
|
|
145
|
+
return record.exitCode;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -6,6 +6,8 @@ export function classifyCliOperation(args, env = process.env) {
|
|
|
6
6
|
if (isMbUpgradeMode(args, env)) {
|
|
7
7
|
return { mode: "mb_upgrade", operation, family, command, action };
|
|
8
8
|
}
|
|
9
|
+
if (family === "run" && command === "reconcile" && !args.includes("--apply"))
|
|
10
|
+
return { mode: "read_only_diagnostics", operation, family, command, action };
|
|
9
11
|
if (isRouterNative(args)) {
|
|
10
12
|
return { mode: "router_native", operation, family, command, action };
|
|
11
13
|
}
|
|
@@ -85,6 +87,8 @@ function isRouterNative(args) {
|
|
|
85
87
|
return true;
|
|
86
88
|
if (["engine", "version", "schema"].includes(first))
|
|
87
89
|
return true;
|
|
90
|
+
if (first === "run" && args[1] === "reconcile")
|
|
91
|
+
return true;
|
|
88
92
|
if (first === "storage" && args[1] === "migrate-writer")
|
|
89
93
|
return true;
|
|
90
94
|
return (first === "codex" && args[1] === "hook" && args[2] === "handle")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseCommandArgs } from "./lifecycle-command.js";
|
|
2
|
+
/** Syntax eligibility only. Storage/project/RUN authority is checked separately. */
|
|
3
|
+
export function isUpgradeAllowlisted(args) {
|
|
4
|
+
const parsed = parseCommandArgs(args), [family, command] = parsed.positional;
|
|
5
|
+
const option = (name) => {
|
|
6
|
+
const value = parsed.options.get(name)?.at(-1);
|
|
7
|
+
return value && value !== "__dd_flow_flag__" ? value : null;
|
|
8
|
+
};
|
|
9
|
+
if (family === "status")
|
|
10
|
+
return true;
|
|
11
|
+
if (family === "engine" && command === "resolve")
|
|
12
|
+
return true;
|
|
13
|
+
if (family === "migration" && ["impact", "plan", "report", "verify", "apply"].includes(command ?? ""))
|
|
14
|
+
return true;
|
|
15
|
+
if (family === "memory" && command === "permissions" && parsed.positional[2] === "preflight")
|
|
16
|
+
return true;
|
|
17
|
+
if (family === "run" && command === "start")
|
|
18
|
+
return option("flow-kind") === "mb-upgrade";
|
|
19
|
+
const bound = Boolean(option("run") || parsed.positional.some(value => value.startsWith("RUN-")));
|
|
20
|
+
if (family === "stage" && ["start", "pause", "resume", "finish"].includes(command ?? ""))
|
|
21
|
+
return bound;
|
|
22
|
+
if (family === "session" && ["status", "register", "stop", "usage"].includes(command ?? ""))
|
|
23
|
+
return bound;
|
|
24
|
+
if (family === "dashboard" && ["data", "render", "refresh", "open"].includes(command ?? ""))
|
|
25
|
+
return true;
|
|
26
|
+
return false;
|
|
27
|
+
}
|