@pushpalsdev/cli 1.0.98 → 1.0.100

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.98",
3
+ "version": "1.0.100",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,6 +16,7 @@ Execution rules:
16
16
  - If the hinted file is a thin wrapper or the behavior lives elsewhere, edit the behavior-owning file(s) needed to solve the task and explain the scope expansion in your final response.
17
17
  - Avoid irrelevant sprawl; the review agent will judge whether changed files are necessary for the requested outcome.
18
18
  - Read relevant files before editing, then run focused validation.
19
+ - PushPals runs the deterministic ValidationGate after your edit, including any repo-required `vision.md` commands. During the editing turn, prefer focused/fast validation. Do not spend the main Codex execution budget repeatedly running long browser/e2e smoke commands such as `bun run web:e2e`; run them only when the task is specifically about the browser harness or when you need a final targeted confirmation and can stop promptly on a clear failure.
19
20
  - Use direct commands without shell wrappers. Prefer plain commands like `git diff -- path`, `git add <path>`, `git status --porcelain`, and `pwd`.
20
21
  - Do not wrap commands in `/bin/bash -lc`, `sh -lc`, `cmd /c`, or `powershell -Command`, and avoid pipelines, `awk`, heredocs, or multi-command shell snippets unless they are truly unavoidable.
21
22
  - If the command router rejects a command, simplify it to a single direct command instead of retrying more shell wrappers.
@@ -53,6 +53,12 @@ function ensureSandboxGitConfig(homeDir: string): void {
53
53
  }
54
54
  }
55
55
 
56
+ function withNodeDnsIpv4First(value: string | undefined): string {
57
+ const existing = (value ?? "").trim();
58
+ if (/(^|\s)--dns-result-order=/.test(existing)) return existing;
59
+ return [existing, "--dns-result-order=ipv4first"].filter(Boolean).join(" ");
60
+ }
61
+
56
62
  function resolveOriginalHome(env: Record<string, string>): string {
57
63
  return env.HOME || env.USERPROFILE || homedir();
58
64
  }
@@ -100,6 +106,8 @@ export function buildWorkerSandboxWritableEnv(
100
106
  EXPO_NO_INTERACTIVE: env.EXPO_NO_INTERACTIVE ?? "1",
101
107
  CI: env.CI ?? "1",
102
108
  BROWSER: env.BROWSER ?? "none",
109
+ NODE_OPTIONS: withNodeDnsIpv4First(env.NODE_OPTIONS),
110
+ REACT_NATIVE_PACKAGER_HOSTNAME: env.REACT_NATIVE_PACKAGER_HOSTNAME ?? "127.0.0.1",
103
111
  EXPO_DEV_SERVER_PORT: env.EXPO_DEV_SERVER_PORT ?? defaultExpoPort,
104
112
  RCT_METRO_PORT: env.RCT_METRO_PORT ?? defaultExpoPort,
105
113
  PUSHPALS_VALIDATION_REPO: repo,
@@ -647,7 +647,89 @@ async function terminateValidationProcessTree(
647
647
  }
648
648
  }
649
649
 
650
- async function runValidationArgv(
650
+ function captureValidationStream(
651
+ stream: ReadableStream<Uint8Array> | null,
652
+ onChunk?: (chunk: string) => void,
653
+ ) {
654
+ let text = "";
655
+ let done = false;
656
+ const reader = stream?.getReader();
657
+ const promise = reader
658
+ ? (async () => {
659
+ try {
660
+ while (true) {
661
+ const result = await reader.read();
662
+ if (result.done) break;
663
+ const chunk = Buffer.from(result.value).toString("utf8");
664
+ text += chunk;
665
+ onChunk?.(chunk);
666
+ }
667
+ } catch {
668
+ // Stream cancellation after process exit is expected when descendants
669
+ // inherit pipes from failed browser/dev-server launchers.
670
+ } finally {
671
+ done = true;
672
+ try {
673
+ reader.releaseLock();
674
+ } catch {
675
+ // ignore
676
+ }
677
+ }
678
+ })()
679
+ : Promise.resolve().then(() => {
680
+ done = true;
681
+ });
682
+
683
+ return {
684
+ cancel: async () => {
685
+ try {
686
+ await reader?.cancel();
687
+ } catch {
688
+ // ignore
689
+ }
690
+ },
691
+ isDone: () => done,
692
+ promise,
693
+ text: () => text,
694
+ };
695
+ }
696
+
697
+ const DEFAULT_BROWSER_VALIDATION_FAILURE_IDLE_MS = 15_000;
698
+
699
+ function browserValidationFailureIdleMs(env: Record<string, string>): number {
700
+ const configured = Number(env.PUSHPALS_VALIDATION_FAILURE_IDLE_MS ?? "");
701
+ if (Number.isFinite(configured) && configured >= 250) {
702
+ return Math.min(120_000, Math.trunc(configured));
703
+ }
704
+ return DEFAULT_BROWSER_VALIDATION_FAILURE_IDLE_MS;
705
+ }
706
+
707
+ function hasBrowserValidationFailureSignal(output: string): boolean {
708
+ const text = String(output ?? "");
709
+ if (!text.trim()) return false;
710
+ const patterns = [
711
+ /\bAssertionError\b/i,
712
+ /\bTimeoutError\b/i,
713
+ /\bWeb end-to-end smoke test failed:/i,
714
+ /\bexpect\([^)]*\)\.[a-z0-9_]+\([^)]*\)\s+failed/i,
715
+ /\bError:\s+expect\(/i,
716
+ /\blocator\.[a-z0-9_]+:\s+Timeout\s+\d+ms\s+exceeded\b/i,
717
+ /\bpage\.[a-z0-9_]+:\s+Timeout\s+\d+ms\s+exceeded\b/i,
718
+ /\bTimeout\s+\d+ms\s+exceeded\b/i,
719
+ /\bTest timeout of \d+ms exceeded\b/i,
720
+ /\bCall log:\s*(?:\r?\n|$)/i,
721
+ /\bwaiting for getBy(?:TestId|Role|Text|Label|Placeholder|Title)\([^)]*\)/i,
722
+ /\bpage\.[a-z0-9_]+:\s+net::ERR_[A-Z0-9_]+/i,
723
+ /\bbrowserType\.launch:/i,
724
+ /\bERR_SOCKET_BAD_PORT\b/i,
725
+ /\blisten\s+EPERM\b/i,
726
+ /\bEADDRINUSE\b/i,
727
+ /\berror:\s+script\s+"[^"]+"\s+exited with code\s+\d+/i,
728
+ ];
729
+ return patterns.some((pattern) => pattern.test(text));
730
+ }
731
+
732
+ export async function runValidationArgv(
651
733
  repo: string,
652
734
  command: string,
653
735
  argv: string[],
@@ -665,32 +747,92 @@ async function runValidationArgv(
665
747
  stderr: "pipe",
666
748
  detached: process.platform !== "win32",
667
749
  });
