auditor-lambda 0.6.9 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/auditStep.d.ts +63 -0
- package/dist/cli/auditStep.js +133 -0
- package/dist/cli/envelope.d.ts +47 -0
- package/dist/cli/envelope.js +64 -0
- package/dist/cli/reviewRun.d.ts +29 -0
- package/dist/cli/reviewRun.js +143 -0
- package/dist/cli.js +6 -319
- package/dist/extractors/graph.js +5 -825
- package/dist/extractors/graphPathUtils.d.ts +12 -0
- package/dist/extractors/graphPathUtils.js +58 -0
- package/dist/extractors/graphRoutes.d.ts +12 -0
- package/dist/extractors/graphRoutes.js +435 -0
- package/dist/extractors/graphSuites.d.ts +4 -0
- package/dist/extractors/graphSuites.js +246 -0
- package/dist/extractors/graphTestSources.d.ts +2 -0
- package/dist/extractors/graphTestSources.js +102 -0
- package/dist/providers/claudeCodeProvider.js +3 -2
- package/dist/providers/localSubprocessProvider.js +2 -2
- package/dist/providers/opencodeProvider.js +6 -22
- package/dist/providers/subprocessTemplateProvider.js +22 -9
- package/package.json +1 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { graphEdge, isJsonSchemaPath, isPytestConftestPath, normalizeGraphPath, resolveCandidate, STRING_LITERAL_PATTERN, } from "./graphPathUtils.js";
|
|
3
|
+
import { isTestPath, normalizeExtractorPath } from "./pathPatterns.js";
|
|
4
|
+
const JSON_SCHEMA_REF_EDGE_CONFIDENCE = 0.93;
|
|
5
|
+
const SCHEMA_CONTRACT_TEST_EDGE_CONFIDENCE = 0.86;
|
|
6
|
+
const SCHEMA_SUITE_EDGE_CONFIDENCE = 0.78;
|
|
7
|
+
const GITHUB_WORKFLOW_SUITE_EDGE_CONFIDENCE = 0.78;
|
|
8
|
+
const PACKAGE_SCRIPT_SUITE_EDGE_CONFIDENCE = 0.78;
|
|
9
|
+
const TYPESCRIPT_TYPE_SUITE_EDGE_CONFIDENCE = 0.78;
|
|
10
|
+
const PYTHON_TEST_UTIL_SUITE_EDGE_CONFIDENCE = 0.72;
|
|
11
|
+
const PYTHON_TEST_UTIL_SEGMENT_NAMES = new Set(["utils", "helpers", "support"]);
|
|
12
|
+
const MAX_BOUNDED_SUITE_EDGE_FILES = 12;
|
|
13
|
+
const MAX_BOUNDED_TYPE_SUITE_EDGE_FILES = 16;
|
|
14
|
+
const MAX_TYPE_CONTRACT_SOURCE_BYTES = 64 * 1024;
|
|
15
|
+
const TYPESCRIPT_TYPE_CONTRACT_EXTENSIONS = [
|
|
16
|
+
".ts",
|
|
17
|
+
".tsx",
|
|
18
|
+
".mts",
|
|
19
|
+
".cts",
|
|
20
|
+
];
|
|
21
|
+
const PACKAGE_SCRIPT_SUITE_EXTENSIONS = [
|
|
22
|
+
".js",
|
|
23
|
+
".jsx",
|
|
24
|
+
".mjs",
|
|
25
|
+
".cjs",
|
|
26
|
+
".ts",
|
|
27
|
+
".tsx",
|
|
28
|
+
".mts",
|
|
29
|
+
".cts",
|
|
30
|
+
];
|
|
31
|
+
function collectJsonSchemaRefs(value, refs) {
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
for (const item of value) {
|
|
34
|
+
collectJsonSchemaRefs(item, refs);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (value === null || typeof value !== "object") {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (const [key, item] of Object.entries(value)) {
|
|
42
|
+
if (key === "$ref" && typeof item === "string" && item.trim().length > 0) {
|
|
43
|
+
refs.add(item.trim());
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
collectJsonSchemaRefs(item, refs);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function resolveJsonSchemaRef(fromPath, ref, pathLookup) {
|
|
50
|
+
const targetSpecifier = (ref.split("#", 1)[0] ?? "").trim();
|
|
51
|
+
if (targetSpecifier.length === 0) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const normalizedSpecifier = normalizeGraphPath(targetSpecifier);
|
|
55
|
+
if (normalizedSpecifier.startsWith("/") ||
|
|
56
|
+
/^[a-z][a-z0-9+.-]*:/i.test(normalizedSpecifier)) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const baseDir = posix.dirname(normalizeGraphPath(fromPath));
|
|
60
|
+
const candidate = targetSpecifier.startsWith(".") || !normalizedSpecifier.includes("/")
|
|
61
|
+
? posix.join(baseDir, normalizedSpecifier)
|
|
62
|
+
: normalizedSpecifier;
|
|
63
|
+
return resolveCandidate(candidate, pathLookup);
|
|
64
|
+
}
|
|
65
|
+
export function extractJsonSchemaReferenceEdges(fromPath, content, pathLookup) {
|
|
66
|
+
if (!isJsonSchemaPath(fromPath)) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(content);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const refs = new Set();
|
|
77
|
+
collectJsonSchemaRefs(parsed, refs);
|
|
78
|
+
const edges = [];
|
|
79
|
+
for (const ref of refs) {
|
|
80
|
+
const target = resolveJsonSchemaRef(fromPath, ref, pathLookup);
|
|
81
|
+
if (!target || target === fromPath) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
edges.push(graphEdge({
|
|
85
|
+
from: fromPath,
|
|
86
|
+
to: target,
|
|
87
|
+
kind: "json-schema-ref",
|
|
88
|
+
confidence: JSON_SCHEMA_REF_EDGE_CONFIDENCE,
|
|
89
|
+
reason: `JSON Schema $ref '${ref}' resolves to '${target}'.`,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
return edges;
|
|
93
|
+
}
|
|
94
|
+
export function extractSchemaContractTestEdges(fromPath, content, pathLookup) {
|
|
95
|
+
if (!isTestPath(normalizeExtractorPath(fromPath)) ||
|
|
96
|
+
!/schema/i.test(fromPath) ||
|
|
97
|
+
!/\.schema\.json/i.test(content)) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const literalBasenames = new Set();
|
|
101
|
+
STRING_LITERAL_PATTERN.lastIndex = 0;
|
|
102
|
+
for (const match of content.matchAll(STRING_LITERAL_PATTERN)) {
|
|
103
|
+
const literal = match[1];
|
|
104
|
+
if (!literal || !literal.toLowerCase().endsWith(".schema.json")) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
literalBasenames.add(posix.basename(normalizeGraphPath(literal)).toLowerCase());
|
|
108
|
+
}
|
|
109
|
+
if (literalBasenames.size === 0 ||
|
|
110
|
+
literalBasenames.size > MAX_BOUNDED_SUITE_EDGE_FILES) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const targets = [...new Set(pathLookup.values())]
|
|
114
|
+
.filter((path) => {
|
|
115
|
+
const normalized = normalizeGraphPath(path);
|
|
116
|
+
return (isJsonSchemaPath(normalized) &&
|
|
117
|
+
literalBasenames.has(posix.basename(normalized).toLowerCase()));
|
|
118
|
+
})
|
|
119
|
+
.sort((a, b) => a.localeCompare(b));
|
|
120
|
+
if (targets.length > MAX_BOUNDED_SUITE_EDGE_FILES) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
return targets.map((target) => graphEdge({
|
|
124
|
+
from: fromPath,
|
|
125
|
+
to: target,
|
|
126
|
+
kind: "schema-contract-test-link",
|
|
127
|
+
confidence: SCHEMA_CONTRACT_TEST_EDGE_CONFIDENCE,
|
|
128
|
+
reason: `Schema contract test references '${posix.basename(target)}'.`,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
function isGithubWorkflowPath(path) {
|
|
132
|
+
const normalized = normalizeGraphPath(path).toLowerCase();
|
|
133
|
+
return (normalized.startsWith(".github/workflows/") &&
|
|
134
|
+
(normalized.endsWith(".yml") || normalized.endsWith(".yaml")));
|
|
135
|
+
}
|
|
136
|
+
function isTypescriptTypeContractPath(path, fileContents) {
|
|
137
|
+
const normalized = normalizeGraphPath(path);
|
|
138
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
139
|
+
if (!segments.includes("types") ||
|
|
140
|
+
isTestPath(normalizeExtractorPath(normalized)) ||
|
|
141
|
+
!TYPESCRIPT_TYPE_CONTRACT_EXTENSIONS.some((extension) => normalized.endsWith(extension))) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const content = fileContents[path];
|
|
145
|
+
if (!content || content.length > MAX_TYPE_CONTRACT_SOURCE_BYTES) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return /\bexport\s+(?:declare\s+)?(?:interface|type|enum|const)\b/.test(content);
|
|
149
|
+
}
|
|
150
|
+
function packageScriptSuiteDirectories(graphEdges) {
|
|
151
|
+
const directories = new Set();
|
|
152
|
+
for (const edge of graphEdges) {
|
|
153
|
+
if (edge.kind !== "package-script-link") {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const directory = posix.dirname(normalizeGraphPath(edge.to));
|
|
157
|
+
const basename = posix.basename(directory);
|
|
158
|
+
if (basename === "scripts" || basename === "bin") {
|
|
159
|
+
directories.add(directory);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return directories;
|
|
163
|
+
}
|
|
164
|
+
function isPackageScriptSuitePath(path, suiteDirectories) {
|
|
165
|
+
const normalized = normalizeGraphPath(path);
|
|
166
|
+
return (suiteDirectories.has(posix.dirname(normalized)) &&
|
|
167
|
+
PACKAGE_SCRIPT_SUITE_EXTENSIONS.some((extension) => normalized.endsWith(extension)));
|
|
168
|
+
}
|
|
169
|
+
function isPythonTestUtilSuitePath(path) {
|
|
170
|
+
const normalized = normalizeGraphPath(path);
|
|
171
|
+
if (!normalized.endsWith(".py"))
|
|
172
|
+
return false;
|
|
173
|
+
if (isPytestConftestPath(normalized))
|
|
174
|
+
return false;
|
|
175
|
+
const dir = posix.dirname(normalized);
|
|
176
|
+
if (!PYTHON_TEST_UTIL_SEGMENT_NAMES.has(posix.basename(dir).toLowerCase()))
|
|
177
|
+
return false;
|
|
178
|
+
return isTestPath(normalizeExtractorPath(dir));
|
|
179
|
+
}
|
|
180
|
+
export function extractBoundedSuiteEdges(pathLookup, fileContents, graphEdges) {
|
|
181
|
+
const files = [...new Set(pathLookup.values())].sort((a, b) => a.localeCompare(b));
|
|
182
|
+
const edges = [];
|
|
183
|
+
const scriptSuiteDirectories = packageScriptSuiteDirectories(graphEdges);
|
|
184
|
+
const addSuiteEdges = (params) => {
|
|
185
|
+
const groups = new Map();
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
if (!params.predicate(file)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const directory = posix.dirname(normalizeGraphPath(file));
|
|
191
|
+
const group = groups.get(directory) ?? [];
|
|
192
|
+
group.push(file);
|
|
193
|
+
groups.set(directory, group);
|
|
194
|
+
}
|
|
195
|
+
for (const [directory, group] of groups) {
|
|
196
|
+
const maxFiles = params.maxFiles ?? MAX_BOUNDED_SUITE_EDGE_FILES;
|
|
197
|
+
if (group.length < 2 ||
|
|
198
|
+
group.length > maxFiles) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const suiteName = directory === "." ? "repository root" : directory;
|
|
202
|
+
for (let index = 1; index < group.length; index++) {
|
|
203
|
+
edges.push(graphEdge({
|
|
204
|
+
from: group[index - 1],
|
|
205
|
+
to: group[index],
|
|
206
|
+
kind: params.kind,
|
|
207
|
+
direction: "undirected",
|
|
208
|
+
confidence: params.confidence,
|
|
209
|
+
reason: `${params.label} suite '${suiteName}' groups ${group.length} related file(s).`,
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
addSuiteEdges({
|
|
215
|
+
predicate: isJsonSchemaPath,
|
|
216
|
+
kind: "schema-suite-link",
|
|
217
|
+
confidence: SCHEMA_SUITE_EDGE_CONFIDENCE,
|
|
218
|
+
label: "JSON Schema",
|
|
219
|
+
});
|
|
220
|
+
addSuiteEdges({
|
|
221
|
+
predicate: isGithubWorkflowPath,
|
|
222
|
+
kind: "github-workflow-suite-link",
|
|
223
|
+
confidence: GITHUB_WORKFLOW_SUITE_EDGE_CONFIDENCE,
|
|
224
|
+
label: "GitHub Actions workflow",
|
|
225
|
+
});
|
|
226
|
+
addSuiteEdges({
|
|
227
|
+
predicate: (path) => isPackageScriptSuitePath(path, scriptSuiteDirectories),
|
|
228
|
+
kind: "package-script-suite-link",
|
|
229
|
+
confidence: PACKAGE_SCRIPT_SUITE_EDGE_CONFIDENCE,
|
|
230
|
+
label: "Package script",
|
|
231
|
+
});
|
|
232
|
+
addSuiteEdges({
|
|
233
|
+
predicate: (path) => isTypescriptTypeContractPath(path, fileContents),
|
|
234
|
+
kind: "typescript-type-suite-link",
|
|
235
|
+
confidence: TYPESCRIPT_TYPE_SUITE_EDGE_CONFIDENCE,
|
|
236
|
+
label: "TypeScript type contract",
|
|
237
|
+
maxFiles: MAX_BOUNDED_TYPE_SUITE_EDGE_FILES,
|
|
238
|
+
});
|
|
239
|
+
addSuiteEdges({
|
|
240
|
+
predicate: isPythonTestUtilSuitePath,
|
|
241
|
+
kind: "python-test-util-suite-link",
|
|
242
|
+
confidence: PYTHON_TEST_UTIL_SUITE_EDGE_CONFIDENCE,
|
|
243
|
+
label: "Python test utility",
|
|
244
|
+
});
|
|
245
|
+
return edges;
|
|
246
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { graphEdge, normalizeGraphPath, resolveCandidate, SOURCE_EXTENSIONS, } from "./graphPathUtils.js";
|
|
3
|
+
import { isTestPath, normalizeExtractorPath } from "./pathPatterns.js";
|
|
4
|
+
import { isPythonSourcePath } from "./graphPythonImports.js";
|
|
5
|
+
const TEST_SOURCE_EDGE_CONFIDENCE = 0.88;
|
|
6
|
+
const TOP_LEVEL_TEST_SEGMENTS = new Set(["test", "tests", "spec", "specs"]);
|
|
7
|
+
const COLOCATED_TEST_SEGMENTS = new Set([
|
|
8
|
+
"__test__",
|
|
9
|
+
"__tests__",
|
|
10
|
+
"__spec__",
|
|
11
|
+
"__specs__",
|
|
12
|
+
"test",
|
|
13
|
+
"tests",
|
|
14
|
+
"spec",
|
|
15
|
+
"specs",
|
|
16
|
+
]);
|
|
17
|
+
function stripKnownSourceExtension(path) {
|
|
18
|
+
const lowerPath = path.toLowerCase();
|
|
19
|
+
const extension = SOURCE_EXTENSIONS.find((item) => lowerPath.endsWith(item));
|
|
20
|
+
if (!extension) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return path.slice(0, -extension.length);
|
|
24
|
+
}
|
|
25
|
+
function stripTestSuffix(pathWithoutExtension) {
|
|
26
|
+
const stripped = pathWithoutExtension.replace(/[._-](?:test|spec)$/i, "");
|
|
27
|
+
return stripped === pathWithoutExtension ? undefined : stripped;
|
|
28
|
+
}
|
|
29
|
+
function stripPythonTestPrefix(pathWithoutExtension) {
|
|
30
|
+
const basename = posix.basename(pathWithoutExtension);
|
|
31
|
+
const match = /^test[._-](.+)$/i.exec(basename);
|
|
32
|
+
if (!match?.[1]) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const directory = posix.dirname(pathWithoutExtension);
|
|
36
|
+
return directory === "." ? match[1] : posix.join(directory, match[1]);
|
|
37
|
+
}
|
|
38
|
+
function addTestSourceCandidatesForBase(basePath, candidates) {
|
|
39
|
+
candidates.add(basePath);
|
|
40
|
+
const parts = basePath.split("/").filter(Boolean);
|
|
41
|
+
const topLevelSegment = parts[0]?.toLowerCase();
|
|
42
|
+
if (topLevelSegment && TOP_LEVEL_TEST_SEGMENTS.has(topLevelSegment)) {
|
|
43
|
+
const mirroredParts = parts.slice(1);
|
|
44
|
+
if (mirroredParts.length > 0) {
|
|
45
|
+
candidates.add(posix.join("src", ...mirroredParts));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (let index = 1; index < parts.length; index++) {
|
|
49
|
+
if (COLOCATED_TEST_SEGMENTS.has(parts[index].toLowerCase())) {
|
|
50
|
+
const colocatedParts = [
|
|
51
|
+
...parts.slice(0, index),
|
|
52
|
+
...parts.slice(index + 1),
|
|
53
|
+
];
|
|
54
|
+
if (colocatedParts.length > 0) {
|
|
55
|
+
candidates.add(posix.join(...colocatedParts));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function testSourceCandidates(testPath) {
|
|
61
|
+
const normalizedPath = normalizeGraphPath(testPath);
|
|
62
|
+
const withoutExtension = stripKnownSourceExtension(normalizedPath);
|
|
63
|
+
if (!withoutExtension) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const baseCandidates = new Set();
|
|
67
|
+
const withoutTestSuffix = stripTestSuffix(withoutExtension);
|
|
68
|
+
if (withoutTestSuffix) {
|
|
69
|
+
baseCandidates.add(withoutTestSuffix);
|
|
70
|
+
}
|
|
71
|
+
if (isPythonSourcePath(normalizedPath)) {
|
|
72
|
+
const withoutPythonPrefix = stripPythonTestPrefix(withoutExtension);
|
|
73
|
+
if (withoutPythonPrefix) {
|
|
74
|
+
baseCandidates.add(withoutPythonPrefix);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const candidates = new Set();
|
|
78
|
+
for (const basePath of baseCandidates) {
|
|
79
|
+
addTestSourceCandidatesForBase(basePath, candidates);
|
|
80
|
+
}
|
|
81
|
+
return [...candidates];
|
|
82
|
+
}
|
|
83
|
+
export function extractTestSourceEdges(fromPath, pathLookup) {
|
|
84
|
+
if (!isTestPath(normalizeExtractorPath(fromPath))) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const edges = [];
|
|
88
|
+
for (const candidate of testSourceCandidates(fromPath)) {
|
|
89
|
+
const target = resolveCandidate(candidate, pathLookup);
|
|
90
|
+
if (!target || isTestPath(normalizeExtractorPath(target))) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
edges.push(graphEdge({
|
|
94
|
+
from: fromPath,
|
|
95
|
+
to: target,
|
|
96
|
+
kind: "test-source-link",
|
|
97
|
+
confidence: TEST_SOURCE_EDGE_CONFIDENCE,
|
|
98
|
+
reason: `Test path naming maps to source path '${target}'.`,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
return edges;
|
|
102
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { spawnLoggedCommand } from "@audit-tools/shared";
|
|
2
|
+
import { readJsonFile, spawnLoggedCommand, applyWorkerTaskLaunchSettings } from "@audit-tools/shared";
|
|
3
3
|
export const ACTIVE_CLAUDE_CODE_SESSION_MESSAGE = "claude-code provider cannot be used inside an active Claude Code session. " +
|
|
4
4
|
'Set provider to "local-subprocess" in .audit-artifacts/session-config.json, ' +
|
|
5
5
|
"then run /audit-code conversationally and follow the dispatch prompts manually.";
|
|
@@ -18,6 +18,7 @@ export class ClaudeCodeProvider {
|
|
|
18
18
|
throw new Error(ACTIVE_CLAUDE_CODE_SESSION_MESSAGE);
|
|
19
19
|
}
|
|
20
20
|
const prompt = await readFile(input.promptPath, "utf8");
|
|
21
|
+
const task = await readJsonFile(input.taskPath);
|
|
21
22
|
const command = this.config.command ?? "claude";
|
|
22
23
|
const promptFlag = this.config.prompt_flag ?? "-p";
|
|
23
24
|
const args = [
|
|
@@ -28,7 +29,7 @@ export class ClaudeCodeProvider {
|
|
|
28
29
|
? ["--dangerously-skip-permissions"]
|
|
29
30
|
: []),
|
|
30
31
|
];
|
|
31
|
-
return await this.launchCommand(command, args, input, undefined, {
|
|
32
|
+
return await this.launchCommand(command, args, applyWorkerTaskLaunchSettings(input, task), undefined, {
|
|
32
33
|
opentoken: this.opentoken.enabled,
|
|
33
34
|
opentokenCommand: this.opentoken.command,
|
|
34
35
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readJsonFile } from "@audit-tools/shared";
|
|
2
|
-
import { spawnLoggedCommand } from "@audit-tools/shared";
|
|
2
|
+
import { spawnLoggedCommand, applyWorkerTaskLaunchSettings } from "@audit-tools/shared";
|
|
3
3
|
export const MISSING_WORKER_COMMAND_MESSAGE = "local-subprocess provider requires task.worker_command.";
|
|
4
4
|
export class LocalSubprocessProvider {
|
|
5
5
|
name = "local-subprocess";
|
|
@@ -13,6 +13,6 @@ export class LocalSubprocessProvider {
|
|
|
13
13
|
throw new Error(MISSING_WORKER_COMMAND_MESSAGE);
|
|
14
14
|
}
|
|
15
15
|
const [command, ...args] = task.worker_command;
|
|
16
|
-
return await this.launchCommand(command, args, input);
|
|
16
|
+
return await this.launchCommand(command, args, applyWorkerTaskLaunchSettings(input, task));
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1,24 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { spawnLoggedCommand } from "@audit-tools/shared";
|
|
3
|
-
function resolveOpenCodeSpawnCommand(command, args, platform = process.platform, shellCommand = process.env.ComSpec ?? "cmd.exe") {
|
|
4
|
-
if (platform !== "win32") {
|
|
5
|
-
return { command, args };
|
|
6
|
-
}
|
|
7
|
-
const base = command.replace(/\.(cmd|bat|exe)$/i, "").toLowerCase();
|
|
8
|
-
if (base === "opencode" || base === "npx" || command.endsWith(".cmd")) {
|
|
9
|
-
return {
|
|
10
|
-
command: shellCommand,
|
|
11
|
-
args: ["/d", "/s", "/c", [command, ...args].map(quoteCmdArg).join(" ")],
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
return { command, args };
|
|
15
|
-
}
|
|
16
|
-
function quoteCmdArg(value) {
|
|
17
|
-
if (/^[A-Za-z0-9_./:=+-]+$/.test(value)) {
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
return `"${value.replace(/(["^&|<>%])/g, "^$1")}"`;
|
|
21
|
-
}
|
|
2
|
+
import { readJsonFile, spawnLoggedCommand, resolveOpenCodeSpawnCommand, applyWorkerTaskLaunchSettings, } from "@audit-tools/shared";
|
|
22
3
|
export class OpenCodeProvider {
|
|
23
4
|
name = "opencode";
|
|
24
5
|
config;
|
|
@@ -29,10 +10,13 @@ export class OpenCodeProvider {
|
|
|
29
10
|
}
|
|
30
11
|
async launch(input) {
|
|
31
12
|
const prompt = await readFile(input.promptPath, "utf8");
|
|
13
|
+
const task = await readJsonFile(input.taskPath);
|
|
32
14
|
const baseCommand = this.config.command ?? "opencode";
|
|
33
15
|
const baseArgs = ["run", prompt, ...(this.config.extra_args ?? [])];
|
|
34
|
-
|
|
35
|
-
|
|
16
|
+
// On Windows the `opencode` launcher is a `.cmd` shim that `spawn` cannot
|
|
17
|
+
// run without a shell; resolve it through cmd.exe (no-op on other OSes).
|
|
18
|
+
const { command, args } = resolveOpenCodeSpawnCommand(baseCommand, baseArgs);
|
|
19
|
+
return await spawnLoggedCommand(command, args, applyWorkerTaskLaunchSettings(input, task), undefined, {
|
|
36
20
|
opentoken: this.opentoken.enabled,
|
|
37
21
|
opentokenCommand: this.opentoken.command,
|
|
38
22
|
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { readJsonFile } from "@audit-tools/shared";
|
|
2
|
-
import { spawnLoggedCommand } from "@audit-tools/shared";
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const workerCommandShell = task.worker_command.map(shellQuote).join(" ");
|
|
2
|
+
import { spawnLoggedCommand, shellQuote, applyWorkerTaskLaunchSettings, } from "@audit-tools/shared";
|
|
3
|
+
function applyTemplate(template, input, task, context) {
|
|
4
|
+
const workerCommandShell = task.worker_command
|
|
5
|
+
.map((arg) => shellQuote(arg))
|
|
6
|
+
.join(" ");
|
|
8
7
|
const workerCommandJson = JSON.stringify(task.worker_command);
|
|
9
8
|
const values = {
|
|
10
9
|
repoRoot: input.repoRoot,
|
|
@@ -20,7 +19,17 @@ function applyTemplate(template, input, task) {
|
|
|
20
19
|
uiMode: input.uiMode,
|
|
21
20
|
timeoutMs: String(input.timeoutMs),
|
|
22
21
|
};
|
|
23
|
-
|
|
22
|
+
const wholePlaceholder = template.match(/^\{([A-Za-z0-9_]+)\}$/);
|
|
23
|
+
return template.replace(/\{([A-Za-z0-9_]+)\}/g, (match, key) => {
|
|
24
|
+
if (!(key in values)) {
|
|
25
|
+
console.warn(`applyTemplate: unknown placeholder ${match} ` +
|
|
26
|
+
`provider=${context.providerName} runId=${input.runId} ` +
|
|
27
|
+
`obligationId=${input.obligationId ?? ""} taskPath=${input.taskPath} ` +
|
|
28
|
+
`entryIndex=${context.entryIndex}`);
|
|
29
|
+
}
|
|
30
|
+
const value = values[key] ?? "";
|
|
31
|
+
return wholePlaceholder || key.endsWith("Shell") ? value : shellQuote(value);
|
|
32
|
+
});
|
|
24
33
|
}
|
|
25
34
|
export class SubprocessTemplateProvider {
|
|
26
35
|
name;
|
|
@@ -36,9 +45,13 @@ export class SubprocessTemplateProvider {
|
|
|
36
45
|
if (!this.config.command_template.length) {
|
|
37
46
|
throw new Error(`${this.name} provider requires a non-empty command_template.`);
|
|
38
47
|
}
|
|
39
|
-
const
|
|
48
|
+
const launchInput = applyWorkerTaskLaunchSettings(input, task);
|
|
49
|
+
const rendered = this.config.command_template.map((entry, entryIndex) => applyTemplate(entry, launchInput, task, {
|
|
50
|
+
providerName: this.name,
|
|
51
|
+
entryIndex,
|
|
52
|
+
}));
|
|
40
53
|
const [command, ...args] = rendered;
|
|
41
|
-
return await spawnLoggedCommand(command, args,
|
|
54
|
+
return await spawnLoggedCommand(command, args, launchInput, this.config.env, {
|
|
42
55
|
opentoken: this.opentoken.enabled,
|
|
43
56
|
opentokenCommand: this.opentoken.command,
|
|
44
57
|
});
|