@pushpalsdev/cli 1.0.82 → 1.0.83

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.82",
3
+ "version": "1.0.83",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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 } 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(["|", "||", "&", "&&", ";", ">", ">>", "<", "<<"]);
@@ -144,7 +145,14 @@ export function buildToolchainPlan(options: BuildToolchainPlanOptions): Toolchai
144
145
  const nativeSignals = detectNativeSignals(repoRoot, options.maxNativeScanEntries ?? 1_000);
145
146
  const requirements: ToolRequirement[] = [];
146
147
  for (const command of options.validationCommands) {
147
- requirements.push(...inferToolRequirementsForValidationCommand(repoRoot, command, nativeSignals));
148
+ requirements.push(
149
+ ...inferToolRequirementsForValidationCommand(
150
+ repoRoot,
151
+ command,
152
+ nativeSignals,
153
+ options.maxScriptScanChars ?? 64_000,
154
+ ),
155
+ );
148
156
  }
149
157
  return {
150
158
  requirements: dedupeToolRequirements(requirements),
@@ -156,6 +164,7 @@ export function inferToolRequirementsForValidationCommand(
156
164
  repoRoot: string,
157
165
  command: string,
158
166
  nativeSignals: NativeSignals = detectNativeSignals(repoRoot),
167
+ maxScriptScanChars = 64_000,
159
168
  ): ToolRequirement[] {
160
169
  const tokens = tokenizeToolchainCommand(command);
161
170
  if (!tokens) return [];
@@ -177,7 +186,18 @@ export function inferToolRequirementsForValidationCommand(
177
186
 
178
187
  const script = resolvePackageScript(repoRoot, tokens);
179
188
  if (script) {
180
- addScriptRequirements(requirements, script.script, script.detectedFrom, command);
189
+ addScriptRequirements(
190
+ requirements,
191
+ repoRoot,
192
+ script.scriptCwd,
193
+ script.script,
194
+ script.detectedFrom,
195
+ command,
196
+ {
197
+ maxScriptScanChars,
198
+ depth: 0,
199
+ },
200
+ );
181
201
  }
182
202
 
183
203
  if (usesNativeBuildCommand(tokens)) {
@@ -257,17 +277,34 @@ function addNodeBackedCliRequirement(
257
277
 
258
278
  function addScriptRequirements(
259
279
  requirements: ToolRequirement[],
280
+ repoRoot: string,
281
+ scriptCwd: string,
260
282
  script: string,
261
283
  detectedFrom: string,
262
284
  command: string,
285
+ options: { maxScriptScanChars: number; depth: number },
263
286
  ): void {
264
287
  const tokens = tokenizeToolchainCommand(script) ?? script.split(/\s+/).filter(Boolean);
265
288
  const first = normalizeToolToken(tokens[0] ?? "");
266
- addDirectExecutableRequirement(requirements, first, command);
289
+ // Package-manager scripts resolve Node CLIs from local node_modules/.bin. Requiring a
290
+ // global expo/vite/tsc binary creates false environment blockers for normal JS repos.
291
+ if (!NODE_BACKED_CLI_NAMES.has(first)) {
292
+ addDirectExecutableRequirement(requirements, first, command);
293
+ }
267
294
  addNodeBackedCliRequirement(requirements, first, detectedFrom, command);
268
295
  for (const token of tokens) {
269
296
  addNodeBackedCliRequirement(requirements, normalizeToolToken(token), detectedFrom, command);
270
297
  }
298
+ for (const scriptPath of inferReferencedScriptPaths(repoRoot, scriptCwd, tokens)) {
299
+ const scanned = scanScriptFileForToolRequirements(
300
+ requirements,
301
+ repoRoot,
302
+ scriptPath,
303
+ command,
304
+ options,
305
+ );
306
+ if (scanned) continue;
307
+ }
271
308
  if (/\bnode\b/.test(script)) {
272
309
  requirements.push({
273
310
  tool: "node",
@@ -288,6 +325,50 @@ function addScriptRequirements(
288
325
  }
289
326
  }
290
327
 
328
+ function scanScriptFileForToolRequirements(
329
+ requirements: ToolRequirement[],
330
+ repoRoot: string,
331
+ scriptPath: string,
332
+ command: string,
333
+ options: { maxScriptScanChars: number; depth: number },
334
+ ): boolean {
335
+ if (options.depth > 2 || !existsSync(scriptPath)) return false;
336
+ let text = "";
337
+ try {
338
+ const stats = statSync(scriptPath);
339
+ if (!stats.isFile() || stats.size > options.maxScriptScanChars) return false;
340
+ text = readFileSync(scriptPath, "utf8");
341
+ } catch {
342
+ return false;
343
+ }
344
+ const detectedFrom = `${repoRelativePath(repoRoot, scriptPath)} referenced by validation command "${command}"`;
345
+ for (const cliName of NODE_BACKED_CLI_NAMES) {
346
+ const pattern = new RegExp(`(?:^|[^A-Za-z0-9_-])${escapeRegExp(cliName)}(?:$|[^A-Za-z0-9_-])`);
347
+ if (pattern.test(text)) {
348
+ addNodeBackedCliRequirement(requirements, cliName, detectedFrom, command);
349
+ }
350
+ }
351
+ if (/\bnode\b/.test(text)) {
352
+ requirements.push({
353
+ tool: "node",
354
+ candidates: ["node"],
355
+ reason: "referenced validation script invokes node directly",
356
+ detectedFrom,
357
+ requiredFor: [command],
358
+ });
359
+ }
360
+ if (/\bbun\b/.test(text)) {
361
+ requirements.push({
362
+ tool: "bun",
363
+ candidates: ["bun"],
364
+ reason: "referenced validation script invokes bun",
365
+ detectedFrom,
366
+ requiredFor: [command],
367
+ });
368
+ }
369
+ return true;
370
+ }
371
+
291
372
  function resolveBunSubcommand(tokens: string[]): { kind: "run" | "x"; value: string } | null {
292
373
  if (normalizeToolToken(tokens[0] ?? "") !== "bun") return null;
293
374
  let index = 1;
@@ -313,7 +394,7 @@ function resolveBunSubcommand(tokens: string[]): { kind: "run" | "x"; value: str
313
394
  function resolvePackageScript(
314
395
  repoRoot: string,
315
396
  tokens: string[],
316
- ): { script: string; detectedFrom: string } | null {
397
+ ): { script: string; scriptCwd: string; detectedFrom: string } | null {
317
398
  const first = normalizeToolToken(tokens[0] ?? "");
318
399
  let cwd = repoRoot;
319
400
  let scriptName = "";
@@ -379,6 +460,7 @@ function resolvePackageScript(
379
460
  if (typeof script !== "string" || !script.trim()) return null;
380
461
  return {
381
462
  script,
463
+ scriptCwd: cwd,
382
464
  detectedFrom: `${repoRelativePath(repoRoot, packagePath)} script "${scriptName}"`,
383
465
  };
384
466
  } catch {
@@ -386,6 +468,33 @@ function resolvePackageScript(
386
468
  }
387
469
  }
388
470
 
471
+ function inferReferencedScriptPaths(repoRoot: string, scriptCwd: string, tokens: string[]): string[] {
472
+ const out: string[] = [];
473
+ const seen = new Set<string>();
474
+ for (const token of tokens) {
475
+ const normalized = normalizeReferencedScriptToken(token);
476
+ if (!normalized) continue;
477
+ const resolved = isAbsolute(normalized) ? normalized : join(scriptCwd, normalized);
478
+ const key = normalize(resolved);
479
+ if (seen.has(key)) continue;
480
+ seen.add(key);
481
+ out.push(resolved);
482
+ }
483
+ return out;
484
+ }
485
+
486
+ function normalizeReferencedScriptToken(token: string): string | null {
487
+ let normalized = token.replace(/\\/g, "/");
488
+ if (normalized.startsWith("-")) {
489
+ const equalsIndex = normalized.indexOf("=");
490
+ if (equalsIndex === -1) return null;
491
+ normalized = normalized.slice(equalsIndex + 1);
492
+ }
493
+ if (!/\.(cjs|cts|js|jsx|mjs|mts|ts|tsx)$/i.test(normalized)) return null;
494
+ if (normalized.includes("://")) return null;
495
+ return normalized;
496
+ }
497
+
389
498
  function detectToolchainEnvironmentSource(repoRoot: string): ToolchainEnvironmentSource {
390
499
  if (existsSync(join(repoRoot, ".devcontainer", "devcontainer.json"))) return "devcontainer";
391
500
  if (existsSync(join(repoRoot, "devcontainer.json"))) return "devcontainer";
@@ -501,6 +610,10 @@ function normalizeToolToken(token: string): string {
501
610
  return normalizedToken.toLowerCase().replace(/\.(cmd|exe|ps1)$/i, "");
502
611
  }
503
612
 
613
+ function escapeRegExp(value: string): string {
614
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
615
+ }
616
+
504
617
  function repoRelativePath(repoRoot: string, pathValue: string): string {
505
618
  const root = normalize(repoRoot).replace(/\\/g, "/").replace(/\/+$/, "");
506
619
  const path = normalize(pathValue).replace(/\\/g, "/");