750
+ let lastOutputAt = Date.now();
751
+ const noteOutput = () => {
752
+ lastOutputAt = Date.now();
753
+ };
754
+ const stdoutCapture = captureValidationStream(proc.stdout, noteOutput);
755
+ const stderrCapture = captureValidationStream(proc.stderr, noteOutput);
668
756
  let timedOut = false;
669
- const timer = setTimeout(
670
- () => {
757
+ let stoppedAfterFailureSignal = false;
758
+ const timeout = Math.max(1_000, timeoutMs);
759
+ let timeoutTimer: ReturnType<typeof setTimeout> | null = null;
760
+ const timeoutPromise = new Promise<{ type: "timeout" }>((resolveTimeout) => {
761
+ timeoutTimer = setTimeout(() => {
671
762
  timedOut = true;
672
- void terminateValidationProcessTree(proc);
673
- },
674
- Math.max(1_000, timeoutMs),
675
- );
763
+ resolveTimeout({ type: "timeout" });
764
+ }, timeout);
765
+ });
766
+
767
+ let failureSignalTimer: ReturnType<typeof setInterval> | null = null;
768
+ const failureSignalPromise = isLongRunningBrowserValidationCommand(command)
769
+ ? new Promise<{ type: "failure-signal" }>((resolveFailureSignal) => {
770
+ const idleMs = browserValidationFailureIdleMs(env);
771
+ failureSignalTimer = setInterval(() => {
772
+ const combinedOutput = `${stdoutCapture.text()}\n${stderrCapture.text()}`;
773
+ if (
774
+ hasBrowserValidationFailureSignal(combinedOutput) &&
775
+ Date.now() - lastOutputAt >= idleMs
776
+ ) {
777
+ stoppedAfterFailureSignal = true;
778
+ resolveFailureSignal({ type: "failure-signal" });
779
+ }
780
+ }, 250);
781
+ })
782
+ : new Promise<never>(() => {
783
+ // Non-browser validations should only end on process exit or timeout.
784
+ });
785
+
786
+ const exitOrTimeout = await Promise.race([
787
+ proc.exited.then((code) => ({ type: "exit" as const, code })),
788
+ timeoutPromise,
789
+ failureSignalPromise,
790
+ ]);
791
+ if (timeoutTimer) clearTimeout(timeoutTimer);
792
+ if (failureSignalTimer) clearInterval(failureSignalTimer);
793
+
794
+ if (timedOut || stoppedAfterFailureSignal) {
795
+ await terminateValidationProcessTree(proc);
796
+ }
797
+
798
+ const exitCode =
799
+ exitOrTimeout.type === "timeout"
800
+ ? 124
801
+ : exitOrTimeout.type === "failure-signal"
802
+ ? 1
803
+ : exitOrTimeout.code;
804
+
805
+ if (!timedOut && !stoppedAfterFailureSignal) {
806
+ await Promise.race([
807
+ Promise.all([stdoutCapture.promise, stderrCapture.promise]),
808
+ Bun.sleep(1_000),
809
+ ]);
810
+ if (!stdoutCapture.isDone() || !stderrCapture.isDone()) {
811
+ await terminateValidationProcessTree(proc);
812
+ await Promise.all([stdoutCapture.cancel(), stderrCapture.cancel()]);
813
+ }
814
+ } else {
815
+ await Promise.all([stdoutCapture.cancel(), stderrCapture.cancel()]);
816
+ }
676
817
 
