@pushpalsdev/cli 1.0.82 → 1.0.84
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/package.json
CHANGED
|
@@ -1763,6 +1763,16 @@ var NODE_BACKED_CLI_NAMES = new Set([
|
|
|
1763
1763
|
"vitest",
|
|
1764
1764
|
"webpack"
|
|
1765
1765
|
]);
|
|
1766
|
+
var BUN_OPTIONS_WITH_VALUE = new Set(["--cwd", "-C"]);
|
|
1767
|
+
var PACKAGE_MANAGER_OPTIONS_WITH_VALUE = new Set([
|
|
1768
|
+
"--cwd",
|
|
1769
|
+
"--dir",
|
|
1770
|
+
"--filter",
|
|
1771
|
+
"--prefix",
|
|
1772
|
+
"--workspace",
|
|
1773
|
+
"-C",
|
|
1774
|
+
"-F"
|
|
1775
|
+
]);
|
|
1766
1776
|
// packages/shared/src/session_event_visibility.ts
|
|
1767
1777
|
var ALWAYS_VISIBLE_EVENT_TYPES = new Set(["question_asked"]);
|
|
1768
1778
|
// packages/shared/src/localbuddy_runtime.ts
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Used by both the host Worker (direct mode) and the Docker job runner.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync, rmSync, unlinkSync } from "fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync } from "fs";
|
|
7
|
+
import { tmpdir } from "os";
|
|
7
8
|
import { resolve } from "path";
|
|
8
9
|
import {
|
|
9
10
|
deriveAutonomyComponentArea,
|
|
@@ -556,6 +557,7 @@ async function runValidationCommand(
|
|
|
556
557
|
const startedAt = Date.now();
|
|
557
558
|
const proc = Bun.spawn(argv, {
|
|
558
559
|
cwd: repo,
|
|
560
|
+
env: buildValidationCommandEnv(repo),
|
|
559
561
|
stdout: "pipe",
|
|
560
562
|
stderr: "pipe",
|
|
561
563
|
});
|
|
@@ -590,6 +592,38 @@ async function runValidationCommand(
|
|
|
590
592
|
};
|
|
591
593
|
}
|
|
592
594
|
|
|
595
|
+
function buildValidationCommandEnv(repo: string): Record<string, string> {
|
|
596
|
+
const homeDir = resolve(tmpdir(), "pushpals-validation-home");
|
|
597
|
+
const cacheDir = resolve(tmpdir(), "pushpals-validation-cache");
|
|
598
|
+
const expoDir = resolve(tmpdir(), "pushpals-validation-expo");
|
|
599
|
+
for (const dir of [homeDir, cacheDir, expoDir]) {
|
|
600
|
+
try {
|
|
601
|
+
mkdirSync(dir, { recursive: true });
|
|
602
|
+
} catch {
|
|
603
|
+
// Keep validation best-effort; the command output will expose any real env blocker.
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const env: Record<string, string> = {};
|
|
607
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
608
|
+
if (typeof value === "string") env[key] = value;
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
...env,
|
|
612
|
+
HOME: homeDir,
|
|
613
|
+
USERPROFILE: homeDir,
|
|
614
|
+
XDG_CACHE_HOME: cacheDir,
|
|
615
|
+
npm_config_cache: resolve(cacheDir, "npm"),
|
|
616
|
+
EXPO_HOME: expoDir,
|
|
617
|
+
EXPO_NO_TELEMETRY: process.env.EXPO_NO_TELEMETRY ?? "1",
|
|
618
|
+
EXPO_NO_INTERACTIVE: process.env.EXPO_NO_INTERACTIVE ?? "1",
|
|
619
|
+
CI: process.env.CI ?? "1",
|
|
620
|
+
BROWSER: process.env.BROWSER ?? "none",
|
|
621
|
+
EXPO_DEV_SERVER_PORT: process.env.EXPO_DEV_SERVER_PORT ?? "19006",
|
|
622
|
+
RCT_METRO_PORT: process.env.RCT_METRO_PORT ?? "19006",
|
|
623
|
+
PUSHPALS_VALIDATION_REPO: repo,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
593
627
|
interface ToolAvailabilityResult {
|
|
594
628
|
requirement: ToolRequirement;
|
|
595
629
|
ok: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
2
|
-
import { join, normalize } from "path";
|
|
2
|
+
import { isAbsolute, join, normalize, resolve } from "path";
|
|
3
3
|
|
|
4
4
|
export type ToolchainEnvironmentSource =
|
|
5
5
|
| "devcontainer"
|
|
@@ -27,6 +27,7 @@ export interface BuildToolchainPlanOptions {
|
|
|
27
27
|
repoRoot: string;
|
|
28
28
|
validationCommands: string[];
|
|
29
29
|
maxNativeScanEntries?: number;
|
|
30
|
+
maxScriptScanChars?: number;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const SHELL_CONTROL_TOKENS = new Set(["|", "||", "&", "&&", ";", ">", ">>", "<", "<<"]);
|
|
@@ -92,6 +93,17 @@ const DIRECT_TOOL_CANDIDATES: Record<string, string[]> = {
|
|
|
92
93
|
yarn: ["yarn"],
|
|
93
94
|
};
|
|
94
95
|
|
|
96
|
+
const BUN_OPTIONS_WITH_VALUE = new Set(["--cwd", "-C"]);
|
|
97
|
+
const PACKAGE_MANAGER_OPTIONS_WITH_VALUE = new Set([
|
|
98
|
+
"--cwd",
|
|
99
|
+
"--dir",
|
|
100
|
+
"--filter",
|
|
101
|
+
"--prefix",
|
|
102
|
+
"--workspace",
|
|
103
|
+
"-C",
|
|
104
|
+
"-F",
|
|
105
|
+
]);
|
|
106
|
+
|
|
95
107
|
interface NativeSignals {
|
|
96
108
|
hasC: boolean;
|
|
97
109
|
hasCxx: boolean;
|
|
@@ -144,7 +156,14 @@ export function buildToolchainPlan(options: BuildToolchainPlanOptions): Toolchai
|
|
|
144
156
|
const nativeSignals = detectNativeSignals(repoRoot, options.maxNativeScanEntries ?? 1_000);
|
|
145
157
|
const requirements: ToolRequirement[] = [];
|
|
146
158
|
for (const command of options.validationCommands) {
|
|
147
|
-
requirements.push(
|
|
159
|
+
requirements.push(
|
|
160
|
+
...inferToolRequirementsForValidationCommand(
|
|
161
|
+
repoRoot,
|
|
162
|
+
command,
|
|
163
|
+
nativeSignals,
|
|
164
|
+
options.maxScriptScanChars ?? 64_000,
|
|
165
|
+
),
|
|
166
|
+
);
|
|
148
167
|
}
|
|
149
168
|
return {
|
|
150
169
|
requirements: dedupeToolRequirements(requirements),
|
|
@@ -156,6 +175,7 @@ export function inferToolRequirementsForValidationCommand(
|
|
|
156
175
|
repoRoot: string,
|
|
157
176
|
command: string,
|
|
158
177
|
nativeSignals: NativeSignals = detectNativeSignals(repoRoot),
|
|
178
|
+
maxScriptScanChars = 64_000,
|
|
159
179
|
): ToolRequirement[] {
|
|
160
180
|
const tokens = tokenizeToolchainCommand(command);
|
|
161
181
|
if (!tokens) return [];
|
|
@@ -177,7 +197,18 @@ export function inferToolRequirementsForValidationCommand(
|
|
|
177
197
|
|
|
178
198
|
const script = resolvePackageScript(repoRoot, tokens);
|
|
179
199
|
if (script) {
|
|
180
|
-
addScriptRequirements(
|
|
200
|
+
addScriptRequirements(
|
|
201
|
+
requirements,
|
|
202
|
+
repoRoot,
|
|
203
|
+
script.scriptCwd,
|
|
204
|
+
script.script,
|
|
205
|
+
script.detectedFrom,
|
|
206
|
+
command,
|
|
207
|
+
{
|
|
208
|
+
maxScriptScanChars,
|
|
209
|
+
depth: 0,
|
|
210
|
+
},
|
|
211
|
+
);
|
|
181
212
|
}
|
|
182
213
|
|
|
183
214
|
if (usesNativeBuildCommand(tokens)) {
|
|
@@ -257,17 +288,34 @@ function addNodeBackedCliRequirement(
|
|
|
257
288
|
|
|
258
289
|
function addScriptRequirements(
|
|
259
290
|
requirements: ToolRequirement[],
|
|
291
|
+
repoRoot: string,
|
|
292
|
+
scriptCwd: string,
|
|
260
293
|
script: string,
|
|
261
294
|
detectedFrom: string,
|
|
262
295
|
command: string,
|
|
296
|
+
options: { maxScriptScanChars: number; depth: number },
|
|
263
297
|
): void {
|
|
264
298
|
const tokens = tokenizeToolchainCommand(script) ?? script.split(/\s+/).filter(Boolean);
|
|
265
299
|
const first = normalizeToolToken(tokens[0] ?? "");
|
|
266
|
-
|
|
300
|
+
// Package-manager scripts resolve Node CLIs from local node_modules/.bin. Requiring a
|
|
301
|
+
// global expo/vite/tsc binary creates false environment blockers for normal JS repos.
|
|
302
|
+
if (!NODE_BACKED_CLI_NAMES.has(first)) {
|
|
303
|
+
addDirectExecutableRequirement(requirements, first, command);
|
|
304
|
+
}
|
|
267
305
|
addNodeBackedCliRequirement(requirements, first, detectedFrom, command);
|
|
268
306
|
for (const token of tokens) {
|
|
269
307
|
addNodeBackedCliRequirement(requirements, normalizeToolToken(token), detectedFrom, command);
|
|
270
308
|
}
|
|
309
|
+
for (const scriptPath of inferReferencedScriptPaths(repoRoot, scriptCwd, tokens)) {
|
|
310
|
+
const scanned = scanScriptFileForToolRequirements(
|
|
311
|
+
requirements,
|
|
312
|
+
repoRoot,
|
|
313
|
+
scriptPath,
|
|
314
|
+
command,
|
|
315
|
+
options,
|
|
316
|
+
);
|
|
317
|
+
if (scanned) continue;
|
|
318
|
+
}
|
|
271
319
|
if (/\bnode\b/.test(script)) {
|
|
272
320
|
requirements.push({
|
|
273
321
|
tool: "node",
|
|
@@ -288,13 +336,58 @@ function addScriptRequirements(
|
|
|
288
336
|
}
|
|
289
337
|
}
|
|
290
338
|
|
|
339
|
+
function scanScriptFileForToolRequirements(
|
|
340
|
+
requirements: ToolRequirement[],
|
|
341
|
+
repoRoot: string,
|
|
342
|
+
scriptPath: string,
|
|
343
|
+
command: string,
|
|
344
|
+
options: { maxScriptScanChars: number; depth: number },
|
|
345
|
+
): boolean {
|
|
346
|
+
if (options.depth > 2 || !existsSync(scriptPath)) return false;
|
|
347
|
+
let text = "";
|
|
348
|
+
try {
|
|
349
|
+
const stats = statSync(scriptPath);
|
|
350
|
+
if (!stats.isFile() || stats.size > options.maxScriptScanChars) return false;
|
|
351
|
+
text = readFileSync(scriptPath, "utf8");
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
const detectedFrom = `${repoRelativePath(repoRoot, scriptPath)} referenced by validation command "${command}"`;
|
|
356
|
+
for (const cliName of NODE_BACKED_CLI_NAMES) {
|
|
357
|
+
const pattern = new RegExp(`(?:^|[^A-Za-z0-9_-])${escapeRegExp(cliName)}(?:$|[^A-Za-z0-9_-])`);
|
|
358
|
+
if (pattern.test(text)) {
|
|
359
|
+
addNodeBackedCliRequirement(requirements, cliName, detectedFrom, command);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (/\bnode\b/.test(text)) {
|
|
363
|
+
requirements.push({
|
|
364
|
+
tool: "node",
|
|
365
|
+
candidates: ["node"],
|
|
366
|
+
reason: "referenced validation script invokes node directly",
|
|
367
|
+
detectedFrom,
|
|
368
|
+
requiredFor: [command],
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (/\bbun\b/.test(text)) {
|
|
372
|
+
requirements.push({
|
|
373
|
+
tool: "bun",
|
|
374
|
+
candidates: ["bun"],
|
|
375
|
+
reason: "referenced validation script invokes bun",
|
|
376
|
+
detectedFrom,
|
|
377
|
+
requiredFor: [command],
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
|
|
291
383
|
function resolveBunSubcommand(tokens: string[]): { kind: "run" | "x"; value: string } | null {
|
|
292
384
|
if (normalizeToolToken(tokens[0] ?? "") !== "bun") return null;
|
|
293
385
|
let index = 1;
|
|
294
386
|
while (index < tokens.length) {
|
|
295
387
|
const token = tokens[index] ?? "";
|
|
296
|
-
|
|
297
|
-
|
|
388
|
+
const bunOption = parseOptionWithValue(token, BUN_OPTIONS_WITH_VALUE, tokens[index + 1]);
|
|
389
|
+
if (bunOption) {
|
|
390
|
+
index += bunOption.consumed;
|
|
298
391
|
continue;
|
|
299
392
|
}
|
|
300
393
|
if (token.startsWith("--")) {
|
|
@@ -313,7 +406,7 @@ function resolveBunSubcommand(tokens: string[]): { kind: "run" | "x"; value: str
|
|
|
313
406
|
function resolvePackageScript(
|
|
314
407
|
repoRoot: string,
|
|
315
408
|
tokens: string[],
|
|
316
|
-
): { script: string; detectedFrom: string } | null {
|
|
409
|
+
): { script: string; scriptCwd: string; detectedFrom: string } | null {
|
|
317
410
|
const first = normalizeToolToken(tokens[0] ?? "");
|
|
318
411
|
let cwd = repoRoot;
|
|
319
412
|
let scriptName = "";
|
|
@@ -321,9 +414,10 @@ function resolvePackageScript(
|
|
|
321
414
|
let index = 1;
|
|
322
415
|
while (index < tokens.length) {
|
|
323
416
|
const token = tokens[index] ?? "";
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
417
|
+
const bunOption = parseOptionWithValue(token, BUN_OPTIONS_WITH_VALUE, tokens[index + 1]);
|
|
418
|
+
if (bunOption) {
|
|
419
|
+
cwd = resolveWorkspacePath(repoRoot, bunOption.value);
|
|
420
|
+
index += bunOption.consumed;
|
|
327
421
|
continue;
|
|
328
422
|
}
|
|
329
423
|
if (token.startsWith("--")) {
|
|
@@ -345,17 +439,31 @@ function resolvePackageScript(
|
|
|
345
439
|
while (index < tokens.length) {
|
|
346
440
|
const token = tokens[index] ?? "";
|
|
347
441
|
const normalized = normalizeToolToken(token);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
442
|
+
const packageOption = parseOptionWithValue(
|
|
443
|
+
token,
|
|
444
|
+
packageManagerOptionsWithValue(first),
|
|
445
|
+
tokens[index + 1],
|
|
446
|
+
);
|
|
447
|
+
if (packageOption) {
|
|
448
|
+
if (packageOption.name !== "--filter" && packageOption.name !== "-F") {
|
|
449
|
+
const optionCwd = resolvePackageOptionCwd(
|
|
450
|
+
repoRoot,
|
|
451
|
+
packageOption.name,
|
|
452
|
+
packageOption.value,
|
|
453
|
+
);
|
|
454
|
+
if (!optionCwd && isWorkspacePackageOption(packageOption.name)) return null;
|
|
455
|
+
if (optionCwd) cwd = optionCwd;
|
|
456
|
+
}
|
|
457
|
+
index += packageOption.consumed;
|
|
357
458
|
continue;
|
|
358
459
|
}
|
|
460
|
+
if (first === "yarn" && normalized === "workspace" && tokens[index + 2]) {
|
|
461
|
+
const workspaceCwd = resolveWorkspacePackageCwd(repoRoot, tokens[index + 1] ?? "");
|
|
462
|
+
if (!workspaceCwd) return null;
|
|
463
|
+
cwd = workspaceCwd;
|
|
464
|
+
scriptName = tokens[index + 2] ?? "";
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
359
467
|
if (normalized === "run") {
|
|
360
468
|
scriptName = tokens[index + 1] ?? "";
|
|
361
469
|
break;
|
|
@@ -379,6 +487,7 @@ function resolvePackageScript(
|
|
|
379
487
|
if (typeof script !== "string" || !script.trim()) return null;
|
|
380
488
|
return {
|
|
381
489
|
script,
|
|
490
|
+
scriptCwd: cwd,
|
|
382
491
|
detectedFrom: `${repoRelativePath(repoRoot, packagePath)} script "${scriptName}"`,
|
|
383
492
|
};
|
|
384
493
|
} catch {
|
|
@@ -386,6 +495,198 @@ function resolvePackageScript(
|
|
|
386
495
|
}
|
|
387
496
|
}
|
|
388
497
|
|
|
498
|
+
function inferReferencedScriptPaths(repoRoot: string, scriptCwd: string, tokens: string[]): string[] {
|
|
499
|
+
const out: string[] = [];
|
|
500
|
+
const seen = new Set<string>();
|
|
501
|
+
for (const token of tokens) {
|
|
502
|
+
const normalized = normalizeReferencedScriptToken(token);
|
|
503
|
+
if (!normalized) continue;
|
|
504
|
+
const resolved = isAbsolute(normalized) ? normalized : join(scriptCwd, normalized);
|
|
505
|
+
const key = normalize(resolved);
|
|
506
|
+
if (seen.has(key)) continue;
|
|
507
|
+
seen.add(key);
|
|
508
|
+
out.push(resolved);
|
|
509
|
+
}
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function resolveWorkspacePath(repoRoot: string, pathValue: string): string {
|
|
514
|
+
return isAbsolute(pathValue) ? normalize(pathValue) : resolve(repoRoot, pathValue);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function resolvePackageOptionCwd(
|
|
518
|
+
repoRoot: string,
|
|
519
|
+
optionName: string,
|
|
520
|
+
optionValue: string,
|
|
521
|
+
): string | null {
|
|
522
|
+
if (isWorkspacePackageOption(optionName)) {
|
|
523
|
+
return resolveWorkspacePackageCwd(repoRoot, optionValue);
|
|
524
|
+
}
|
|
525
|
+
const optionCwd = resolveWorkspacePath(repoRoot, optionValue);
|
|
526
|
+
return existsSync(join(optionCwd, "package.json")) ? optionCwd : null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function resolveWorkspacePackageCwd(repoRoot: string, workspaceRef: string): string | null {
|
|
530
|
+
const directCwd = resolveWorkspacePath(repoRoot, workspaceRef);
|
|
531
|
+
if (existsSync(join(directCwd, "package.json"))) return directCwd;
|
|
532
|
+
for (const candidate of expandWorkspacePackageDirs(repoRoot)) {
|
|
533
|
+
try {
|
|
534
|
+
const parsed = JSON.parse(readFileSync(join(candidate, "package.json"), "utf8")) as {
|
|
535
|
+
name?: unknown;
|
|
536
|
+
};
|
|
537
|
+
if (parsed.name === workspaceRef) return candidate;
|
|
538
|
+
} catch {
|
|
539
|
+
// Ignore malformed workspace packages; validation will report real repo failures.
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function expandWorkspacePackageDirs(repoRoot: string, maxPackages = 200): string[] {
|
|
546
|
+
const packageJsonPath = join(repoRoot, "package.json");
|
|
547
|
+
if (!existsSync(packageJsonPath)) return [];
|
|
548
|
+
let patterns: string[] = [];
|
|
549
|
+
try {
|
|
550
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
|
551
|
+
workspaces?: unknown;
|
|
552
|
+
};
|
|
553
|
+
if (Array.isArray(parsed.workspaces)) {
|
|
554
|
+
patterns = parsed.workspaces.filter((entry): entry is string => typeof entry === "string");
|
|
555
|
+
} else if (
|
|
556
|
+
parsed.workspaces &&
|
|
557
|
+
typeof parsed.workspaces === "object" &&
|
|
558
|
+
Array.isArray((parsed.workspaces as { packages?: unknown }).packages)
|
|
559
|
+
) {
|
|
560
|
+
patterns = (parsed.workspaces as { packages: unknown[] }).packages.filter(
|
|
561
|
+
(entry): entry is string => typeof entry === "string",
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
const out: string[] = [];
|
|
568
|
+
const seen = new Set<string>();
|
|
569
|
+
for (const pattern of patterns) {
|
|
570
|
+
if (out.length >= maxPackages) break;
|
|
571
|
+
for (const candidate of expandWorkspacePattern(repoRoot, pattern, maxPackages - out.length)) {
|
|
572
|
+
const key = normalize(candidate);
|
|
573
|
+
if (seen.has(key) || !existsSync(join(candidate, "package.json"))) continue;
|
|
574
|
+
seen.add(key);
|
|
575
|
+
out.push(candidate);
|
|
576
|
+
if (out.length >= maxPackages) break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return out;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function expandWorkspacePattern(repoRoot: string, pattern: string, maxPackages: number): string[] {
|
|
583
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
584
|
+
if (!normalizedPattern || normalizedPattern.startsWith("!")) return [];
|
|
585
|
+
const segments = normalizedPattern.split("/").filter(Boolean);
|
|
586
|
+
let dirs = [repoRoot];
|
|
587
|
+
for (const segment of segments) {
|
|
588
|
+
const next: string[] = [];
|
|
589
|
+
for (const dir of dirs) {
|
|
590
|
+
if (next.length >= maxPackages) break;
|
|
591
|
+
if (segment === "**") {
|
|
592
|
+
next.push(...collectDescendantDirs(dir, Math.max(0, maxPackages - next.length), 3));
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (segment.includes("*")) {
|
|
596
|
+
const patternRegex = wildcardSegmentRegex(segment);
|
|
597
|
+
for (const entry of safeReadDir(dir)) {
|
|
598
|
+
if (next.length >= maxPackages) break;
|
|
599
|
+
if (!patternRegex.test(entry)) continue;
|
|
600
|
+
const candidate = join(dir, entry);
|
|
601
|
+
if (safeIsDirectory(candidate)) next.push(candidate);
|
|
602
|
+
}
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const candidate = join(dir, segment);
|
|
606
|
+
if (safeIsDirectory(candidate)) next.push(candidate);
|
|
607
|
+
}
|
|
608
|
+
dirs = next;
|
|
609
|
+
if (dirs.length === 0) break;
|
|
610
|
+
}
|
|
611
|
+
return dirs;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function collectDescendantDirs(dir: string, limit: number, maxDepth: number): string[] {
|
|
615
|
+
const out: string[] = [];
|
|
616
|
+
const visit = (current: string, depth: number) => {
|
|
617
|
+
if (out.length >= limit || depth > maxDepth) return;
|
|
618
|
+
for (const entry of safeReadDir(current)) {
|
|
619
|
+
if (entry === "node_modules" || entry === ".git") continue;
|
|
620
|
+
const candidate = join(current, entry);
|
|
621
|
+
if (!safeIsDirectory(candidate)) continue;
|
|
622
|
+
out.push(candidate);
|
|
623
|
+
visit(candidate, depth + 1);
|
|
624
|
+
if (out.length >= limit) return;
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
visit(dir, 0);
|
|
628
|
+
return out;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function safeReadDir(dir: string): string[] {
|
|
632
|
+
try {
|
|
633
|
+
return readdirSync(dir);
|
|
634
|
+
} catch {
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function safeIsDirectory(pathValue: string): boolean {
|
|
640
|
+
try {
|
|
641
|
+
return statSync(pathValue).isDirectory();
|
|
642
|
+
} catch {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function wildcardSegmentRegex(segment: string): RegExp {
|
|
648
|
+
return new RegExp(`^${segment.split("*").map(escapeRegExp).join(".*")}$`);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function isWorkspacePackageOption(optionName: string): boolean {
|
|
652
|
+
return optionName === "--workspace" || optionName === "-w";
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function parseOptionWithValue(
|
|
656
|
+
token: string,
|
|
657
|
+
optionsWithValue: Set<string>,
|
|
658
|
+
nextToken?: string,
|
|
659
|
+
): { name: string; value: string; consumed: number } | null {
|
|
660
|
+
const equalsIndex = token.indexOf("=");
|
|
661
|
+
if (equalsIndex > 0) {
|
|
662
|
+
const name = token.slice(0, equalsIndex);
|
|
663
|
+
if (!optionsWithValue.has(name)) return null;
|
|
664
|
+
const value = token.slice(equalsIndex + 1);
|
|
665
|
+
return value ? { name, value, consumed: 1 } : null;
|
|
666
|
+
}
|
|
667
|
+
if (!optionsWithValue.has(token) || !nextToken) return null;
|
|
668
|
+
return { name: token, value: nextToken, consumed: 2 };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function packageManagerOptionsWithValue(packageManager: string): Set<string> {
|
|
672
|
+
if (packageManager === "npm") {
|
|
673
|
+
return new Set([...PACKAGE_MANAGER_OPTIONS_WITH_VALUE, "-w"]);
|
|
674
|
+
}
|
|
675
|
+
return PACKAGE_MANAGER_OPTIONS_WITH_VALUE;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function normalizeReferencedScriptToken(token: string): string | null {
|
|
679
|
+
let normalized = token.replace(/\\/g, "/");
|
|
680
|
+
if (normalized.startsWith("-")) {
|
|
681
|
+
const equalsIndex = normalized.indexOf("=");
|
|
682
|
+
if (equalsIndex === -1) return null;
|
|
683
|
+
normalized = normalized.slice(equalsIndex + 1);
|
|
684
|
+
}
|
|
685
|
+
if (!/\.(cjs|cts|js|jsx|mjs|mts|ts|tsx)$/i.test(normalized)) return null;
|
|
686
|
+
if (normalized.includes("://")) return null;
|
|
687
|
+
return normalized;
|
|
688
|
+
}
|
|
689
|
+
|
|
389
690
|
function detectToolchainEnvironmentSource(repoRoot: string): ToolchainEnvironmentSource {
|
|
390
691
|
if (existsSync(join(repoRoot, ".devcontainer", "devcontainer.json"))) return "devcontainer";
|
|
391
692
|
if (existsSync(join(repoRoot, "devcontainer.json"))) return "devcontainer";
|
|
@@ -501,6 +802,10 @@ function normalizeToolToken(token: string): string {
|
|
|
501
802
|
return normalizedToken.toLowerCase().replace(/\.(cmd|exe|ps1)$/i, "");
|
|
502
803
|
}
|
|
503
804
|
|
|
805
|
+
function escapeRegExp(value: string): string {
|
|
806
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
807
|
+
}
|
|
808
|
+
|
|
504
809
|
function repoRelativePath(repoRoot: string, pathValue: string): string {
|
|
505
810
|
const root = normalize(repoRoot).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
506
811
|
const path = normalize(pathValue).replace(/\\/g, "/");
|