@pushpalsdev/cli 1.0.83 → 1.0.85

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.
@@ -150,7 +150,14 @@ file_modifying_jobs = ["task.execute"]
150
150
  output_max_chars = 196608
151
151
  output_max_lines = 600
152
152
  output_max_head_lines = 120
153
- quality_max_auto_revisions = 1
153
+ quality_max_auto_revisions = 3
154
+ quality_validation_max_auto_revisions = 3
155
+ quality_scope_gate_enabled = true
156
+ quality_validation_gate_enabled = true
157
+ quality_critic_gate_enabled = true
158
+ quality_publish_gate_enabled = true
159
+ # Browser/e2e validation commands get a longer built-in floor (10m) because they
160
+ # may need to start a dev server and run browser automation.
154
161
  quality_validation_step_timeout_ms = 180000
155
162
  quality_critic_timeout_ms = 45000
156
163
  quality_soft_pass_on_exhausted = true
@@ -66,7 +66,14 @@ file_modifying_jobs = ["task.execute"]
66
66
  output_max_chars = 196608
67
67
  output_max_lines = 600
68
68
  output_max_head_lines = 120
69
- quality_max_auto_revisions = 1
69
+ quality_max_auto_revisions = 3
70
+ quality_validation_max_auto_revisions = 3
71
+ quality_scope_gate_enabled = true
72
+ quality_validation_gate_enabled = true
73
+ quality_critic_gate_enabled = true
74
+ quality_publish_gate_enabled = true
75
+ # Browser/e2e validation commands get a longer built-in floor (10m) because they
76
+ # may need to start a dev server and run browser automation.
70
77
  quality_validation_step_timeout_ms = 180000
71
78
  quality_critic_timeout_ms = 45000
72
79
  quality_soft_pass_on_exhausted = true
@@ -14,7 +14,7 @@ const DEFAULT_CONFIG_DIR = "configs";
14
14
  const TRUTHY = new Set(["1", "true", "yes", "on"]);
15
15
  const FALSY = new Set(["0", "false", "no", "off"]);
16
16
  const DEFAULT_WORKERPALS_QUALITY_CRITIC_MIN_SCORE = 8;
17
- const DEFAULT_WORKERPALS_QUALITY_MAX_AUTO_REVISIONS = 1;
17
+ const DEFAULT_WORKERPALS_QUALITY_MAX_AUTO_REVISIONS = 3;
18
18
  const DEFAULT_WORKERPALS_FILE_MODIFYING_JOBS = ["task.execute"];
19
19
  const DEFAULT_WORKERPALS_OUTPUT_MAX_CHARS = 192 * 1024;
20
20
  const DEFAULT_WORKERPALS_OUTPUT_MAX_LINES = 600;
@@ -210,6 +210,11 @@ export interface PushPalsConfig {
210
210
  outputMaxLines: number;
211
211
  outputMaxHeadLines: number;
212
212
  qualityMaxAutoRevisions: number;
213
+ qualityValidationMaxAutoRevisions: number;
214
+ qualityScopeGateEnabled: boolean;
215
+ qualityValidationGateEnabled: boolean;
216
+ qualityCriticGateEnabled: boolean;
217
+ qualityPublishGateEnabled: boolean;
213
218
  qualityValidationStepTimeoutMs: number;
214
219
  qualityCriticTimeoutMs: number;
215
220
  qualitySoftPassOnExhausted: boolean;
@@ -931,6 +936,17 @@ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
931
936
  ),
932
937
  ),
933
938
  );