677
- const [stdout, stderr, exitCode] = await Promise.all([
678
- new Response(proc.stdout).text(),
679
- new Response(proc.stderr).text(),
680
- proc.exited,
818
+ await Promise.race([
819
+ Promise.all([stdoutCapture.promise, stderrCapture.promise]),
820
+ Bun.sleep(500),
681
821
  ]);
682
- clearTimeout(timer);
683
822
 
684
823
  return {
685
824
  step: command,
686
825
  command,
687
826
  ok: !timedOut && exitCode === 0,
688
- exitCode: timedOut ? 124 : exitCode,
689
- stdout: compactJobOutput(stdout.trim(), outputPolicy),
827
+ exitCode,
828
+ stdout: compactJobOutput(stdoutCapture.text().trim(), outputPolicy),
690
829
  stderr: compactJobOutput(
691
830
  [
692
- stderr.trim(),
831
+ stderrCapture.text().trim(),
693
832
  timedOut ? timeoutMessage : "",
833
+ stoppedAfterFailureSignal
834
+ ? `Validation command emitted a browser/e2e failure signal and then produced no output for ${browserValidationFailureIdleMs(env)}ms. PushPals terminated the leaked process tree and preserved the captured failure output for repair.`
835
+ : "",
694
836
  ]
695
837
  .filter(Boolean)
696
838
  .join("\n"),
@@ -1242,6 +1384,10 @@ function detectValidationBlocker(runs: ValidationExecutionResult[]): ValidationB
1242
1384
  combined.includes("network access") ||
1243
1385
  combined.includes("connection refused") ||
1244
1386
  combined.includes("getaddrinfo") ||
1387
+ combined.includes("err_socket_bad_port") ||
1388
+ combined.includes("expo exited early") ||
1389
+ combined.includes("eperm") ||
1390
+ combined.includes("operation not permitted") ||
1245
1391
  combined.includes("eacces")
1246
1392
  ) {
1247
1393
  return {
@@ -1407,6 +1553,12 @@ export function extractValidationFailureDigest(run: {
1407
1553
  /\bCould not resolve\s+['"`]?[^'"`\r\n]+['"`]?[^\r\n]*/i,
1408
1554
  /\bModule not found[^\r\n]*/i,
1409
1555
  /\bbrowserType\.launch:[^\r\n]*/i,
1556
+ /\bWeb end-to-end smoke test failed:[^\r\n]*/i,
1557
+ /\blocator\.[a-z0-9_]+:\s+Timeout\s+\d+ms\s+exceeded[^\r\n]*/i,
1558
+ /\bpage\.[a-z0-9_]+:\s+Timeout\s+\d+ms\s+exceeded[^\r\n]*/i,
1559
+ /\bTimeout\s+\d+ms\s+exceeded[^\r\n]*/i,
1560
+ /\bwaiting for getBy(?:TestId|Role|Text|Label|Placeholder|Title)\([^)]*\)[^\r\n]*/i,
1561
+ /\bpage\.[a-z0-9_]+:\s+net::ERR_[A-Z0-9_]+[^\r\n]*/i,
1410
1562
  /\bExecutable doesn't exist[^\r\n]*/i,
1411
1563
  /\bPlease run the following command to download new browsers:[^\r\n]*(?:\r?\n\s+[^\r\n]+)?/i,
1412
1564
  /\bRun ["`]?npx playwright install[^'"`\r\n]*["`]?[^\r\n]*/i,
@@ -16,6 +16,7 @@ Execution rules:
16
16
  - If the hinted file is a thin wrapper or the behavior lives elsewhere, edit the behavior-owning file(s) needed to solve the task and explain the scope expansion in your final response.
17
17
  - Avoid irrelevant sprawl; the review agent will judge whether changed files are necessary for the requested outcome.
18
18
  - Read relevant files before editing, then run focused validation.
19
+ - PushPals runs the deterministic ValidationGate after your edit, including any repo-required `vision.md` commands. During the editing turn, prefer focused/fast validation. Do not spend the main Codex execution budget repeatedly running long browser/e2e smoke commands such as `bun run web:e2e`; run them only when the task is specifically about the browser harness or when you need a final targeted confirmation and can stop promptly on a clear failure.
19
20
  - Use direct commands without shell wrappers. Prefer plain commands like `git diff -- path`, `git add <path>`, `git status --porcelain`, and `pwd`.
20
21
  - Do not wrap commands in `/bin/bash -lc`, `sh -lc`, `cmd /c`, or `powershell -Command`, and avoid pipelines, `awk`, heredocs, or multi-command shell snippets unless they are truly unavoidable.
21
22
  - If the command router rejects a command, simplify it to a single direct command instead of retrying more shell wrappers.