agentweaver 0.1.0 → 0.1.2
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 +24 -0
- package/dist/executors/claude-executor.js +36 -0
- package/dist/executors/claude-summary-executor.js +31 -0
- package/dist/executors/codex-docker-executor.js +27 -0
- package/dist/executors/codex-local-executor.js +25 -0
- package/dist/executors/command-check-executor.js +14 -0
- package/dist/executors/configs/claude-config.js +11 -0
- package/dist/executors/configs/claude-summary-config.js +8 -0
- package/dist/executors/configs/codex-docker-config.js +10 -0
- package/dist/executors/configs/codex-local-config.js +8 -0
- package/dist/executors/configs/jira-fetch-config.js +4 -0
- package/dist/executors/configs/process-config.js +3 -0
- package/dist/executors/configs/verify-build-config.js +7 -0
- package/dist/executors/jira-fetch-executor.js +11 -0
- package/dist/executors/process-executor.js +21 -0
- package/dist/executors/types.js +1 -0
- package/dist/executors/verify-build-executor.js +22 -0
- package/dist/index.js +270 -450
- package/dist/interactive-ui.js +109 -12
- package/dist/pipeline/build-failure-summary.js +6 -0
- package/dist/pipeline/checks.js +15 -0
- package/dist/pipeline/context.js +17 -0
- package/dist/pipeline/flow-runner.js +13 -0
- package/dist/pipeline/flow-types.js +1 -0
- package/dist/pipeline/flows/implement-flow.js +48 -0
- package/dist/pipeline/flows/plan-flow.js +42 -0
- package/dist/pipeline/flows/preflight-flow.js +59 -0
- package/dist/pipeline/flows/review-fix-flow.js +63 -0
- package/dist/pipeline/flows/review-flow.js +120 -0
- package/dist/pipeline/flows/test-fix-flow.js +13 -0
- package/dist/pipeline/flows/test-flow.js +32 -0
- package/dist/pipeline/node-runner.js +14 -0
- package/dist/pipeline/nodes/build-failure-summary-node.js +71 -0
- package/dist/pipeline/nodes/claude-summary-node.js +32 -0
- package/dist/pipeline/nodes/codex-docker-prompt-node.js +31 -0
- package/dist/pipeline/nodes/command-check-node.js +10 -0
- package/dist/pipeline/nodes/implement-codex-node.js +16 -0
- package/dist/pipeline/nodes/jira-fetch-node.js +25 -0
- package/dist/pipeline/nodes/plan-codex-node.js +32 -0
- package/dist/pipeline/nodes/review-claude-node.js +38 -0
- package/dist/pipeline/nodes/review-reply-codex-node.js +40 -0
- package/dist/pipeline/nodes/task-summary-node.js +36 -0
- package/dist/pipeline/nodes/verify-build-node.js +14 -0
- package/dist/pipeline/registry.js +25 -0
- package/dist/pipeline/types.js +10 -0
- package/dist/runtime/command-resolution.js +139 -0
- package/dist/runtime/docker-runtime.js +51 -0
- package/dist/runtime/process-runner.js +111 -0
- package/dist/tui.js +34 -0
- package/package.json +2 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { toExecutorContext } from "../types.js";
|
|
2
|
+
const DEFAULT_CLAUDE_SUMMARY_MODEL = "haiku";
|
|
3
|
+
function truncateText(text, maxChars = 12000) {
|
|
4
|
+
return text.length <= maxChars ? text.trim() : text.trim().slice(-maxChars);
|
|
5
|
+
}
|
|
6
|
+
function fallbackBuildFailureSummary(output) {
|
|
7
|
+
const lines = output
|
|
8
|
+
.split(/\r?\n/)
|
|
9
|
+
.map((line) => line.trim())
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
const tail = lines.length > 0 ? lines.slice(-8) : ["No build output captured."];
|
|
12
|
+
return `Не удалось получить summary через Claude.\n\nПоследние строки лога:\n${tail.join("\n")}`;
|
|
13
|
+
}
|
|
14
|
+
function claudeSummaryModel(env) {
|
|
15
|
+
return env.CLAUDE_SUMMARY_MODEL?.trim() || DEFAULT_CLAUDE_SUMMARY_MODEL;
|
|
16
|
+
}
|
|
17
|
+
export const buildFailureSummaryNode = {
|
|
18
|
+
kind: "build-failure-summary",
|
|
19
|
+
version: 1,
|
|
20
|
+
async run(context, params) {
|
|
21
|
+
if (!params.output.trim()) {
|
|
22
|
+
return {
|
|
23
|
+
value: {
|
|
24
|
+
summaryText: "Build verification failed, but no output was captured.",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
let claudeCmd;
|
|
29
|
+
try {
|
|
30
|
+
claudeCmd = context.runtime.resolveCmd("claude", "CLAUDE_BIN");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {
|
|
34
|
+
value: {
|
|
35
|
+
summaryText: fallbackBuildFailureSummary(params.output),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const model = claudeSummaryModel(context.env);
|
|
40
|
+
const prompt = "Ниже лог упавшей build verification.\n" +
|
|
41
|
+
"Сделай краткое резюме на русском языке, без воды.\n" +
|
|
42
|
+
"Нужно обязательно выделить:\n" +
|
|
43
|
+
"1. Где именно упало.\n" +
|
|
44
|
+
"2. Главную причину падения.\n" +
|
|
45
|
+
"3. Что нужно исправить дальше, если это очевидно.\n" +
|
|
46
|
+
"Ответ дай максимум 5 короткими пунктами.\n\n" +
|
|
47
|
+
`Лог:\n${truncateText(params.output)}`;
|
|
48
|
+
try {
|
|
49
|
+
const executor = context.executors.get("process");
|
|
50
|
+
const result = await executor.execute(toExecutorContext(context), {
|
|
51
|
+
argv: [claudeCmd, "--model", model, "-p", prompt],
|
|
52
|
+
env: { ...context.env },
|
|
53
|
+
dryRun: false,
|
|
54
|
+
verbose: false,
|
|
55
|
+
label: `claude:${model}`,
|
|
56
|
+
}, executor.defaultConfig);
|
|
57
|
+
return {
|
|
58
|
+
value: {
|
|
59
|
+
summaryText: result.output.trim() || fallbackBuildFailureSummary(params.output),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return {
|
|
65
|
+
value: {
|
|
66
|
+
summaryText: fallbackBuildFailureSummary(params.output),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { printInfo, printPrompt, printSummary } from "../../tui.js";
|
|
2
|
+
import { toExecutorContext } from "../types.js";
|
|
3
|
+
export const claudeSummaryNode = {
|
|
4
|
+
kind: "claude-summary",
|
|
5
|
+
version: 1,
|
|
6
|
+
async run(context, params) {
|
|
7
|
+
printInfo(`Preparing summary in ${params.outputFile}`);
|
|
8
|
+
printPrompt("Claude", params.prompt);
|
|
9
|
+
const executor = context.executors.get("claude-summary");
|
|
10
|
+
const value = await executor.execute(toExecutorContext(context), {
|
|
11
|
+
prompt: params.prompt,
|
|
12
|
+
outputFile: params.outputFile,
|
|
13
|
+
command: params.claudeCmd,
|
|
14
|
+
env: { ...context.env },
|
|
15
|
+
verbose: params.verbose,
|
|
16
|
+
}, executor.defaultConfig);
|
|
17
|
+
printSummary(params.summaryTitle, value.artifactText);
|
|
18
|
+
return {
|
|
19
|
+
value,
|
|
20
|
+
outputs: [{ kind: "artifact", path: params.outputFile, required: true }],
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
checks(_context, params) {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
kind: "require-artifacts",
|
|
27
|
+
paths: [params.outputFile],
|
|
28
|
+
message: `Claude summary did not produce ${params.outputFile}.`,
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { printInfo, printPrompt } from "../../tui.js";
|
|
2
|
+
import { toExecutorContext } from "../types.js";
|
|
3
|
+
export const codexDockerPromptNode = {
|
|
4
|
+
kind: "codex-docker-prompt",
|
|
5
|
+
version: 1,
|
|
6
|
+
async run(context, params) {
|
|
7
|
+
printInfo(params.labelText);
|
|
8
|
+
printPrompt("Codex", params.prompt);
|
|
9
|
+
const executor = context.executors.get("codex-docker");
|
|
10
|
+
const value = await executor.execute(toExecutorContext(context), {
|
|
11
|
+
dockerComposeFile: params.dockerComposeFile,
|
|
12
|
+
prompt: params.prompt,
|
|
13
|
+
}, executor.defaultConfig);
|
|
14
|
+
return {
|
|
15
|
+
value,
|
|
16
|
+
outputs: (params.requiredArtifacts ?? []).map((path) => ({ kind: "artifact", path, required: true })),
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
checks(_context, params) {
|
|
20
|
+
if (!params.requiredArtifacts || params.requiredArtifacts.length === 0) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
kind: "require-artifacts",
|
|
26
|
+
paths: params.requiredArtifacts,
|
|
27
|
+
message: params.missingArtifactsMessage ?? "Codex docker node did not produce required artifacts.",
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { toExecutorContext } from "../types.js";
|
|
2
|
+
export const commandCheckNode = {
|
|
3
|
+
kind: "command-check",
|
|
4
|
+
version: 1,
|
|
5
|
+
async run(context, params) {
|
|
6
|
+
const executor = context.executors.get("command-check");
|
|
7
|
+
const value = await executor.execute(toExecutorContext(context), params, executor.defaultConfig);
|
|
8
|
+
return { value };
|
|
9
|
+
},
|
|
10
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { printInfo, printPrompt } from "../../tui.js";
|
|
2
|
+
import { toExecutorContext } from "../types.js";
|
|
3
|
+
export const implementCodexNode = {
|
|
4
|
+
kind: "implement-codex",
|
|
5
|
+
version: 1,
|
|
6
|
+
async run(context, params) {
|
|
7
|
+
printInfo(params.labelText);
|
|
8
|
+
printPrompt("Codex", params.prompt);
|
|
9
|
+
const executor = context.executors.get("codex-docker");
|
|
10
|
+
const value = await executor.execute(toExecutorContext(context), {
|
|
11
|
+
dockerComposeFile: params.dockerComposeFile,
|
|
12
|
+
prompt: params.prompt,
|
|
13
|
+
}, executor.defaultConfig);
|
|
14
|
+
return { value };
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { toExecutorContext } from "../types.js";
|
|
2
|
+
export const jiraFetchNode = {
|
|
3
|
+
kind: "jira-fetch",
|
|
4
|
+
version: 1,
|
|
5
|
+
async run(context, params) {
|
|
6
|
+
const executor = context.executors.get("jira-fetch");
|
|
7
|
+
const value = await executor.execute(toExecutorContext(context), {
|
|
8
|
+
jiraApiUrl: params.jiraApiUrl,
|
|
9
|
+
outputFile: params.outputFile,
|
|
10
|
+
}, executor.defaultConfig);
|
|
11
|
+
return {
|
|
12
|
+
value,
|
|
13
|
+
outputs: [{ kind: "file", path: params.outputFile, required: true }],
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
checks(_context, params) {
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
kind: "require-file",
|
|
20
|
+
path: params.outputFile,
|
|
21
|
+
message: `Jira fetch node did not produce ${params.outputFile}.`,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { printInfo, printPrompt } from "../../tui.js";
|
|
2
|
+
import { toExecutorContext } from "../types.js";
|
|
3
|
+
export const planCodexNode = {
|
|
4
|
+
kind: "plan-codex",
|
|
5
|
+
version: 1,
|
|
6
|
+
async run(context, params) {
|
|
7
|
+
printInfo("Running Codex planning mode");
|
|
8
|
+
printPrompt("Codex", params.prompt);
|
|
9
|
+
const executor = context.executors.get("codex-local");
|
|
10
|
+
const input = {
|
|
11
|
+
prompt: params.prompt,
|
|
12
|
+
env: { ...context.env },
|
|
13
|
+
};
|
|
14
|
+
if (params.command) {
|
|
15
|
+
input.command = params.command;
|
|
16
|
+
}
|
|
17
|
+
const value = await executor.execute(toExecutorContext(context), input, executor.defaultConfig);
|
|
18
|
+
return {
|
|
19
|
+
value,
|
|
20
|
+
outputs: params.requiredArtifacts.map((path) => ({ kind: "artifact", path, required: true })),
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
checks(_context, params) {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
kind: "require-artifacts",
|
|
27
|
+
paths: params.requiredArtifacts,
|
|
28
|
+
message: "Plan mode did not produce the required artifacts.",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { artifactFile, designFile, planFile } from "../../artifacts.js";
|
|
2
|
+
import { REVIEW_PROMPT_TEMPLATE, formatPrompt, formatTemplate } from "../../prompts.js";
|
|
3
|
+
import { printInfo, printPrompt } from "../../tui.js";
|
|
4
|
+
import { toExecutorContext } from "../types.js";
|
|
5
|
+
export const reviewClaudeNode = {
|
|
6
|
+
kind: "review-claude",
|
|
7
|
+
version: 1,
|
|
8
|
+
async run(context, params) {
|
|
9
|
+
const reviewFile = artifactFile("review", params.taskKey, params.iteration);
|
|
10
|
+
const prompt = formatPrompt(formatTemplate(REVIEW_PROMPT_TEMPLATE, {
|
|
11
|
+
jira_task_file: params.jiraTaskFile,
|
|
12
|
+
design_file: designFile(params.taskKey),
|
|
13
|
+
plan_file: planFile(params.taskKey),
|
|
14
|
+
review_file: reviewFile,
|
|
15
|
+
}), params.extraPrompt);
|
|
16
|
+
printInfo(`Running Claude review mode (iteration ${params.iteration})`);
|
|
17
|
+
printPrompt("Claude", prompt);
|
|
18
|
+
const executor = context.executors.get("claude");
|
|
19
|
+
const value = await executor.execute(toExecutorContext(context), {
|
|
20
|
+
prompt,
|
|
21
|
+
command: params.claudeCmd,
|
|
22
|
+
env: { ...context.env },
|
|
23
|
+
}, executor.defaultConfig);
|
|
24
|
+
return {
|
|
25
|
+
value,
|
|
26
|
+
outputs: [{ kind: "artifact", path: reviewFile, required: true }],
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
checks(_context, params) {
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
kind: "require-artifacts",
|
|
33
|
+
paths: [artifactFile("review", params.taskKey, params.iteration)],
|
|
34
|
+
message: "Claude review did not produce the required review artifact.",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { artifactFile, designFile, planFile } from "../../artifacts.js";
|
|
2
|
+
import { REVIEW_REPLY_PROMPT_TEMPLATE, formatPrompt, formatTemplate } from "../../prompts.js";
|
|
3
|
+
import { printInfo, printPrompt } from "../../tui.js";
|
|
4
|
+
import { toExecutorContext } from "../types.js";
|
|
5
|
+
export const reviewReplyCodexNode = {
|
|
6
|
+
kind: "review-reply-codex",
|
|
7
|
+
version: 1,
|
|
8
|
+
async run(context, params) {
|
|
9
|
+
const reviewFile = artifactFile("review", params.taskKey, params.iteration);
|
|
10
|
+
const reviewReplyFile = artifactFile("review-reply", params.taskKey, params.iteration);
|
|
11
|
+
const prompt = formatPrompt(formatTemplate(REVIEW_REPLY_PROMPT_TEMPLATE, {
|
|
12
|
+
review_file: reviewFile,
|
|
13
|
+
jira_task_file: params.jiraTaskFile,
|
|
14
|
+
design_file: designFile(params.taskKey),
|
|
15
|
+
plan_file: planFile(params.taskKey),
|
|
16
|
+
review_reply_file: reviewReplyFile,
|
|
17
|
+
}), params.extraPrompt);
|
|
18
|
+
printInfo(`Running Codex review reply mode (iteration ${params.iteration})`);
|
|
19
|
+
printPrompt("Codex", prompt);
|
|
20
|
+
const executor = context.executors.get("codex-local");
|
|
21
|
+
const value = await executor.execute(toExecutorContext(context), {
|
|
22
|
+
prompt,
|
|
23
|
+
command: params.codexCmd,
|
|
24
|
+
env: { ...context.env },
|
|
25
|
+
}, executor.defaultConfig);
|
|
26
|
+
return {
|
|
27
|
+
value,
|
|
28
|
+
outputs: [{ kind: "artifact", path: reviewReplyFile, required: true }],
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
checks(_context, params) {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
kind: "require-artifacts",
|
|
35
|
+
paths: [artifactFile("review-reply", params.taskKey, params.iteration)],
|
|
36
|
+
message: "Codex review reply did not produce the required review-reply artifact.",
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { taskSummaryFile } from "../../artifacts.js";
|
|
2
|
+
import { TASK_SUMMARY_PROMPT_TEMPLATE, formatTemplate } from "../../prompts.js";
|
|
3
|
+
import { toExecutorContext } from "../types.js";
|
|
4
|
+
export const taskSummaryNode = {
|
|
5
|
+
kind: "task-summary",
|
|
6
|
+
version: 1,
|
|
7
|
+
async run(context, params) {
|
|
8
|
+
const outputFile = taskSummaryFile(params.taskKey);
|
|
9
|
+
const prompt = formatTemplate(TASK_SUMMARY_PROMPT_TEMPLATE, {
|
|
10
|
+
jira_task_file: params.jiraTaskFile,
|
|
11
|
+
task_summary_file: outputFile,
|
|
12
|
+
});
|
|
13
|
+
const executor = context.executors.get("claude-summary");
|
|
14
|
+
const value = await executor.execute(toExecutorContext(context), {
|
|
15
|
+
prompt,
|
|
16
|
+
outputFile,
|
|
17
|
+
command: params.claudeCmd,
|
|
18
|
+
env: { ...context.env },
|
|
19
|
+
verbose: params.verbose,
|
|
20
|
+
}, executor.defaultConfig);
|
|
21
|
+
context.setSummary?.(value.artifactText);
|
|
22
|
+
return {
|
|
23
|
+
value,
|
|
24
|
+
outputs: [{ kind: "artifact", path: outputFile, required: true }],
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
checks(_context, params) {
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
kind: "require-artifacts",
|
|
31
|
+
paths: [taskSummaryFile(params.taskKey)],
|
|
32
|
+
message: `Claude summary did not produce ${taskSummaryFile(params.taskKey)}.`,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { printInfo } from "../../tui.js";
|
|
2
|
+
import { toExecutorContext } from "../types.js";
|
|
3
|
+
export const verifyBuildNode = {
|
|
4
|
+
kind: "verify-build",
|
|
5
|
+
version: 1,
|
|
6
|
+
async run(context, params) {
|
|
7
|
+
printInfo(params.labelText);
|
|
8
|
+
const executor = context.executors.get("verify-build");
|
|
9
|
+
const value = await executor.execute(toExecutorContext(context), {
|
|
10
|
+
dockerComposeFile: params.dockerComposeFile,
|
|
11
|
+
}, executor.defaultConfig);
|
|
12
|
+
return { value };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { commandCheckExecutor } from "../executors/command-check-executor.js";
|
|
2
|
+
import { claudeExecutor } from "../executors/claude-executor.js";
|
|
3
|
+
import { claudeSummaryExecutor } from "../executors/claude-summary-executor.js";
|
|
4
|
+
import { codexDockerExecutor } from "../executors/codex-docker-executor.js";
|
|
5
|
+
import { codexLocalExecutor } from "../executors/codex-local-executor.js";
|
|
6
|
+
import { jiraFetchExecutor } from "../executors/jira-fetch-executor.js";
|
|
7
|
+
import { processExecutor } from "../executors/process-executor.js";
|
|
8
|
+
import { verifyBuildExecutor } from "../executors/verify-build-executor.js";
|
|
9
|
+
const builtInExecutors = {
|
|
10
|
+
process: processExecutor,
|
|
11
|
+
"command-check": commandCheckExecutor,
|
|
12
|
+
"jira-fetch": jiraFetchExecutor,
|
|
13
|
+
"codex-local": codexLocalExecutor,
|
|
14
|
+
"codex-docker": codexDockerExecutor,
|
|
15
|
+
claude: claudeExecutor,
|
|
16
|
+
"claude-summary": claudeSummaryExecutor,
|
|
17
|
+
"verify-build": verifyBuildExecutor,
|
|
18
|
+
};
|
|
19
|
+
export function createExecutorRegistry() {
|
|
20
|
+
return {
|
|
21
|
+
get(id) {
|
|
22
|
+
return builtInExecutors[id];
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { accessSync, constants } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { TaskRunnerError } from "../errors.js";
|
|
5
|
+
function splitArgs(input) {
|
|
6
|
+
const result = [];
|
|
7
|
+
let current = "";
|
|
8
|
+
let quote = null;
|
|
9
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
10
|
+
const char = input[index] ?? "";
|
|
11
|
+
if (quote) {
|
|
12
|
+
if (char === quote) {
|
|
13
|
+
quote = null;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
current += char;
|
|
17
|
+
}
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (char === "'" || char === '"') {
|
|
21
|
+
quote = char;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (/\s/.test(char)) {
|
|
25
|
+
if (current) {
|
|
26
|
+
result.push(current);
|
|
27
|
+
current = "";
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
current += char;
|
|
32
|
+
}
|
|
33
|
+
if (quote) {
|
|
34
|
+
throw new TaskRunnerError("Cannot parse command: unterminated quote");
|
|
35
|
+
}
|
|
36
|
+
if (current) {
|
|
37
|
+
result.push(current);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
export function shellQuote(value) {
|
|
42
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
43
|
+
}
|
|
44
|
+
export function isExecutable(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
accessSync(filePath, constants.X_OK);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function commandExists(commandName) {
|
|
54
|
+
const result = spawnSync("bash", ["-lc", `command -v ${shellQuote(commandName)}`], { stdio: "ignore" });
|
|
55
|
+
return result.status === 0;
|
|
56
|
+
}
|
|
57
|
+
export function findCmdPath(commandName, envVarName) {
|
|
58
|
+
const configuredPath = process.env[envVarName];
|
|
59
|
+
if (configuredPath && isExecutable(configuredPath)) {
|
|
60
|
+
return configuredPath;
|
|
61
|
+
}
|
|
62
|
+
const direct = spawnSync("bash", ["-lc", `command -v ${shellQuote(commandName)}`], { encoding: "utf8" });
|
|
63
|
+
if (direct.status === 0) {
|
|
64
|
+
const candidate = direct.stdout.trim().split(/\r?\n/)[0] ?? "";
|
|
65
|
+
if (candidate && !candidate.includes("alias") && isExecutable(candidate)) {
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const interactive = spawnSync("bash", ["-ic", `type -a -- ${shellQuote(commandName)}`], {
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
});
|
|
72
|
+
if (interactive.status !== 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
for (const rawLine of interactive.stdout.split(/\r?\n/)) {
|
|
76
|
+
const line = rawLine.trim();
|
|
77
|
+
if (!line) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (line.startsWith(`${commandName} is aliased to `)) {
|
|
81
|
+
const aliasValue = line.split(" is aliased to ", 2)[1]?.replace(/^['`]|['`]$/g, "") ?? "";
|
|
82
|
+
if (aliasValue && isExecutable(aliasValue)) {
|
|
83
|
+
return aliasValue;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (line.startsWith("/")) {
|
|
88
|
+
const candidate = line.split(/\s+/)[0] ?? "";
|
|
89
|
+
if (candidate && isExecutable(candidate)) {
|
|
90
|
+
return candidate;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
export function resolveCmd(commandName, envVarName) {
|
|
97
|
+
const candidate = findCmdPath(commandName, envVarName);
|
|
98
|
+
if (candidate) {
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
throw new TaskRunnerError(`Missing required command: ${commandName}`);
|
|
102
|
+
}
|
|
103
|
+
export function requireDockerCompose() {
|
|
104
|
+
if (!commandExists("docker")) {
|
|
105
|
+
throw new TaskRunnerError("Missing required command: docker");
|
|
106
|
+
}
|
|
107
|
+
const result = spawnSync("docker", ["compose", "version"], { stdio: "ignore" });
|
|
108
|
+
if (result.status !== 0) {
|
|
109
|
+
throw new TaskRunnerError("Missing required docker compose plugin");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function resolveDockerComposeCmd() {
|
|
113
|
+
const configured = process.env.DOCKER_COMPOSE_BIN?.trim() ?? "";
|
|
114
|
+
if (configured) {
|
|
115
|
+
const parts = splitArgs(configured);
|
|
116
|
+
if (parts.length === 0) {
|
|
117
|
+
throw new TaskRunnerError("DOCKER_COMPOSE_BIN is set but empty.");
|
|
118
|
+
}
|
|
119
|
+
const executable = parts[0] ?? "";
|
|
120
|
+
try {
|
|
121
|
+
if (path.isAbsolute(executable)) {
|
|
122
|
+
accessSync(executable, constants.X_OK);
|
|
123
|
+
return parts;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new TaskRunnerError(`Configured docker compose command is not executable: ${configured}`);
|
|
128
|
+
}
|
|
129
|
+
if (commandExists(executable)) {
|
|
130
|
+
return parts;
|
|
131
|
+
}
|
|
132
|
+
throw new TaskRunnerError(`Configured docker compose command is not executable: ${configured}`);
|
|
133
|
+
}
|
|
134
|
+
if (commandExists("docker-compose")) {
|
|
135
|
+
return ["docker-compose"];
|
|
136
|
+
}
|
|
137
|
+
requireDockerCompose();
|
|
138
|
+
return ["docker", "compose"];
|
|
139
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function agentweaverHome(packageRoot) {
|
|
5
|
+
const configured = process.env.AGENTWEAVER_HOME?.trim();
|
|
6
|
+
if (configured) {
|
|
7
|
+
return path.resolve(configured);
|
|
8
|
+
}
|
|
9
|
+
return packageRoot;
|
|
10
|
+
}
|
|
11
|
+
export function defaultDockerComposeFile(packageRoot) {
|
|
12
|
+
return path.join(agentweaverHome(packageRoot), "docker-compose.yml");
|
|
13
|
+
}
|
|
14
|
+
function defaultCodexHomeDir(packageRoot) {
|
|
15
|
+
return path.join(agentweaverHome(packageRoot), ".codex-home");
|
|
16
|
+
}
|
|
17
|
+
function ensureRuntimeBindPath(targetPath, isDir) {
|
|
18
|
+
mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
19
|
+
if (isDir) {
|
|
20
|
+
mkdirSync(targetPath, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
else if (!existsSync(targetPath)) {
|
|
23
|
+
writeFileSync(targetPath, "", "utf8");
|
|
24
|
+
}
|
|
25
|
+
return targetPath;
|
|
26
|
+
}
|
|
27
|
+
function defaultHostSshDir(packageRoot) {
|
|
28
|
+
const candidate = path.join(os.homedir(), ".ssh");
|
|
29
|
+
if (existsSync(candidate)) {
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
return ensureRuntimeBindPath(path.join(agentweaverHome(packageRoot), ".runtime", "ssh"), true);
|
|
33
|
+
}
|
|
34
|
+
function defaultHostGitconfig(packageRoot) {
|
|
35
|
+
const candidate = path.join(os.homedir(), ".gitconfig");
|
|
36
|
+
if (existsSync(candidate)) {
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
return ensureRuntimeBindPath(path.join(agentweaverHome(packageRoot), ".runtime", "gitconfig"), false);
|
|
40
|
+
}
|
|
41
|
+
export function dockerRuntimeEnv(packageRoot) {
|
|
42
|
+
const env = { ...process.env };
|
|
43
|
+
env.AGENTWEAVER_HOME ??= agentweaverHome(packageRoot);
|
|
44
|
+
env.PROJECT_DIR ??= process.cwd();
|
|
45
|
+
env.CODEX_HOME_DIR ??= ensureRuntimeBindPath(defaultCodexHomeDir(packageRoot), true);
|
|
46
|
+
env.HOST_SSH_DIR ??= defaultHostSshDir(packageRoot);
|
|
47
|
+
env.HOST_GITCONFIG ??= defaultHostGitconfig(packageRoot);
|
|
48
|
+
env.LOCAL_UID ??= typeof process.getuid === "function" ? String(process.getuid()) : "1000";
|
|
49
|
+
env.LOCAL_GID ??= typeof process.getgid === "function" ? String(process.getgid()) : "1000";
|
|
50
|
+
return env;
|
|
51
|
+
}
|