@nyxa/automation 0.1.0
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/README.md +559 -0
- package/dist/cancellation.d.ts +1 -0
- package/dist/cancellation.js +5 -0
- package/dist/claude-code.d.ts +19 -0
- package/dist/claude-code.js +182 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +487 -0
- package/dist/codex-output.d.ts +6 -0
- package/dist/codex-output.js +167 -0
- package/dist/codex.d.ts +17 -0
- package/dist/codex.js +228 -0
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.js +138 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +51 -0
- package/dist/events.d.ts +55 -0
- package/dist/events.js +51 -0
- package/dist/filesystem.d.ts +2 -0
- package/dist/filesystem.js +13 -0
- package/dist/harness-process.d.ts +39 -0
- package/dist/harness-process.js +359 -0
- package/dist/harness-prompt.d.ts +2 -0
- package/dist/harness-prompt.js +14 -0
- package/dist/harness.d.ts +44 -0
- package/dist/harness.js +68 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +305 -0
- package/dist/json.d.ts +4 -0
- package/dist/json.js +1 -0
- package/dist/kimi-code.d.ts +16 -0
- package/dist/kimi-code.js +299 -0
- package/dist/opencode.d.ts +15 -0
- package/dist/opencode.js +164 -0
- package/dist/run-coordination.d.ts +10 -0
- package/dist/run-coordination.js +96 -0
- package/dist/run-journal.d.ts +14 -0
- package/dist/run-journal.js +129 -0
- package/dist/schema.d.ts +167 -0
- package/dist/schema.js +516 -0
- package/dist/standard-input.d.ts +1 -0
- package/dist/standard-input.js +7 -0
- package/dist/type-utils.d.ts +1 -0
- package/dist/type-utils.js +1 -0
- package/dist/workflow.d.ts +160 -0
- package/dist/workflow.js +530 -0
- package/package.json +51 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { AutomationError } from "./errors.js";
|
|
2
|
+
import { createHarness, harnessProcessControls, } from "./harness.js";
|
|
3
|
+
import { createHarnessPreflight, harnessProcessExitError, harnessProcessFailureCause, runHarnessProcess, } from "./harness-process.js";
|
|
4
|
+
export const CLAUDE_CODE_HARNESS_PREFLIGHT = {
|
|
5
|
+
harnessName: "Claude Code",
|
|
6
|
+
minimumVersion: [2, 1, 205],
|
|
7
|
+
unsupportedVersionMessage: "Claude Code CLI 2.1.205 or later is required.",
|
|
8
|
+
versionPattern: /\b(\d+)\.(\d+)\.(\d+)(?:-([0-9a-z.-]+))?(?:\+[0-9a-z.-]+)?(?:\s|$)/iu,
|
|
9
|
+
};
|
|
10
|
+
const preflightClaudeCode = createHarnessPreflight(CLAUDE_CODE_HARNESS_PREFLIGHT);
|
|
11
|
+
export function claudeCode(options = {}) {
|
|
12
|
+
const configuredOptions = Object.freeze({ ...options });
|
|
13
|
+
return createHarness({
|
|
14
|
+
name: "Claude Code",
|
|
15
|
+
async run(request) {
|
|
16
|
+
const executable = await preflightClaudeCode(configuredOptions.executable ?? "claude", request.environment, harnessProcessControls(request));
|
|
17
|
+
return executeClaudeCode(executable, configuredOptions, request);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async function executeClaudeCode(executable, options, request) {
|
|
22
|
+
const arguments_ = [
|
|
23
|
+
"-p",
|
|
24
|
+
"--output-format",
|
|
25
|
+
"json",
|
|
26
|
+
"--disallowed-tools",
|
|
27
|
+
"AskUserQuestion",
|
|
28
|
+
"--permission-mode",
|
|
29
|
+
claudeCodePermissionMode(request.access, request.approval),
|
|
30
|
+
];
|
|
31
|
+
arguments_.push("--settings", JSON.stringify(claudeCodeRunSettings(request.access)));
|
|
32
|
+
if (options.model !== undefined) {
|
|
33
|
+
arguments_.push("--model", options.model);
|
|
34
|
+
}
|
|
35
|
+
if (options.effort !== undefined) {
|
|
36
|
+
arguments_.push("--effort", options.effort);
|
|
37
|
+
}
|
|
38
|
+
if (options.fallbackModel !== undefined) {
|
|
39
|
+
arguments_.push("--fallback-model", options.fallbackModel);
|
|
40
|
+
}
|
|
41
|
+
if (options.maxTurns !== undefined) {
|
|
42
|
+
arguments_.push("--max-turns", String(options.maxTurns));
|
|
43
|
+
}
|
|
44
|
+
if (options.maxBudgetUsd !== undefined) {
|
|
45
|
+
arguments_.push("--max-budget-usd", String(options.maxBudgetUsd));
|
|
46
|
+
}
|
|
47
|
+
if (request.output !== undefined) {
|
|
48
|
+
arguments_.push("--json-schema", JSON.stringify(request.output.toJSONSchema()));
|
|
49
|
+
}
|
|
50
|
+
if (request.nativeSessionId !== undefined) {
|
|
51
|
+
arguments_.push("--resume", request.nativeSessionId);
|
|
52
|
+
}
|
|
53
|
+
arguments_.push(request.prompt);
|
|
54
|
+
await request.start();
|
|
55
|
+
let envelope;
|
|
56
|
+
const result = await runHarnessProcess(executable, arguments_, request.workingDirectory, "Claude Code", {
|
|
57
|
+
environment: request.environment,
|
|
58
|
+
...harnessProcessControls(request),
|
|
59
|
+
async onStdoutLine(line) {
|
|
60
|
+
if (line.trim().length === 0) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
envelope = JSON.parse(line);
|
|
65
|
+
}
|
|
66
|
+
catch (cause) {
|
|
67
|
+
throw new AutomationError("HARNESS_PROTOCOL_ERROR", "Claude Code emitted malformed JSON.", { cause });
|
|
68
|
+
}
|
|
69
|
+
await request.emitNativeEvent(envelope);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
if (result.exitCode !== 0) {
|
|
73
|
+
if (request.approval === "auto" &&
|
|
74
|
+
automaticApprovalUnavailable(result.stderr)) {
|
|
75
|
+
throw new AutomationError("HARNESS_CAPABILITY_UNSUPPORTED", "Claude Code automatic approval is not available.", { cause: harnessProcessFailureCause(result.stderr) });
|
|
76
|
+
}
|
|
77
|
+
const structuredOutputFailure = request.output === undefined
|
|
78
|
+
? undefined
|
|
79
|
+
: parseStructuredOutputFailure(result.stdout);
|
|
80
|
+
if (structuredOutputFailure !== undefined) {
|
|
81
|
+
throw new AutomationError("RUN_OUTPUT_INVALID", "The Claude Code Run could not produce valid structured output.", { cause: structuredOutputFailure });
|
|
82
|
+
}
|
|
83
|
+
throw harnessProcessExitError("Claude Code", result);
|
|
84
|
+
}
|
|
85
|
+
const structuredOutputFailure = request.output === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: structuredOutputFailureCause(envelope);
|
|
88
|
+
if (structuredOutputFailure !== undefined) {
|
|
89
|
+
throw new AutomationError("RUN_OUTPUT_INVALID", "The Claude Code Run could not produce valid structured output.", { cause: structuredOutputFailure });
|
|
90
|
+
}
|
|
91
|
+
if (!isSuccessResult(envelope)) {
|
|
92
|
+
throw new AutomationError("HARNESS_PROTOCOL_ERROR", "Claude Code completed without a successful result.");
|
|
93
|
+
}
|
|
94
|
+
const nativeSessionId = envelope.session_id;
|
|
95
|
+
if (request.nativeSessionId !== undefined &&
|
|
96
|
+
nativeSessionId !== request.nativeSessionId) {
|
|
97
|
+
throw new AutomationError("HARNESS_PROTOCOL_ERROR", "Claude Code resumed a different Session than requested.");
|
|
98
|
+
}
|
|
99
|
+
if (request.output !== undefined) {
|
|
100
|
+
if (!Object.hasOwn(envelope, "structured_output")) {
|
|
101
|
+
request.sessionReady?.(nativeSessionId);
|
|
102
|
+
throw new AutomationError("RUN_OUTPUT_INVALID", "The Claude Code Run completed without structured output.", { cause: new TypeError("Missing Claude Code structured output.") });
|
|
103
|
+
}
|
|
104
|
+
return { nativeSessionId, value: envelope.structured_output };
|
|
105
|
+
}
|
|
106
|
+
if (!isTextResult(envelope)) {
|
|
107
|
+
throw new AutomationError("HARNESS_PROTOCOL_ERROR", "Claude Code completed without a textual result.");
|
|
108
|
+
}
|
|
109
|
+
return { nativeSessionId, value: envelope.result };
|
|
110
|
+
}
|
|
111
|
+
function automaticApprovalUnavailable(stderr) {
|
|
112
|
+
return /\bauto(?:matic)? mode\b.*\b(?:not available|unavailable|unsupported|requires)\b/iu.test(stderr);
|
|
113
|
+
}
|
|
114
|
+
function claudeCodeRunSettings(access) {
|
|
115
|
+
return {
|
|
116
|
+
...(access === "read"
|
|
117
|
+
? { permissions: { deny: ["Bash", "Edit"] } }
|
|
118
|
+
: {}),
|
|
119
|
+
...(access === "write-workspace"
|
|
120
|
+
? {
|
|
121
|
+
sandbox: {
|
|
122
|
+
allowUnsandboxedCommands: false,
|
|
123
|
+
enabled: true,
|
|
124
|
+
failIfUnavailable: true,
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
: {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function claudeCodePermissionMode(access, approval) {
|
|
131
|
+
if (approval === "auto") {
|
|
132
|
+
return "auto";
|
|
133
|
+
}
|
|
134
|
+
switch (access) {
|
|
135
|
+
case "read":
|
|
136
|
+
return "plan";
|
|
137
|
+
case "write-workspace":
|
|
138
|
+
return "acceptEdits";
|
|
139
|
+
case "full":
|
|
140
|
+
return "bypassPermissions";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function parseStructuredOutputFailure(stdout) {
|
|
144
|
+
try {
|
|
145
|
+
return structuredOutputFailureCause(JSON.parse(stdout));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function structuredOutputFailureCause(value) {
|
|
152
|
+
if (typeof value !== "object" ||
|
|
153
|
+
value === null ||
|
|
154
|
+
value.type !== "result" ||
|
|
155
|
+
value.subtype !==
|
|
156
|
+
"error_max_structured_output_retries") {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
const errors = value.errors;
|
|
160
|
+
const diagnostic = Array.isArray(errors)
|
|
161
|
+
? errors
|
|
162
|
+
.filter((entry) => typeof entry === "string")
|
|
163
|
+
.join("\n")
|
|
164
|
+
: "";
|
|
165
|
+
return new Error(diagnostic.length === 0
|
|
166
|
+
? "Claude Code exhausted its structured output attempts."
|
|
167
|
+
: diagnostic);
|
|
168
|
+
}
|
|
169
|
+
function isTextResult(value) {
|
|
170
|
+
return (isSuccessResult(value) &&
|
|
171
|
+
typeof value.result === "string");
|
|
172
|
+
}
|
|
173
|
+
function isSuccessResult(value) {
|
|
174
|
+
return (typeof value === "object" &&
|
|
175
|
+
value !== null &&
|
|
176
|
+
value.type === "result" &&
|
|
177
|
+
value.subtype === "success" &&
|
|
178
|
+
value.is_error === false &&
|
|
179
|
+
typeof value.session_id ===
|
|
180
|
+
"string" &&
|
|
181
|
+
value.session_id.length > 0);
|
|
182
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, realpathSync, statSync, writeSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { tsImport } from "tsx/esm/api";
|
|
7
|
+
import { AutomationError, isAutomationError } from "./errors.js";
|
|
8
|
+
import { isMissingFileError } from "./filesystem.js";
|
|
9
|
+
import { diagnoseEnvironment, } from "./doctor.js";
|
|
10
|
+
import { initializeProject } from "./init.js";
|
|
11
|
+
import { isConcurrencyLimit } from "./run-coordination.js";
|
|
12
|
+
import { readStandardInput } from "./standard-input.js";
|
|
13
|
+
import { executeWorkflowWithControls, isWorkflowDefinition, } from "./workflow.js";
|
|
14
|
+
const PRE_EXECUTION_ERROR = 2;
|
|
15
|
+
const EXECUTION_ERROR = 1;
|
|
16
|
+
const CANCELLED = 130;
|
|
17
|
+
const SUCCESS = 0;
|
|
18
|
+
const PACKAGE_NAME = "@nyxa/automation";
|
|
19
|
+
const HELP = `Usage: automation <workflow> [options]
|
|
20
|
+
automation run <workflow> [options]
|
|
21
|
+
automation init <workflow> [options]
|
|
22
|
+
automation <command>
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
25
|
+
init Initialize Automation in an existing npm project
|
|
26
|
+
doctor Diagnose the local Automation environment
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--harness <name> Select codex, claude-code, kimi-code, or opencode for init or doctor
|
|
30
|
+
--no-install Generate a Workflow without installing the package
|
|
31
|
+
--yes Accept init installation and auto-select one ready Harness
|
|
32
|
+
--input <json|-> Read Workflow Input from inline JSON or stdin
|
|
33
|
+
--input-file <path> Read Workflow Input from a JSON file
|
|
34
|
+
--concurrency <n> Limit concurrent Runs
|
|
35
|
+
--json Write Automation Events as JSONL
|
|
36
|
+
--no-record Disable the Run Journal
|
|
37
|
+
-h, --help Show this help
|
|
38
|
+
-v, --version Show the installed CLI version
|
|
39
|
+
`;
|
|
40
|
+
async function main(arguments_) {
|
|
41
|
+
const invocation = classifyInvocation(arguments_);
|
|
42
|
+
if (invocation === "help") {
|
|
43
|
+
writeSync(process.stdout.fd, HELP);
|
|
44
|
+
return SUCCESS;
|
|
45
|
+
}
|
|
46
|
+
if (invocation === "version") {
|
|
47
|
+
try {
|
|
48
|
+
writeSync(process.stdout.fd, `${ownPackageVersion()}\n`);
|
|
49
|
+
return SUCCESS;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
writeDiagnostic(error);
|
|
53
|
+
return PRE_EXECUTION_ERROR;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (invocation === "doctor") {
|
|
57
|
+
try {
|
|
58
|
+
const harness = parseDoctorArguments(arguments_.slice(1));
|
|
59
|
+
const localPackage = resolveProjectLocalPackage(process.cwd());
|
|
60
|
+
const result = await diagnoseEnvironment({
|
|
61
|
+
environment: process.env,
|
|
62
|
+
...(harness === undefined ? {} : { harness }),
|
|
63
|
+
...(localPackage === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: {
|
|
66
|
+
localPackage: {
|
|
67
|
+
binaryAvailable: localPackage.binaryAvailable,
|
|
68
|
+
version: localPackage.version,
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
writeSync(process.stdout.fd, result.output);
|
|
73
|
+
return result.ready ? SUCCESS : EXECUTION_ERROR;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
writeDiagnostic(error);
|
|
77
|
+
return PRE_EXECUTION_ERROR;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (invocation === "init") {
|
|
81
|
+
try {
|
|
82
|
+
const output = await initializeProject(arguments_.slice(1));
|
|
83
|
+
writeSync(process.stdout.fd, output);
|
|
84
|
+
return SUCCESS;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
writeDiagnostic(error);
|
|
88
|
+
return PRE_EXECUTION_ERROR;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (invocation === "workflow") {
|
|
92
|
+
try {
|
|
93
|
+
const localPackage = resolveProjectLocalPackage(process.cwd());
|
|
94
|
+
if (localPackage === undefined) {
|
|
95
|
+
throw new Error(`A project-local installation of ${PACKAGE_NAME} is required to execute a Workflow. Install it as a development dependency before running this command.`);
|
|
96
|
+
}
|
|
97
|
+
if (!localPackage.binaryAvailable) {
|
|
98
|
+
throw new Error(`The project-local ${PACKAGE_NAME} package is missing its automation binary. Reinstall it before running a Workflow.`);
|
|
99
|
+
}
|
|
100
|
+
if (!isCurrentCli(localPackage.cliPath)) {
|
|
101
|
+
return await delegateToLocalCli(localPackage.cliPath, arguments_);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
writeDiagnostic(error);
|
|
106
|
+
return PRE_EXECUTION_ERROR;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let parsedArguments;
|
|
110
|
+
let loadedInput;
|
|
111
|
+
try {
|
|
112
|
+
parsedArguments = parseArguments(arguments_);
|
|
113
|
+
loadedInput = await loadWorkflowInput(parsedArguments.inputSource);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
writeDiagnostic(error);
|
|
117
|
+
return PRE_EXECUTION_ERROR;
|
|
118
|
+
}
|
|
119
|
+
let workflowModule;
|
|
120
|
+
try {
|
|
121
|
+
const workflowUrl = pathToFileURL(path.resolve(process.cwd(), parsedArguments.workflowPath));
|
|
122
|
+
workflowModule = (await tsImport(workflowUrl.href, import.meta.url));
|
|
123
|
+
}
|
|
124
|
+
catch (cause) {
|
|
125
|
+
writeDiagnostic(new AutomationError("WORKFLOW_MODULE_LOAD_FAILED", `Could not load Workflow module: ${errorMessage(cause)}`, { cause }));
|
|
126
|
+
return PRE_EXECUTION_ERROR;
|
|
127
|
+
}
|
|
128
|
+
if (!isWorkflowDefinition(workflowModule.default)) {
|
|
129
|
+
writeDiagnostic(new Error("The module must export a default Workflow Definition created by defineWorkflow()."));
|
|
130
|
+
return PRE_EXECUTION_ERROR;
|
|
131
|
+
}
|
|
132
|
+
let result;
|
|
133
|
+
let runStarted = false;
|
|
134
|
+
const cancellation = new AbortController();
|
|
135
|
+
const force = new AbortController();
|
|
136
|
+
let forceTimer;
|
|
137
|
+
let interrupted = false;
|
|
138
|
+
const handleInterrupt = () => {
|
|
139
|
+
if (!interrupted) {
|
|
140
|
+
interrupted = true;
|
|
141
|
+
cancellation.abort();
|
|
142
|
+
forceTimer = setTimeout(() => force.abort(), 5_000);
|
|
143
|
+
forceTimer.unref();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
force.abort();
|
|
147
|
+
};
|
|
148
|
+
process.on("SIGINT", handleInterrupt);
|
|
149
|
+
try {
|
|
150
|
+
const executeDynamicWorkflow = executeWorkflowWithControls;
|
|
151
|
+
const writeEvent = parsedArguments.json ? writeJsonEvent : writeHumanEvent;
|
|
152
|
+
const onEvent = (event) => {
|
|
153
|
+
if (event.type === "run.started") {
|
|
154
|
+
runStarted = true;
|
|
155
|
+
}
|
|
156
|
+
writeEvent(event);
|
|
157
|
+
};
|
|
158
|
+
const executionOptions = {
|
|
159
|
+
...(parsedArguments.concurrency === undefined
|
|
160
|
+
? {}
|
|
161
|
+
: { concurrency: parsedArguments.concurrency }),
|
|
162
|
+
...(parsedArguments.noRecord ? { record: false } : {}),
|
|
163
|
+
onEvent,
|
|
164
|
+
signal: cancellation.signal,
|
|
165
|
+
};
|
|
166
|
+
result = loadedInput.hasInput
|
|
167
|
+
? await executeDynamicWorkflow(workflowModule.default, {
|
|
168
|
+
...executionOptions,
|
|
169
|
+
input: loadedInput.value,
|
|
170
|
+
}, force.signal)
|
|
171
|
+
: await executeDynamicWorkflow(workflowModule.default, executionOptions, force.signal);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
writeDiagnostic(error);
|
|
175
|
+
if (!runStarted && isPreExecutionError(error)) {
|
|
176
|
+
return PRE_EXECUTION_ERROR;
|
|
177
|
+
}
|
|
178
|
+
return isCancelled(error) ? CANCELLED : EXECUTION_ERROR;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
process.removeListener("SIGINT", handleInterrupt);
|
|
182
|
+
if (forceTimer !== undefined) {
|
|
183
|
+
clearTimeout(forceTimer);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!parsedArguments.json) {
|
|
187
|
+
writeResult(result);
|
|
188
|
+
}
|
|
189
|
+
return SUCCESS;
|
|
190
|
+
}
|
|
191
|
+
function parseDoctorArguments(arguments_) {
|
|
192
|
+
if (arguments_.length === 0) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
if (arguments_[0] !== "--harness") {
|
|
196
|
+
throw new Error(`Unexpected doctor argument: ${String(arguments_[0])}`);
|
|
197
|
+
}
|
|
198
|
+
const harness = arguments_[1];
|
|
199
|
+
if (harness !== "codex" &&
|
|
200
|
+
harness !== "claude-code" &&
|
|
201
|
+
harness !== "kimi-code" &&
|
|
202
|
+
harness !== "opencode") {
|
|
203
|
+
throw new Error("The --harness option requires codex, claude-code, kimi-code, or opencode.");
|
|
204
|
+
}
|
|
205
|
+
if (arguments_.length !== 2) {
|
|
206
|
+
throw new Error(`Unexpected doctor argument: ${String(arguments_[2])}`);
|
|
207
|
+
}
|
|
208
|
+
return harness;
|
|
209
|
+
}
|
|
210
|
+
function classifyInvocation(arguments_) {
|
|
211
|
+
const [command] = arguments_;
|
|
212
|
+
if (command === undefined) {
|
|
213
|
+
return "missing";
|
|
214
|
+
}
|
|
215
|
+
if (command === "--help" || command === "-h") {
|
|
216
|
+
return "help";
|
|
217
|
+
}
|
|
218
|
+
if (command === "--version" || command === "-v") {
|
|
219
|
+
return "version";
|
|
220
|
+
}
|
|
221
|
+
if (command === "doctor") {
|
|
222
|
+
return "doctor";
|
|
223
|
+
}
|
|
224
|
+
if (command === "init") {
|
|
225
|
+
return "init";
|
|
226
|
+
}
|
|
227
|
+
return "workflow";
|
|
228
|
+
}
|
|
229
|
+
function ownPackageVersion() {
|
|
230
|
+
const manifest = readPackageManifest(fileURLToPath(new URL("../package.json", import.meta.url)));
|
|
231
|
+
if (typeof manifest.version !== "string") {
|
|
232
|
+
throw new Error(`The ${PACKAGE_NAME} package has no valid version.`);
|
|
233
|
+
}
|
|
234
|
+
return manifest.version;
|
|
235
|
+
}
|
|
236
|
+
function resolveProjectLocalPackage(invocationDirectory) {
|
|
237
|
+
let directory = path.resolve(invocationDirectory);
|
|
238
|
+
let dependencyDeclared = false;
|
|
239
|
+
while (true) {
|
|
240
|
+
const projectManifestPath = path.join(directory, "package.json");
|
|
241
|
+
try {
|
|
242
|
+
const projectManifest = readPackageManifest(projectManifestPath);
|
|
243
|
+
dependencyDeclared ||= hasAutomationDevelopmentDependency(projectManifest);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
if (!isMissingFileError(error)) {
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const manifestPath = path.join(directory, "node_modules", "@nyxa", "automation", "package.json");
|
|
251
|
+
if (dependencyDeclared) {
|
|
252
|
+
let manifest;
|
|
253
|
+
try {
|
|
254
|
+
manifest = readPackageManifest(manifestPath);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (!isMissingFileError(error)) {
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (manifest !== undefined) {
|
|
262
|
+
if (manifest.name !== PACKAGE_NAME) {
|
|
263
|
+
throw new Error(`The project-local package at ${manifestPath} is not ${PACKAGE_NAME}.`);
|
|
264
|
+
}
|
|
265
|
+
const binary = typeof manifest.bin === "string"
|
|
266
|
+
? manifest.bin
|
|
267
|
+
: manifest.bin?.automation;
|
|
268
|
+
if (typeof binary !== "string") {
|
|
269
|
+
throw new Error(`The project-local ${PACKAGE_NAME} package does not expose the automation binary.`);
|
|
270
|
+
}
|
|
271
|
+
if (typeof manifest.version !== "string") {
|
|
272
|
+
throw new Error(`The project-local ${PACKAGE_NAME} package has no valid version.`);
|
|
273
|
+
}
|
|
274
|
+
const cliPath = path.resolve(path.dirname(manifestPath), binary);
|
|
275
|
+
return {
|
|
276
|
+
binaryAvailable: isRegularFile(cliPath),
|
|
277
|
+
cliPath,
|
|
278
|
+
version: manifest.version,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const parent = path.dirname(directory);
|
|
283
|
+
if (parent === directory) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
directory = parent;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function isRegularFile(filePath) {
|
|
290
|
+
try {
|
|
291
|
+
return statSync(filePath).isFile();
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (isMissingFileError(error)) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function hasAutomationDevelopmentDependency(manifest) {
|
|
301
|
+
return typeof manifest.devDependencies?.[PACKAGE_NAME] === "string";
|
|
302
|
+
}
|
|
303
|
+
function readPackageManifest(manifestPath) {
|
|
304
|
+
const serialized = readFileSync(manifestPath, "utf8");
|
|
305
|
+
try {
|
|
306
|
+
return JSON.parse(serialized);
|
|
307
|
+
}
|
|
308
|
+
catch (cause) {
|
|
309
|
+
throw new Error(`Could not parse package manifest at ${manifestPath}.`, {
|
|
310
|
+
cause,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function isCurrentCli(localCliPath) {
|
|
315
|
+
const localCli = realpathSync(localCliPath);
|
|
316
|
+
const currentCli = realpathSync(fileURLToPath(import.meta.url));
|
|
317
|
+
return localCli === currentCli;
|
|
318
|
+
}
|
|
319
|
+
async function delegateToLocalCli(localCliPath, arguments_) {
|
|
320
|
+
const { spawn } = await import("node:child_process");
|
|
321
|
+
return await new Promise((resolve, reject) => {
|
|
322
|
+
const child = spawn(process.execPath, [localCliPath, ...arguments_], {
|
|
323
|
+
cwd: process.cwd(),
|
|
324
|
+
env: process.env,
|
|
325
|
+
stdio: "inherit",
|
|
326
|
+
});
|
|
327
|
+
child.once("error", (cause) => {
|
|
328
|
+
reject(new Error(`Could not start the project-local ${PACKAGE_NAME} CLI.`, {
|
|
329
|
+
cause,
|
|
330
|
+
}));
|
|
331
|
+
});
|
|
332
|
+
child.once("exit", (code, signal) => {
|
|
333
|
+
if (code !== null) {
|
|
334
|
+
resolve(code);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
resolve(signal === "SIGINT" ? CANCELLED : EXECUTION_ERROR);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function parseArguments(arguments_) {
|
|
342
|
+
if (arguments_.length === 0) {
|
|
343
|
+
throw new Error("A Workflow module path is required.");
|
|
344
|
+
}
|
|
345
|
+
const [commandOrPath, possiblePath] = arguments_;
|
|
346
|
+
const explicitRun = commandOrPath === "run";
|
|
347
|
+
const workflowPath = explicitRun ? possiblePath : commandOrPath;
|
|
348
|
+
if (workflowPath === undefined) {
|
|
349
|
+
throw new Error("A Workflow module path is required.");
|
|
350
|
+
}
|
|
351
|
+
let inputSource;
|
|
352
|
+
let concurrency;
|
|
353
|
+
let json = false;
|
|
354
|
+
let noRecord = false;
|
|
355
|
+
let index = explicitRun ? 2 : 1;
|
|
356
|
+
while (index < arguments_.length) {
|
|
357
|
+
const option = arguments_[index];
|
|
358
|
+
if (option === "--json") {
|
|
359
|
+
if (json) {
|
|
360
|
+
throw new Error("The --json option may only be provided once.");
|
|
361
|
+
}
|
|
362
|
+
json = true;
|
|
363
|
+
index += 1;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (option === "--no-record") {
|
|
367
|
+
if (noRecord) {
|
|
368
|
+
throw new Error("The --no-record option may only be provided once.");
|
|
369
|
+
}
|
|
370
|
+
noRecord = true;
|
|
371
|
+
index += 1;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (option === "--concurrency") {
|
|
375
|
+
if (concurrency !== undefined) {
|
|
376
|
+
throw new Error("The --concurrency option may only be provided once.");
|
|
377
|
+
}
|
|
378
|
+
const value = arguments_[index + 1];
|
|
379
|
+
if (value === undefined) {
|
|
380
|
+
throw new Error("The --concurrency option requires a value.");
|
|
381
|
+
}
|
|
382
|
+
concurrency = Number(value);
|
|
383
|
+
if (!isConcurrencyLimit(concurrency)) {
|
|
384
|
+
throw new Error("The --concurrency option must be a positive integer.");
|
|
385
|
+
}
|
|
386
|
+
index += 2;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (option !== "--input" && option !== "--input-file") {
|
|
390
|
+
throw new Error(`Unexpected argument: ${option}`);
|
|
391
|
+
}
|
|
392
|
+
const value = arguments_[index + 1];
|
|
393
|
+
if (value === undefined) {
|
|
394
|
+
throw new Error(`The ${option} option requires a value.`);
|
|
395
|
+
}
|
|
396
|
+
if (inputSource !== undefined) {
|
|
397
|
+
throw new Error("Only one Workflow Input source may be provided.");
|
|
398
|
+
}
|
|
399
|
+
inputSource =
|
|
400
|
+
option === "--input-file"
|
|
401
|
+
? { kind: "file", value }
|
|
402
|
+
: value === "-"
|
|
403
|
+
? { kind: "stdin" }
|
|
404
|
+
: { kind: "inline", value };
|
|
405
|
+
index += 2;
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
...(concurrency === undefined ? {} : { concurrency }),
|
|
409
|
+
...(inputSource === undefined ? {} : { inputSource }),
|
|
410
|
+
json,
|
|
411
|
+
noRecord,
|
|
412
|
+
workflowPath,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
async function loadWorkflowInput(source) {
|
|
416
|
+
if (source === undefined) {
|
|
417
|
+
return { hasInput: false };
|
|
418
|
+
}
|
|
419
|
+
let serialized;
|
|
420
|
+
if (source.kind === "file") {
|
|
421
|
+
try {
|
|
422
|
+
serialized = await readFile(path.resolve(process.cwd(), source.value), "utf8");
|
|
423
|
+
}
|
|
424
|
+
catch (cause) {
|
|
425
|
+
throw new Error(`Could not read Workflow Input file: ${errorMessage(cause)}`, { cause });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else if (source.kind === "stdin") {
|
|
429
|
+
serialized = await readStandardInput();
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
serialized = source.value;
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
return { hasInput: true, value: JSON.parse(serialized) };
|
|
436
|
+
}
|
|
437
|
+
catch (cause) {
|
|
438
|
+
throw new Error(`Workflow Input must be valid JSON: ${errorMessage(cause)}`, {
|
|
439
|
+
cause,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function isPreExecutionError(error) {
|
|
444
|
+
return (isAutomationError(error) &&
|
|
445
|
+
(error.code === "HARNESS_NOT_FOUND" ||
|
|
446
|
+
error.code === "HARNESS_CAPABILITY_UNSUPPORTED" ||
|
|
447
|
+
error.code === "HARNESS_NOT_AUTHENTICATED" ||
|
|
448
|
+
error.code === "HARNESS_REQUIRED" ||
|
|
449
|
+
error.code === "HARNESS_VERSION_UNSUPPORTED" ||
|
|
450
|
+
error.code === "OUTPUT_SCHEMA_UNSUPPORTED" ||
|
|
451
|
+
error.code === "RUN_CWD_INVALID" ||
|
|
452
|
+
error.code === "RUN_JOURNAL_UNAVAILABLE" ||
|
|
453
|
+
error.code === "RUN_POLICY_INVALID" ||
|
|
454
|
+
error.code === "WORKFLOW_INPUT_INVALID" ||
|
|
455
|
+
error.code === "WORKFLOW_INPUT_UNEXPECTED"));
|
|
456
|
+
}
|
|
457
|
+
function isCancelled(error) {
|
|
458
|
+
return isAutomationError(error) && error.code === "RUN_CANCELLED";
|
|
459
|
+
}
|
|
460
|
+
function writeResult(result) {
|
|
461
|
+
if (result === undefined) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
writeSync(process.stdout.fd, typeof result === "string" ? result : JSON.stringify(result));
|
|
465
|
+
}
|
|
466
|
+
function writeJsonEvent(event) {
|
|
467
|
+
writeSync(process.stdout.fd, `${JSON.stringify(event)}\n`);
|
|
468
|
+
}
|
|
469
|
+
function writeHumanEvent(event) {
|
|
470
|
+
if (event.type === "run.started") {
|
|
471
|
+
const run = event.runName ?? event.runId;
|
|
472
|
+
const harness = event.harness ?? "an unresolved Harness";
|
|
473
|
+
writeSync(process.stderr.fd, `Run ${run} started with ${harness}.\n`);
|
|
474
|
+
}
|
|
475
|
+
else if (event.type === "run.completed") {
|
|
476
|
+
const run = event.runName ?? event.runId;
|
|
477
|
+
writeSync(process.stderr.fd, `Run ${run} completed.\n`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function writeDiagnostic(error) {
|
|
481
|
+
const code = isAutomationError(error) ? `${error.code}: ` : "";
|
|
482
|
+
writeSync(process.stderr.fd, `${code}${errorMessage(error)}\n`);
|
|
483
|
+
}
|
|
484
|
+
function errorMessage(error) {
|
|
485
|
+
return error instanceof Error ? error.message : String(error);
|
|
486
|
+
}
|
|
487
|
+
process.exitCode = await main(process.argv.slice(2));
|