@pushpalsdev/cli 1.0.80 → 1.0.82
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/monitor-ui/+not-found.html +1 -1
- package/monitor-ui/_expo/static/js/web/{entry-c6862f701ea52ccf8692a6c9e749af5c.js → entry-e66b4de45f75e702ac16916082bcc9a5.js} +172 -171
- package/monitor-ui/_expo/static/js/web/{index-6013f9ebc87a963a55bb9137af1a5a06.js → index-ec13ec62e2b37ed3c5f6d324ef6784e1.js} +4 -4
- package/monitor-ui/_sitemap.html +1 -1
- package/monitor-ui/index.html +1 -1
- package/monitor-ui/modal.html +1 -1
- package/package.json +1 -1
- package/runtime/sandbox/.pushpals-remotebuddy-fallback.js +45 -5
- package/runtime/sandbox/apps/workerpals/Dockerfile.sandbox +5 -0
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +158 -0
- package/runtime/sandbox/packages/shared/src/index.ts +11 -0
- package/runtime/sandbox/packages/shared/src/toolchain.ts +509 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
2
|
+
import { join, normalize } from "path";
|
|
3
|
+
|
|
4
|
+
export type ToolchainEnvironmentSource =
|
|
5
|
+
| "devcontainer"
|
|
6
|
+
| "dockerfile"
|
|
7
|
+
| "mise"
|
|
8
|
+
| "asdf"
|
|
9
|
+
| "nix"
|
|
10
|
+
| "pushpals-default-sandbox";
|
|
11
|
+
|
|
12
|
+
export interface ToolRequirement {
|
|
13
|
+
tool: string;
|
|
14
|
+
candidates: string[];
|
|
15
|
+
reason: string;
|
|
16
|
+
detectedFrom: string;
|
|
17
|
+
requiredFor: string[];
|
|
18
|
+
optional?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ToolchainPlan {
|
|
22
|
+
requirements: ToolRequirement[];
|
|
23
|
+
environmentSource: ToolchainEnvironmentSource;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BuildToolchainPlanOptions {
|
|
27
|
+
repoRoot: string;
|
|
28
|
+
validationCommands: string[];
|
|
29
|
+
maxNativeScanEntries?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SHELL_CONTROL_TOKENS = new Set(["|", "||", "&", "&&", ";", ">", ">>", "<", "<<"]);
|
|
33
|
+
|
|
34
|
+
const NODE_BACKED_CLI_NAMES = new Set([
|
|
35
|
+
"astro",
|
|
36
|
+
"babel",
|
|
37
|
+
"cypress",
|
|
38
|
+
"eslint",
|
|
39
|
+
"expo",
|
|
40
|
+
"jest",
|
|
41
|
+
"metro",
|
|
42
|
+
"next",
|
|
43
|
+
"nuxt",
|
|
44
|
+
"playwright",
|
|
45
|
+
"react-native",
|
|
46
|
+
"rollup",
|
|
47
|
+
"tsc",
|
|
48
|
+
"tsx",
|
|
49
|
+
"vite",
|
|
50
|
+
"vitest",
|
|
51
|
+
"webpack",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const DIRECT_TOOL_CANDIDATES: Record<string, string[]> = {
|
|
55
|
+
bash: ["bash"],
|
|
56
|
+
bun: ["bun"],
|
|
57
|
+
bunx: ["bun"],
|
|
58
|
+
cargo: ["cargo"],
|
|
59
|
+
cc: ["cc"],
|
|
60
|
+
clang: ["clang"],
|
|
61
|
+
"clang++": ["clang++"],
|
|
62
|
+
cmake: ["cmake"],
|
|
63
|
+
cypress: ["cypress"],
|
|
64
|
+
docker: ["docker"],
|
|
65
|
+
eslint: ["eslint"],
|
|
66
|
+
expo: ["expo"],
|
|
67
|
+
gcc: ["gcc"],
|
|
68
|
+
"g++": ["g++"],
|
|
69
|
+
gh: ["gh"],
|
|
70
|
+
go: ["go"],
|
|
71
|
+
java: ["java"],
|
|
72
|
+
javac: ["javac"],
|
|
73
|
+
make: ["make"],
|
|
74
|
+
mvn: ["mvn"],
|
|
75
|
+
next: ["next"],
|
|
76
|
+
ninja: ["ninja"],
|
|
77
|
+
node: ["node"],
|
|
78
|
+
npm: ["npm"],
|
|
79
|
+
npx: ["npx"],
|
|
80
|
+
playwright: ["playwright"],
|
|
81
|
+
pnpm: ["pnpm"],
|
|
82
|
+
powershell: ["powershell"],
|
|
83
|
+
pwsh: ["pwsh"],
|
|
84
|
+
python: ["python3", "python", "py"],
|
|
85
|
+
python3: ["python3", "python"],
|
|
86
|
+
pytest: ["python3", "python", "py"],
|
|
87
|
+
rustc: ["rustc"],
|
|
88
|
+
sh: ["sh"],
|
|
89
|
+
tsc: ["tsc"],
|
|
90
|
+
vite: ["vite"],
|
|
91
|
+
vitest: ["vitest"],
|
|
92
|
+
yarn: ["yarn"],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
interface NativeSignals {
|
|
96
|
+
hasC: boolean;
|
|
97
|
+
hasCxx: boolean;
|
|
98
|
+
hasMakefile: boolean;
|
|
99
|
+
hasCMake: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function tokenizeToolchainCommand(command: string): string[] | null {
|
|
103
|
+
const input = command.trim();
|
|
104
|
+
if (!input) return null;
|
|
105
|
+
const out: string[] = [];
|
|
106
|
+
let current = "";
|
|
107
|
+
let quote: "'" | '"' | null = null;
|
|
108
|
+
|
|
109
|
+
const pushCurrent = () => {
|
|
110
|
+
const trimmed = current.trim();
|
|
111
|
+
if (trimmed) out.push(trimmed);
|
|
112
|
+
current = "";
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
116
|
+
const ch = input[index] ?? "";
|
|
117
|
+
if (quote) {
|
|
118
|
+
if (ch === quote) {
|
|
119
|
+
quote = null;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
current += ch;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (ch === "'" || ch === '"') {
|
|
126
|
+
quote = ch;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (/\s/.test(ch)) {
|
|
130
|
+
pushCurrent();
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
current += ch;
|
|
134
|
+
}
|
|
135
|
+
if (quote) return null;
|
|
136
|
+
pushCurrent();
|
|
137
|
+
if (out.length === 0) return null;
|
|
138
|
+
if (out.some((token) => SHELL_CONTROL_TOKENS.has(token))) return null;
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function buildToolchainPlan(options: BuildToolchainPlanOptions): ToolchainPlan {
|
|
143
|
+
const repoRoot = options.repoRoot;
|
|
144
|
+
const nativeSignals = detectNativeSignals(repoRoot, options.maxNativeScanEntries ?? 1_000);
|
|
145
|
+
const requirements: ToolRequirement[] = [];
|
|
146
|
+
for (const command of options.validationCommands) {
|
|
147
|
+
requirements.push(...inferToolRequirementsForValidationCommand(repoRoot, command, nativeSignals));
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
requirements: dedupeToolRequirements(requirements),
|
|
151
|
+
environmentSource: detectToolchainEnvironmentSource(repoRoot),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function inferToolRequirementsForValidationCommand(
|
|
156
|
+
repoRoot: string,
|
|
157
|
+
command: string,
|
|
158
|
+
nativeSignals: NativeSignals = detectNativeSignals(repoRoot),
|
|
159
|
+
): ToolRequirement[] {
|
|
160
|
+
const tokens = tokenizeToolchainCommand(command);
|
|
161
|
+
if (!tokens) return [];
|
|
162
|
+
const requirements: ToolRequirement[] = [];
|
|
163
|
+
const first = normalizeToolToken(tokens[0] ?? "");
|
|
164
|
+
|
|
165
|
+
addDirectExecutableRequirement(requirements, first, command);
|
|
166
|
+
addNodeBackedCliRequirement(requirements, first, `validation command "${command}"`, command);
|
|
167
|
+
|
|
168
|
+
const bunSubcommand = resolveBunSubcommand(tokens);
|
|
169
|
+
if (bunSubcommand?.kind === "x") {
|
|
170
|
+
addNodeBackedCliRequirement(
|
|
171
|
+
requirements,
|
|
172
|
+
normalizeToolToken(bunSubcommand.value),
|
|
173
|
+
`bun x package "${bunSubcommand.value}"`,
|
|
174
|
+
command,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const script = resolvePackageScript(repoRoot, tokens);
|
|
179
|
+
if (script) {
|
|
180
|
+
addScriptRequirements(requirements, script.script, script.detectedFrom, command);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (usesNativeBuildCommand(tokens)) {
|
|
184
|
+
if (nativeSignals.hasC) {
|
|
185
|
+
requirements.push({
|
|
186
|
+
tool: "c-compiler",
|
|
187
|
+
candidates: ["cc", "gcc", "clang"],
|
|
188
|
+
reason: "native C sources may be compiled by this validation command",
|
|
189
|
+
detectedFrom: nativeSignals.hasCMake
|
|
190
|
+
? "CMakeLists.txt/native source scan"
|
|
191
|
+
: "Makefile/native source scan",
|
|
192
|
+
requiredFor: [command],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (nativeSignals.hasCxx) {
|
|
196
|
+
requirements.push({
|
|
197
|
+
tool: "cxx-compiler",
|
|
198
|
+
candidates: ["c++", "g++", "clang++"],
|
|
199
|
+
reason: "native C++ sources may be compiled by this validation command",
|
|
200
|
+
detectedFrom: nativeSignals.hasCMake
|
|
201
|
+
? "CMakeLists.txt/native source scan"
|
|
202
|
+
: "Makefile/native source scan",
|
|
203
|
+
requiredFor: [command],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return dedupeToolRequirements(requirements);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function requirementsForValidationCommand(
|
|
212
|
+
plan: ToolchainPlan,
|
|
213
|
+
command: string,
|
|
214
|
+
): ToolRequirement[] {
|
|
215
|
+
return plan.requirements.filter((requirement) => requirement.requiredFor.includes(command));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function formatToolRequirement(requirement: ToolRequirement): string {
|
|
219
|
+
const candidates =
|
|
220
|
+
requirement.candidates.length === 1
|
|
221
|
+
? requirement.candidates[0]
|
|
222
|
+
: `${requirement.tool} (${requirement.candidates.join(" or ")})`;
|
|
223
|
+
return `${candidates} from ${requirement.detectedFrom}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function addDirectExecutableRequirement(
|
|
227
|
+
requirements: ToolRequirement[],
|
|
228
|
+
tool: string,
|
|
229
|
+
command: string,
|
|
230
|
+
): void {
|
|
231
|
+
const candidates = DIRECT_TOOL_CANDIDATES[tool];
|
|
232
|
+
if (!candidates) return;
|
|
233
|
+
requirements.push({
|
|
234
|
+
tool: canonicalToolName(tool),
|
|
235
|
+
candidates,
|
|
236
|
+
reason: `validation command invokes ${tool}`,
|
|
237
|
+
detectedFrom: `validation command "${command}"`,
|
|
238
|
+
requiredFor: [command],
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function addNodeBackedCliRequirement(
|
|
243
|
+
requirements: ToolRequirement[],
|
|
244
|
+
cliName: string,
|
|
245
|
+
detectedFrom: string,
|
|
246
|
+
command: string,
|
|
247
|
+
): void {
|
|
248
|
+
if (!NODE_BACKED_CLI_NAMES.has(cliName)) return;
|
|
249
|
+
requirements.push({
|
|
250
|
+
tool: "node",
|
|
251
|
+
candidates: ["node"],
|
|
252
|
+
reason: `${cliName} is normally distributed as a Node.js CLI`,
|
|
253
|
+
detectedFrom,
|
|
254
|
+
requiredFor: [command],
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function addScriptRequirements(
|
|
259
|
+
requirements: ToolRequirement[],
|
|
260
|
+
script: string,
|
|
261
|
+
detectedFrom: string,
|
|
262
|
+
command: string,
|
|
263
|
+
): void {
|
|
264
|
+
const tokens = tokenizeToolchainCommand(script) ?? script.split(/\s+/).filter(Boolean);
|
|
265
|
+
const first = normalizeToolToken(tokens[0] ?? "");
|
|
266
|
+
addDirectExecutableRequirement(requirements, first, command);
|
|
267
|
+
addNodeBackedCliRequirement(requirements, first, detectedFrom, command);
|
|
268
|
+
for (const token of tokens) {
|
|
269
|
+
addNodeBackedCliRequirement(requirements, normalizeToolToken(token), detectedFrom, command);
|
|
270
|
+
}
|
|
271
|
+
if (/\bnode\b/.test(script)) {
|
|
272
|
+
requirements.push({
|
|
273
|
+
tool: "node",
|
|
274
|
+
candidates: ["node"],
|
|
275
|
+
reason: "package script invokes node directly",
|
|
276
|
+
detectedFrom,
|
|
277
|
+
requiredFor: [command],
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (/\bbun\b/.test(script)) {
|
|
281
|
+
requirements.push({
|
|
282
|
+
tool: "bun",
|
|
283
|
+
candidates: ["bun"],
|
|
284
|
+
reason: "package script invokes bun",
|
|
285
|
+
detectedFrom,
|
|
286
|
+
requiredFor: [command],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function resolveBunSubcommand(tokens: string[]): { kind: "run" | "x"; value: string } | null {
|
|
292
|
+
if (normalizeToolToken(tokens[0] ?? "") !== "bun") return null;
|
|
293
|
+
let index = 1;
|
|
294
|
+
while (index < tokens.length) {
|
|
295
|
+
const token = tokens[index] ?? "";
|
|
296
|
+
if (token === "--cwd" || token === "-C") {
|
|
297
|
+
index += 2;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (token.startsWith("--")) {
|
|
301
|
+
index += 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
const subcommand = normalizeToolToken(tokens[index] ?? "");
|
|
307
|
+
if ((subcommand === "run" || subcommand === "x") && tokens[index + 1]) {
|
|
308
|
+
return { kind: subcommand, value: tokens[index + 1] ?? "" };
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function resolvePackageScript(
|
|
314
|
+
repoRoot: string,
|
|
315
|
+
tokens: string[],
|
|
316
|
+
): { script: string; detectedFrom: string } | null {
|
|
317
|
+
const first = normalizeToolToken(tokens[0] ?? "");
|
|
318
|
+
let cwd = repoRoot;
|
|
319
|
+
let scriptName = "";
|
|
320
|
+
if (first === "bun") {
|
|
321
|
+
let index = 1;
|
|
322
|
+
while (index < tokens.length) {
|
|
323
|
+
const token = tokens[index] ?? "";
|
|
324
|
+
if ((token === "--cwd" || token === "-C") && tokens[index + 1]) {
|
|
325
|
+
cwd = join(repoRoot, tokens[index + 1] ?? "");
|
|
326
|
+
index += 2;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (token.startsWith("--")) {
|
|
330
|
+
index += 1;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
if (normalizeToolToken(tokens[index] ?? "") === "run") {
|
|
336
|
+
scriptName = tokens[index + 1] ?? "";
|
|
337
|
+
} else {
|
|
338
|
+
const candidate = tokens[index] ?? "";
|
|
339
|
+
if (candidate && !["install", "test", "x"].includes(normalizeToolToken(candidate))) {
|
|
340
|
+
scriptName = candidate;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} else if (first === "npm" || first === "pnpm" || first === "yarn") {
|
|
344
|
+
let index = 1;
|
|
345
|
+
while (index < tokens.length) {
|
|
346
|
+
const token = tokens[index] ?? "";
|
|
347
|
+
const normalized = normalizeToolToken(token);
|
|
348
|
+
if (
|
|
349
|
+
(token === "--prefix" ||
|
|
350
|
+
token === "--dir" ||
|
|
351
|
+
token === "--cwd" ||
|
|
352
|
+
token === "-C") &&
|
|
353
|
+
tokens[index + 1]
|
|
354
|
+
) {
|
|
355
|
+
cwd = join(repoRoot, tokens[index + 1] ?? "");
|
|
356
|
+
index += 2;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (normalized === "run") {
|
|
360
|
+
scriptName = tokens[index + 1] ?? "";
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
if (!token.startsWith("-")) {
|
|
364
|
+
scriptName = normalized;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
index += 1;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (!scriptName) return null;
|
|
371
|
+
|
|
372
|
+
const packagePath = join(cwd, "package.json");
|
|
373
|
+
if (!existsSync(packagePath)) return null;
|
|
374
|
+
try {
|
|
375
|
+
const parsed = JSON.parse(readFileSync(packagePath, "utf8")) as {
|
|
376
|
+
scripts?: Record<string, unknown>;
|
|
377
|
+
};
|
|
378
|
+
const script = parsed.scripts?.[scriptName];
|
|
379
|
+
if (typeof script !== "string" || !script.trim()) return null;
|
|
380
|
+
return {
|
|
381
|
+
script,
|
|
382
|
+
detectedFrom: `${repoRelativePath(repoRoot, packagePath)} script "${scriptName}"`,
|
|
383
|
+
};
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function detectToolchainEnvironmentSource(repoRoot: string): ToolchainEnvironmentSource {
|
|
390
|
+
if (existsSync(join(repoRoot, ".devcontainer", "devcontainer.json"))) return "devcontainer";
|
|
391
|
+
if (existsSync(join(repoRoot, "devcontainer.json"))) return "devcontainer";
|
|
392
|
+
if (existsSync(join(repoRoot, "Dockerfile"))) return "dockerfile";
|
|
393
|
+
if (existsSync(join(repoRoot, "mise.toml")) || existsSync(join(repoRoot, ".mise.toml"))) {
|
|
394
|
+
return "mise";
|
|
395
|
+
}
|
|
396
|
+
if (existsSync(join(repoRoot, ".tool-versions"))) return "asdf";
|
|
397
|
+
if (existsSync(join(repoRoot, "flake.nix")) || existsSync(join(repoRoot, "shell.nix"))) {
|
|
398
|
+
return "nix";
|
|
399
|
+
}
|
|
400
|
+
return "pushpals-default-sandbox";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function detectNativeSignals(repoRoot: string, maxEntries = 1_000): NativeSignals {
|
|
404
|
+
const signals: NativeSignals = {
|
|
405
|
+
hasC: false,
|
|
406
|
+
hasCxx: false,
|
|
407
|
+
hasMakefile:
|
|
408
|
+
existsSync(join(repoRoot, "Makefile")) ||
|
|
409
|
+
existsSync(join(repoRoot, "makefile")) ||
|
|
410
|
+
existsSync(join(repoRoot, "GNUmakefile")),
|
|
411
|
+
hasCMake: existsSync(join(repoRoot, "CMakeLists.txt")),
|
|
412
|
+
};
|
|
413
|
+
const ignored = new Set([
|
|
414
|
+
".git",
|
|
415
|
+
".worktrees",
|
|
416
|
+
"node_modules",
|
|
417
|
+
"outputs",
|
|
418
|
+
"dist",
|
|
419
|
+
"build",
|
|
420
|
+
".next",
|
|
421
|
+
".expo",
|
|
422
|
+
]);
|
|
423
|
+
let visited = 0;
|
|
424
|
+
const scan = (dir: string, depth: number) => {
|
|
425
|
+
if (visited >= maxEntries || depth > 4 || (signals.hasC && signals.hasCxx)) return;
|
|
426
|
+
let entries: string[] = [];
|
|
427
|
+
try {
|
|
428
|
+
entries = readdirSync(dir);
|
|
429
|
+
} catch {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
for (const entry of entries) {
|
|
433
|
+
if (visited >= maxEntries) return;
|
|
434
|
+
if (ignored.has(entry)) continue;
|
|
435
|
+
const fullPath = join(dir, entry);
|
|
436
|
+
visited += 1;
|
|
437
|
+
let stats;
|
|
438
|
+
try {
|
|
439
|
+
stats = statSync(fullPath);
|
|
440
|
+
} catch {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (stats.isDirectory()) {
|
|
444
|
+
scan(fullPath, depth + 1);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const lower = entry.toLowerCase();
|
|
448
|
+
if (/\.(c|h)$/.test(lower)) signals.hasC = true;
|
|
449
|
+
if (/\.(cc|cpp|cxx|hpp|hh|hxx)$/.test(lower)) signals.hasCxx = true;
|
|
450
|
+
if (lower === "cmakelists.txt") signals.hasCMake = true;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
scan(repoRoot, 0);
|
|
454
|
+
return signals;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function usesNativeBuildCommand(tokens: string[]): boolean {
|
|
458
|
+
return tokens.some((token) => {
|
|
459
|
+
const normalized = normalizeToolToken(token);
|
|
460
|
+
return normalized === "make" || normalized === "cmake" || normalized === "ninja";
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function dedupeToolRequirements(requirements: ToolRequirement[]): ToolRequirement[] {
|
|
465
|
+
const merged = new Map<string, ToolRequirement>();
|
|
466
|
+
for (const requirement of requirements) {
|
|
467
|
+
const key = requirement.tool;
|
|
468
|
+
const existing = merged.get(key);
|
|
469
|
+
if (!existing) {
|
|
470
|
+
merged.set(key, {
|
|
471
|
+
...requirement,
|
|
472
|
+
candidates: Array.from(new Set(requirement.candidates)),
|
|
473
|
+
requiredFor: Array.from(new Set(requirement.requiredFor)),
|
|
474
|
+
});
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
for (const candidate of requirement.candidates) {
|
|
478
|
+
if (!existing.candidates.includes(candidate)) existing.candidates.push(candidate);
|
|
479
|
+
}
|
|
480
|
+
for (const command of requirement.requiredFor) {
|
|
481
|
+
if (!existing.requiredFor.includes(command)) existing.requiredFor.push(command);
|
|
482
|
+
}
|
|
483
|
+
if (!existing.detectedFrom.includes(requirement.detectedFrom)) {
|
|
484
|
+
existing.detectedFrom = `${existing.detectedFrom}; ${requirement.detectedFrom}`;
|
|
485
|
+
}
|
|
486
|
+
if (!existing.reason.includes(requirement.reason)) {
|
|
487
|
+
existing.reason = `${existing.reason}; ${requirement.reason}`;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return Array.from(merged.values()).sort((a, b) => a.tool.localeCompare(b.tool));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function canonicalToolName(tool: string): string {
|
|
494
|
+
if (tool === "bunx") return "bun";
|
|
495
|
+
if (tool === "python3" || tool === "pytest") return "python";
|
|
496
|
+
return tool;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function normalizeToolToken(token: string): string {
|
|
500
|
+
const normalizedToken = token.trim().replace(/\\/g, "/").split("/").pop() ?? token;
|
|
501
|
+
return normalizedToken.toLowerCase().replace(/\.(cmd|exe|ps1)$/i, "");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function repoRelativePath(repoRoot: string, pathValue: string): string {
|
|
505
|
+
const root = normalize(repoRoot).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
506
|
+
const path = normalize(pathValue).replace(/\\/g, "/");
|
|
507
|
+
if (path.startsWith(`${root}/`)) return path.slice(root.length + 1);
|
|
508
|
+
return path;
|
|
509
|
+
}
|