939
+ const workerQualityValidationMaxAutoRevisions = Math.max(
940
+ 0,
941
+ Math.min(
942
+ 10,
943
+ asInt(
944
+ parseIntEnv("WORKERPALS_QUALITY_VALIDATION_MAX_AUTO_REVISIONS") ??
945
+ workerNode.quality_validation_max_auto_revisions,
946
+ DEFAULT_WORKERPALS_QUALITY_MAX_AUTO_REVISIONS,
947
+ ),
948
+ ),
949
+ );
934
950
  const workerFileModifyingJobs = (() => {
935
951
  const envRaw = firstNonEmpty(process.env.WORKERPALS_FILE_MODIFYING_JOBS);
936
952
  const parsed = envRaw
@@ -990,6 +1006,18 @@ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
990
1006
  const workerQualitySoftPassOnExhausted =
991
1007
  parseBoolEnv("WORKERPALS_QUALITY_SOFT_PASS_ON_EXHAUSTED") ??
992
1008
  asBoolean(workerNode.quality_soft_pass_on_exhausted, true);
1009
+ const workerQualityScopeGateEnabled =
1010
+ parseBoolEnv("WORKERPALS_QUALITY_SCOPE_GATE_ENABLED") ??
1011
+ asBoolean(workerNode.quality_scope_gate_enabled, true);
1012
+ const workerQualityValidationGateEnabled =
1013
+ parseBoolEnv("WORKERPALS_QUALITY_VALIDATION_GATE_ENABLED") ??
1014
+ asBoolean(workerNode.quality_validation_gate_enabled, true);
1015
+ const workerQualityCriticGateEnabled =
1016
+ parseBoolEnv("WORKERPALS_QUALITY_CRITIC_GATE_ENABLED") ??
1017
+ asBoolean(workerNode.quality_critic_gate_enabled, true);
1018
+ const workerQualityPublishGateEnabled =
1019
+ parseBoolEnv("WORKERPALS_QUALITY_PUBLISH_GATE_ENABLED") ??
1020
+ asBoolean(workerNode.quality_publish_gate_enabled, true);
993
1021
  const workerQualityCriticMinScore = (() => {
994
1022
  const configThresholdRaw =
995
1023
  workerNode.quality_critic_min_score == null
@@ -2032,6 +2060,11 @@ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
2032
2060
  outputMaxLines: workerOutputMaxLines,
2033
2061
  outputMaxHeadLines: workerOutputMaxHeadLines,
2034
2062
  qualityMaxAutoRevisions: workerQualityMaxAutoRevisions,
2063
+ qualityValidationMaxAutoRevisions: workerQualityValidationMaxAutoRevisions,
2064
+ qualityScopeGateEnabled: workerQualityScopeGateEnabled,
2065
+ qualityValidationGateEnabled: workerQualityValidationGateEnabled,
2066
+ qualityCriticGateEnabled: workerQualityCriticGateEnabled,
2067
+ qualityPublishGateEnabled: workerQualityPublishGateEnabled,
2035
2068
  qualityValidationStepTimeoutMs: workerQualityValidationStepTimeoutMs,
2036
2069
  qualityCriticTimeoutMs: workerQualityCriticTimeoutMs,
2037
2070
  qualitySoftPassOnExhausted: workerQualitySoftPassOnExhausted,
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from "fs";
2
- import { isAbsolute, join, normalize } from "path";
2
+ import { isAbsolute, join, normalize, resolve } from "path";
3
3
 
4
4
  export type ToolchainEnvironmentSource =
5
5
  | "devcontainer"
@@ -93,6 +93,17 @@ const DIRECT_TOOL_CANDIDATES: Record<string, string[]> = {
93
93
  yarn: ["yarn"],
94
94
  };
95
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
+
96
107
  interface NativeSignals {
97
108
  hasC: boolean;
98
109
  hasCxx: boolean;
@@ -374,8 +385,9 @@ function resolveBunSubcommand(tokens: string[]): { kind: "run" | "x"; value: str
374
385
  let index = 1;
375
386
  while (index < tokens.length) {
376
387
  const token = tokens[index] ?? "";
377
- if (token === "--cwd" || token === "-C") {
378
- index += 2;
388
+ const bunOption = parseOptionWithValue(token, BUN_OPTIONS_WITH_VALUE, tokens[index + 1]);
389
+ if (bunOption) {
390
+ index += bunOption.consumed;
379
391
  continue;
380
392
  }
381
393
  if (token.startsWith("--")) {
@@ -402,9 +414,10 @@ function resolvePackageScript(
402
414
  let index = 1;
403
415
  while (index < tokens.length) {
404
416
  const token = tokens[index] ?? "";
405
- if ((token === "--cwd" || token === "-C") && tokens[index + 1]) {
406
- cwd = join(repoRoot, tokens[index + 1] ?? "");
407
- index += 2;
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;
408
421
  continue;
409
422
  }
410
423
  if (token.startsWith("--")) {
@@ -426,17 +439,31 @@ function resolvePackageScript(
426
439
  while (index < tokens.length) {
427
440
  const token = tokens[index] ?? "";
428
441
  const normalized = normalizeToolToken(token);
429
- if (
430
- (token === "--prefix" ||
431
- token === "--dir" ||
432
- token === "--cwd" ||
433
- token === "-C") &&
434
- tokens[index + 1]
435
- ) {
436
- cwd = join(repoRoot, tokens[index + 1] ?? "");
437
- index += 2;
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;
438
458
  continue;
439
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
+ }
440
467
  if (normalized === "run") {
441
468
  scriptName = tokens[index + 1] ?? "";
442
469
  break;
@@ -483,6 +510,171 @@ function inferReferencedScriptPaths(repoRoot: string, scriptCwd: string, tokens:
483
510
  return out;
484
511
  }
485
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
+
486
678
  function normalizeReferencedScriptToken(token: string): string | null {
487
679
  let normalized = token.replace(/\\/g, "/");
488
680
  if (normalized.startsWith("-")) {