@elench/testkit 0.1.97 → 0.1.99
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 +8 -8
- package/lib/app/browser-bridge.mjs +1 -1
- package/lib/cli/assistant/actions.mjs +333 -0
- package/lib/cli/assistant/app.mjs +25 -1
- package/lib/cli/assistant/command-observer.mjs +110 -0
- package/lib/cli/assistant/command-results.mjs +167 -0
- package/lib/cli/assistant/composer.mjs +1 -1
- package/lib/cli/assistant/context-pack.mjs +73 -6
- package/lib/cli/assistant/interactive.mjs +1 -1
- package/lib/cli/assistant/prompt-builder.mjs +15 -8
- package/lib/cli/{agents → assistant}/providers/claude.mjs +2 -3
- package/lib/cli/{agents → assistant}/providers/codex.mjs +2 -6
- package/lib/cli/{agents → assistant/providers}/index.mjs +5 -5
- package/lib/cli/assistant/session.mjs +36 -94
- package/lib/cli/assistant/slash-commands.mjs +22 -1
- package/lib/cli/assistant/state.mjs +187 -100
- package/lib/cli/assistant/view-model.mjs +1 -1
- package/lib/cli/command-flags.mjs +61 -0
- package/lib/cli/commands/assistant.mjs +4 -3
- package/lib/cli/commands/browser/serve.mjs +5 -23
- package/lib/cli/commands/cleanup.mjs +8 -2
- package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
- package/lib/cli/commands/destroy.mjs +8 -2
- package/lib/cli/commands/discover.mjs +13 -32
- package/lib/cli/commands/doctor.mjs +17 -14
- package/lib/cli/commands/run.mjs +14 -3
- package/lib/cli/commands/status.mjs +14 -3
- package/lib/cli/commands/typecheck.mjs +12 -9
- package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
- package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
- package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
- package/lib/cli/config.mjs +63 -0
- package/lib/cli/entrypoint.mjs +14 -5
- package/lib/cli/operations/browser/serve/operation.mjs +23 -0
- package/lib/cli/operations/cleanup/operation.mjs +8 -0
- package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
- package/lib/cli/operations/destroy/operation.mjs +12 -0
- package/lib/cli/operations/discover/operation.mjs +32 -0
- package/lib/cli/operations/doctor/operation.mjs +5 -0
- package/lib/cli/operations/run/operation.mjs +129 -0
- package/lib/cli/operations/status/operation.mjs +7 -0
- package/lib/cli/operations/typecheck/operation.mjs +5 -0
- package/lib/cli/renderers/browser-serve/text.mjs +6 -0
- package/lib/cli/renderers/cleanup/text.mjs +3 -0
- package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
- package/lib/cli/renderers/destroy/text.mjs +3 -0
- package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
- package/lib/cli/renderers/discover/text.mjs +7 -0
- package/lib/cli/renderers/doctor/text.mjs +7 -0
- package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
- package/lib/cli/renderers/run/interactive.mjs +119 -0
- package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
- package/lib/cli/renderers/status/text.mjs +7 -0
- package/lib/cli/renderers/typecheck/text.mjs +7 -0
- package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
- package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
- package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
- package/lib/cli/terminal/capabilities.mjs +33 -0
- package/lib/database/index.mjs +9 -21
- package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
- package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
- package/lib/runner/maintenance.mjs +25 -14
- package/lib/runner/readiness.mjs +5 -4
- package/lib/runner/state-io.mjs +10 -4
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +7 -8
- package/lib/cli/assistant/protocol.mjs +0 -67
- package/lib/cli/assistant/tool-registry.mjs +0 -318
- package/lib/cli/command-helpers.mjs +0 -191
- package/lib/cli/presentation/tree-reporter.mjs +0 -96
- package/lib/cli/tui/inspect-artifact-adapter.mjs +0 -3
- package/lib/cli/tui/inspect-live-adapter.mjs +0 -15
- /package/lib/cli/{agents → assistant}/providers/shared.mjs +0 -0
- /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
- /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
- /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
- /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import {
|
|
3
3
|
cleanupOrphanedLocalInfrastructure,
|
|
4
|
+
collectServiceDatabaseStatus,
|
|
4
5
|
destroyRuntimeDatabase,
|
|
5
6
|
destroyServiceDatabaseCache,
|
|
6
7
|
isDatabaseStateDir,
|
|
7
|
-
showServiceDatabaseStatus,
|
|
8
8
|
} from "../database/index.mjs";
|
|
9
9
|
import { cleanupRuns, formatRunSummary } from "./lifecycle.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { buildRunStatusLines } from "./readiness.mjs";
|
|
11
11
|
import { findGraphDirsForService, findRuntimeStateDirs } from "./state.mjs";
|
|
12
|
-
import {
|
|
12
|
+
import { collectStateDirLines } from "./state-io.mjs";
|
|
13
13
|
|
|
14
14
|
export async function destroy(config) {
|
|
15
15
|
await cleanupRuns(config.productDir, { includeActive: true });
|
|
@@ -35,38 +35,49 @@ export async function destroy(config) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function showStatus(config) {
|
|
38
|
-
|
|
38
|
+
const lines = [...buildRunStatusLines(config.productDir)];
|
|
39
39
|
const graphDirs = findGraphDirsForService(config.productDir, config.name);
|
|
40
40
|
const hasDirectState = fs.existsSync(config.stateDir);
|
|
41
41
|
const hasGraphState = graphDirs.length > 0;
|
|
42
42
|
|
|
43
43
|
if (!hasDirectState && !hasGraphState) {
|
|
44
|
-
|
|
44
|
+
lines.push("No state — run tests first.");
|
|
45
45
|
} else {
|
|
46
46
|
if (hasDirectState) {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
lines.push(" service-state/");
|
|
48
|
+
lines.push(...collectStateDirLines(config.stateDir, " "));
|
|
49
49
|
}
|
|
50
50
|
for (const graphDir of graphDirs) {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
lines.push(` graph-state/${graphDir.split("/").at(-1)}/`);
|
|
52
|
+
lines.push(...collectStateDirLines(graphDir, " "));
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
lines.push(...collectServiceDatabaseStatus(config.productDir, config.name));
|
|
57
|
+
return {
|
|
58
|
+
name: config.name,
|
|
59
|
+
lines,
|
|
60
|
+
};
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
export async function cleanup(productDir) {
|
|
60
64
|
const summary = await cleanupRuns(productDir, { includeActive: false });
|
|
61
65
|
if (summary.cleaned.length === 0 && summary.skippedActive.length === 0) {
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
return {
|
|
67
|
+
...summary,
|
|
68
|
+
lines: ["No stale runs to clean."],
|
|
69
|
+
};
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
const lines = [];
|
|
66
73
|
for (const manifest of summary.cleaned) {
|
|
67
|
-
|
|
74
|
+
lines.push(`Cleaned stale run ${formatRunSummary(manifest)}`);
|
|
68
75
|
}
|
|
69
76
|
for (const manifest of summary.skippedActive) {
|
|
70
|
-
|
|
77
|
+
lines.push(`Active run still present: ${formatRunSummary(manifest)}`);
|
|
71
78
|
}
|
|
79
|
+
return {
|
|
80
|
+
...summary,
|
|
81
|
+
lines,
|
|
82
|
+
};
|
|
72
83
|
}
|
package/lib/runner/readiness.mjs
CHANGED
|
@@ -96,11 +96,11 @@ export async function isPortInUse({ host, port }) {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
export function
|
|
99
|
+
export function buildRunStatusLines(productDir) {
|
|
100
100
|
const manifests = listRunManifests(productDir);
|
|
101
|
-
if (manifests.length === 0) return;
|
|
101
|
+
if (manifests.length === 0) return [];
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
const lines = [" runs/"];
|
|
104
104
|
for (const manifest of manifests) {
|
|
105
105
|
const state = isPidRunning(manifest.pid) ? "active" : "stale";
|
|
106
106
|
const ports = [
|
|
@@ -110,8 +110,9 @@ export function printRunStatus(productDir) {
|
|
|
110
110
|
)
|
|
111
111
|
),
|
|
112
112
|
];
|
|
113
|
-
|
|
113
|
+
lines.push(
|
|
114
114
|
` ${manifest.runId}: ${state} pid=${manifest.pid}${ports.length > 0 ? ` ports=${ports.join(",")}` : ""}`
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
|
+
return lines;
|
|
117
118
|
}
|
package/lib/runner/state-io.mjs
CHANGED
|
@@ -33,16 +33,22 @@ export function readStateValue(filePath) {
|
|
|
33
33
|
return fs.readFileSync(filePath, "utf8").trim();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function
|
|
36
|
+
export function collectStateDirLines(dir, indent = "") {
|
|
37
|
+
const lines = [];
|
|
38
|
+
appendStateDirLines(lines, dir, indent);
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function appendStateDirLines(lines, dir, indent) {
|
|
37
43
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
38
44
|
const filePath = path.join(dir, entry.name);
|
|
39
45
|
if (entry.isDirectory()) {
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
lines.push(`${indent}${entry.name}/`);
|
|
47
|
+
appendStateDirLines(lines, filePath, `${indent} `);
|
|
42
48
|
continue;
|
|
43
49
|
}
|
|
44
50
|
const value =
|
|
45
51
|
entry.name === "password" ? "********" : fs.readFileSync(filePath, "utf8").trim();
|
|
46
|
-
|
|
52
|
+
lines.push(`${indent}${entry.name}: ${value}`);
|
|
47
53
|
}
|
|
48
54
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.99",
|
|
4
4
|
"description": "Browser bridge helpers for testkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@elench/testkit-protocol": "0.1.
|
|
25
|
+
"@elench/testkit-protocol": "0.1.99"
|
|
26
26
|
},
|
|
27
27
|
"private": false
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.99",
|
|
4
4
|
"description": "Assistant-first CLI for running, inspecting, and debugging local testkit suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -58,12 +58,11 @@
|
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build:packages": "npm --workspace packages/testkit-protocol run build && npm --workspace packages/ts-analysis run build && npm --workspace packages/next-analysis run build && npm --workspace packages/testkit-bridge run build",
|
|
60
60
|
"typecheck:packages": "npm --workspace packages/testkit-protocol run typecheck && npm --workspace packages/ts-analysis run typecheck && npm --workspace packages/next-analysis run typecheck && npm --workspace packages/testkit-bridge run typecheck && npm --workspace packages/testkit-extension run compile",
|
|
61
|
-
"test": "npm run build:packages && vitest run",
|
|
61
|
+
"test": "npm run build:packages && vitest run && node scripts/live-sandbox/harness.mjs",
|
|
62
62
|
"test:audit": "node scripts/test-boundary-audit.mjs",
|
|
63
63
|
"test:unit": "npm run build:packages && npm run test:audit && vitest run --config vitest.unit.config.mjs",
|
|
64
64
|
"test:integration": "npm run build:packages && vitest run test/integration",
|
|
65
|
-
"test:system": "npm run build:packages && vitest run test/system --passWithNoTests"
|
|
66
|
-
"test:live-providers": "npm run build:packages && vitest run --config vitest.live.config.mjs --passWithNoTests"
|
|
65
|
+
"test:system": "npm run build:packages && vitest run test/system --passWithNoTests"
|
|
67
66
|
},
|
|
68
67
|
"files": [
|
|
69
68
|
"bin/",
|
|
@@ -84,10 +83,10 @@
|
|
|
84
83
|
},
|
|
85
84
|
"dependencies": {
|
|
86
85
|
"@babel/code-frame": "^7.29.0",
|
|
87
|
-
"@elench/next-analysis": "0.1.
|
|
88
|
-
"@elench/testkit-bridge": "0.1.
|
|
89
|
-
"@elench/testkit-protocol": "0.1.
|
|
90
|
-
"@elench/ts-analysis": "0.1.
|
|
86
|
+
"@elench/next-analysis": "0.1.99",
|
|
87
|
+
"@elench/testkit-bridge": "0.1.99",
|
|
88
|
+
"@elench/testkit-protocol": "0.1.99",
|
|
89
|
+
"@elench/ts-analysis": "0.1.99",
|
|
91
90
|
"@oclif/core": "^4.10.6",
|
|
92
91
|
"esbuild": "^0.25.11",
|
|
93
92
|
"execa": "^9.5.0",
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
export function buildAssistantResponseContract({ tools = [] } = {}) {
|
|
2
|
-
return [
|
|
3
|
-
"Respond with exactly one JSON object and no surrounding commentary.",
|
|
4
|
-
'Use {"type":"answer","message":"..."} when you can answer directly.',
|
|
5
|
-
'Use {"type":"tool","tool":"<name>","arguments":{...},"commentary":"..."} when you need testkit to act before you answer.',
|
|
6
|
-
"Only request one tool at a time.",
|
|
7
|
-
`Available tools: ${tools.map((tool) => tool.name).join(", ") || "none"}.`,
|
|
8
|
-
].join("\n");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function parseAssistantEnvelope(text) {
|
|
12
|
-
const raw = String(text || "").trim();
|
|
13
|
-
if (!raw) {
|
|
14
|
-
return {
|
|
15
|
-
type: "answer",
|
|
16
|
-
message: "",
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const candidate = extractJsonObject(raw);
|
|
21
|
-
if (!candidate) {
|
|
22
|
-
return {
|
|
23
|
-
type: "answer",
|
|
24
|
-
message: raw,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const parsed = JSON.parse(candidate);
|
|
30
|
-
if (parsed?.type === "tool" && parsed.tool) {
|
|
31
|
-
return {
|
|
32
|
-
type: "tool",
|
|
33
|
-
tool: String(parsed.tool),
|
|
34
|
-
arguments: normalizePlainObject(parsed.arguments),
|
|
35
|
-
commentary: parsed.commentary ? String(parsed.commentary) : "",
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
if (parsed?.type === "answer") {
|
|
39
|
-
return {
|
|
40
|
-
type: "answer",
|
|
41
|
-
message: parsed.message ? String(parsed.message) : "",
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// Fall through to raw answer.
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
type: "answer",
|
|
50
|
-
message: raw,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function extractJsonObject(text) {
|
|
55
|
-
const fencedMatch = text.match(/```json\s*([\s\S]*?)```/i);
|
|
56
|
-
if (fencedMatch?.[1]) return fencedMatch[1].trim();
|
|
57
|
-
|
|
58
|
-
const start = text.indexOf("{");
|
|
59
|
-
const end = text.lastIndexOf("}");
|
|
60
|
-
if (start < 0 || end <= start) return null;
|
|
61
|
-
return text.slice(start, end + 1);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function normalizePlainObject(value) {
|
|
65
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { execaCommand } from "execa";
|
|
4
|
-
import { loadCurrentRunArtifact, loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
|
|
5
|
-
import {
|
|
6
|
-
readContextContent,
|
|
7
|
-
} from "../context-resources.mjs";
|
|
8
|
-
|
|
9
|
-
const COMMAND_OUTPUT_LIMIT = 14_000;
|
|
10
|
-
const COMMAND_LINE_LIMIT = 220;
|
|
11
|
-
const FILE_LINE_LIMIT = 160;
|
|
12
|
-
|
|
13
|
-
export function listAssistantTools() {
|
|
14
|
-
return [
|
|
15
|
-
{
|
|
16
|
-
name: "shell_exec",
|
|
17
|
-
description: "Execute a shell command inside the repository. Prefer real repo commands such as npm, npx, and testkit.",
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: "read_context",
|
|
21
|
-
description: "Read testkit-managed context such as focused detail, logs, artifacts, setup, or run summary.",
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: "read_file",
|
|
25
|
-
description: "Read a local file with optional start and end lines.",
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
name: "search_repo",
|
|
29
|
-
description: "Search the repository with ripgrep and return matching lines.",
|
|
30
|
-
},
|
|
31
|
-
];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function executeAssistantTool(name, argumentsObject, context) {
|
|
35
|
-
const args = argumentsObject && typeof argumentsObject === "object" ? argumentsObject : {};
|
|
36
|
-
switch (name) {
|
|
37
|
-
case "shell_exec":
|
|
38
|
-
return shellExecTool(args, context);
|
|
39
|
-
case "read_context":
|
|
40
|
-
return readContextTool(args, context);
|
|
41
|
-
case "read_file":
|
|
42
|
-
return readFileTool(args, context);
|
|
43
|
-
case "search_repo":
|
|
44
|
-
return searchRepoTool(args, context);
|
|
45
|
-
default:
|
|
46
|
-
throw new Error(`Unknown assistant tool "${name}"`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function shellExecTool(args, context) {
|
|
51
|
-
const command = String(args.command || "").trim();
|
|
52
|
-
if (!command) throw new Error("shell_exec requires a command string");
|
|
53
|
-
|
|
54
|
-
const shellCommand = classifyShellCommand(command);
|
|
55
|
-
const commandId = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
56
|
-
context.commandLog?.appendCommandLog({
|
|
57
|
-
type: "command_start",
|
|
58
|
-
command: shellCommand.command,
|
|
59
|
-
commandId,
|
|
60
|
-
cwd: context.productDir,
|
|
61
|
-
raw: command,
|
|
62
|
-
});
|
|
63
|
-
context.onEvent?.({
|
|
64
|
-
type: "tool-start",
|
|
65
|
-
tool: "shell_exec",
|
|
66
|
-
command,
|
|
67
|
-
title: shellCommand.title,
|
|
68
|
-
testkitRelated: shellCommand.testkitRelated,
|
|
69
|
-
message: `Running ${shellCommand.display}`,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const result = await execaCommand(command, {
|
|
73
|
-
cwd: context.productDir,
|
|
74
|
-
reject: false,
|
|
75
|
-
shell: true,
|
|
76
|
-
env: {
|
|
77
|
-
...process.env,
|
|
78
|
-
...context.env,
|
|
79
|
-
PATH: [context.commandLog?.binDir, context.env?.PATH, process.env.PATH].filter(Boolean).join(path.delimiter),
|
|
80
|
-
TESTKIT_NO_ASSISTANT_DEFAULT: "1",
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
context.commandLog?.appendCommandLog({
|
|
85
|
-
type: "command_exit",
|
|
86
|
-
command: shellCommand.command,
|
|
87
|
-
commandId,
|
|
88
|
-
cwd: context.productDir,
|
|
89
|
-
raw: command,
|
|
90
|
-
code: result.exitCode ?? 0,
|
|
91
|
-
signal: result.signal ?? null,
|
|
92
|
-
});
|
|
93
|
-
context.onEvent?.({
|
|
94
|
-
type: "tool-exit",
|
|
95
|
-
tool: "shell_exec",
|
|
96
|
-
command,
|
|
97
|
-
title: shellCommand.title,
|
|
98
|
-
testkitRelated: shellCommand.testkitRelated,
|
|
99
|
-
code: result.exitCode ?? 0,
|
|
100
|
-
signal: result.signal ?? null,
|
|
101
|
-
message: `${shellCommand.display} exited ${result.exitCode ?? 0}`,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
if (shellCommand.testkitRelated) {
|
|
105
|
-
refreshArtifactSelection(context);
|
|
106
|
-
}
|
|
107
|
-
context.commandLog?.refresh?.();
|
|
108
|
-
|
|
109
|
-
const lines = formatCommandResult(command, result, shellCommand);
|
|
110
|
-
return {
|
|
111
|
-
ok: (result.exitCode ?? 0) === 0,
|
|
112
|
-
title: shellCommand.title,
|
|
113
|
-
text: lines.join("\n"),
|
|
114
|
-
data: {
|
|
115
|
-
command,
|
|
116
|
-
stdout: result.stdout || "",
|
|
117
|
-
stderr: result.stderr || "",
|
|
118
|
-
exitCode: result.exitCode ?? 0,
|
|
119
|
-
signal: result.signal ?? null,
|
|
120
|
-
testkitRelated: shellCommand.testkitRelated,
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function readContextTool(args, context) {
|
|
126
|
-
if (args.file || args.path) {
|
|
127
|
-
ensureArtifactLoaded(context);
|
|
128
|
-
const artifact = context.inspectState.getSnapshot().runArtifact;
|
|
129
|
-
const subject = resolveFileSubject(artifact, args.file || args.path, args.service || null);
|
|
130
|
-
context.inspectState.revealFile(subject.service.name, subject.file.path);
|
|
131
|
-
} else if (args.service) {
|
|
132
|
-
ensureArtifactLoaded(context);
|
|
133
|
-
if (!context.inspectState.revealService(args.service)) {
|
|
134
|
-
throw new Error(`Unknown service "${args.service}"`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const content = readContextContent({
|
|
139
|
-
productDir: context.productDir,
|
|
140
|
-
snapshot: context.inspectState.getSnapshot(),
|
|
141
|
-
mode: normalizeContextMode(args.mode),
|
|
142
|
-
logTail: args.logTail == null ? 12 : Number(args.logTail),
|
|
143
|
-
});
|
|
144
|
-
context.commandLog?.refresh?.();
|
|
145
|
-
return {
|
|
146
|
-
ok: true,
|
|
147
|
-
title: content.title,
|
|
148
|
-
text: content.lines.join("\n"),
|
|
149
|
-
data: {
|
|
150
|
-
title: content.title,
|
|
151
|
-
lines: content.lines,
|
|
152
|
-
selection: content.selection,
|
|
153
|
-
mode: content.mode,
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function readFileTool(args, context) {
|
|
159
|
-
const file = String(args.path || args.file || "").trim();
|
|
160
|
-
if (!file) throw new Error("read_file requires a path");
|
|
161
|
-
const resolved = resolveRepoPath(context.productDir, file);
|
|
162
|
-
if (!resolved.startsWith(path.resolve(context.productDir))) {
|
|
163
|
-
throw new Error("read_file only supports paths inside the current repository");
|
|
164
|
-
}
|
|
165
|
-
if (!fs.existsSync(resolved)) {
|
|
166
|
-
throw new Error(`File not found: ${file}`);
|
|
167
|
-
}
|
|
168
|
-
const startLine = Math.max(1, Number(args.startLine || args.start || 1) || 1);
|
|
169
|
-
const requestedEnd = Number(args.endLine || args.end || startLine + FILE_LINE_LIMIT - 1) || startLine + FILE_LINE_LIMIT - 1;
|
|
170
|
-
const endLine = Math.max(startLine, Math.min(requestedEnd, startLine + FILE_LINE_LIMIT - 1));
|
|
171
|
-
const lines = fs.readFileSync(resolved, "utf8").split(/\r?\n/);
|
|
172
|
-
const selected = [];
|
|
173
|
-
for (let lineNumber = startLine; lineNumber <= Math.min(endLine, lines.length); lineNumber += 1) {
|
|
174
|
-
selected.push(`${lineNumber}: ${lines[lineNumber - 1]}`);
|
|
175
|
-
}
|
|
176
|
-
const title = `File ${path.relative(context.productDir, resolved) || path.basename(resolved)}`;
|
|
177
|
-
return {
|
|
178
|
-
ok: true,
|
|
179
|
-
title,
|
|
180
|
-
text: selected.join("\n"),
|
|
181
|
-
data: {
|
|
182
|
-
path: resolved,
|
|
183
|
-
relativePath: path.relative(context.productDir, resolved),
|
|
184
|
-
startLine,
|
|
185
|
-
endLine,
|
|
186
|
-
lines: selected,
|
|
187
|
-
},
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function searchRepoTool(args, context) {
|
|
192
|
-
const query = String(args.query || args.pattern || "").trim();
|
|
193
|
-
if (!query) throw new Error("search_repo requires a query");
|
|
194
|
-
const result = await execaCommand(
|
|
195
|
-
`rg --line-number --smart-case --hidden --glob '!node_modules' --glob '!.git' ${shellQuote(query)} .`,
|
|
196
|
-
{
|
|
197
|
-
cwd: context.productDir,
|
|
198
|
-
reject: false,
|
|
199
|
-
shell: true,
|
|
200
|
-
}
|
|
201
|
-
);
|
|
202
|
-
const combined = truncateLines((result.stdout || "").split(/\r?\n/).filter(Boolean), COMMAND_LINE_LIMIT);
|
|
203
|
-
const lines =
|
|
204
|
-
combined.length > 0
|
|
205
|
-
? combined
|
|
206
|
-
: [`No matches for ${query}`];
|
|
207
|
-
return {
|
|
208
|
-
ok: (result.exitCode ?? 1) === 0,
|
|
209
|
-
title: `Search ${query}`,
|
|
210
|
-
text: lines.join("\n"),
|
|
211
|
-
data: {
|
|
212
|
-
query,
|
|
213
|
-
matches: combined,
|
|
214
|
-
exitCode: result.exitCode ?? 1,
|
|
215
|
-
},
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function classifyShellCommand(command) {
|
|
220
|
-
const normalized = command.trim();
|
|
221
|
-
if (/^(testkit)\b/.test(normalized)) {
|
|
222
|
-
return {
|
|
223
|
-
command: "testkit",
|
|
224
|
-
display: normalized,
|
|
225
|
-
title: "testkit command",
|
|
226
|
-
testkitRelated: true,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
if (/^(npx)\s+testkit\b/.test(normalized)) {
|
|
230
|
-
return {
|
|
231
|
-
command: "npx testkit",
|
|
232
|
-
display: normalized,
|
|
233
|
-
title: "npx testkit",
|
|
234
|
-
testkitRelated: true,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
if (/^(npm)\s+run\s+testkit\b/.test(normalized) || /^(npm)\s+run\s+testkit:/.test(normalized)) {
|
|
238
|
-
return {
|
|
239
|
-
command: "npm run testkit",
|
|
240
|
-
display: normalized,
|
|
241
|
-
title: "npm testkit script",
|
|
242
|
-
testkitRelated: true,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
return {
|
|
246
|
-
command: normalized.split(/\s+/)[0] || "command",
|
|
247
|
-
display: normalized,
|
|
248
|
-
title: "Shell command",
|
|
249
|
-
testkitRelated: false,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function formatCommandResult(command, result, shellCommand) {
|
|
254
|
-
const lines = [`$ ${command}`];
|
|
255
|
-
const stdout = (result.stdout || "").trim();
|
|
256
|
-
const stderr = (result.stderr || "").trim();
|
|
257
|
-
const merged = [];
|
|
258
|
-
if (stdout) merged.push(...stdout.split(/\r?\n/));
|
|
259
|
-
if (stderr) merged.push(...stderr.split(/\r?\n/).map((line) => `stderr: ${line}`));
|
|
260
|
-
if (merged.length === 0) {
|
|
261
|
-
merged.push(`exit ${result.exitCode ?? 0}`);
|
|
262
|
-
}
|
|
263
|
-
const trimmed = truncateLines(merged, COMMAND_LINE_LIMIT).map((line) => truncateText(line, COMMAND_OUTPUT_LIMIT));
|
|
264
|
-
lines.push(...trimmed);
|
|
265
|
-
if ((result.exitCode ?? 0) !== 0) {
|
|
266
|
-
lines.push(`exit code: ${result.exitCode ?? 0}`);
|
|
267
|
-
} else if (!shellCommand.testkitRelated) {
|
|
268
|
-
lines.push("exit code: 0");
|
|
269
|
-
}
|
|
270
|
-
return lines;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function truncateLines(lines, limit) {
|
|
274
|
-
if (lines.length <= limit) return lines;
|
|
275
|
-
return [...lines.slice(0, limit - 1), `… ${lines.length - limit + 1} more lines omitted`];
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function truncateText(value, maxLength) {
|
|
279
|
-
const normalized = String(value || "");
|
|
280
|
-
if (normalized.length <= maxLength) return normalized;
|
|
281
|
-
return `${normalized.slice(0, maxLength - 1)}…`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function resolveRepoPath(productDir, file) {
|
|
285
|
-
return path.resolve(productDir, file);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function shellQuote(value) {
|
|
289
|
-
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function normalizeContextMode(mode) {
|
|
293
|
-
if (mode === "logs" || mode === "artifacts" || mode === "setup") return mode;
|
|
294
|
-
return "detail";
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function ensureArtifactLoaded(context) {
|
|
298
|
-
const snapshot = context.inspectState.getSnapshot();
|
|
299
|
-
if (snapshot.runArtifact) return snapshot.runArtifact;
|
|
300
|
-
try {
|
|
301
|
-
context.inspectState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
|
|
302
|
-
} catch {
|
|
303
|
-
context.inspectState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
|
|
304
|
-
}
|
|
305
|
-
return context.inspectState.getSnapshot().runArtifact;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function refreshArtifactSelection(context) {
|
|
309
|
-
try {
|
|
310
|
-
context.inspectState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
|
|
311
|
-
} catch {
|
|
312
|
-
try {
|
|
313
|
-
context.inspectState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
|
|
314
|
-
} catch {
|
|
315
|
-
// Ignore missing artifacts.
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|