looply-cli 0.5.2 → 0.5.4

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.
Files changed (143) hide show
  1. package/dist/.src-hash +1 -1
  2. package/dist/agent-contained.d.ts +2 -0
  3. package/dist/agent-contained.d.ts.map +1 -1
  4. package/dist/agent-contained.js +22 -5
  5. package/dist/agent-contained.js.map +1 -1
  6. package/dist/agent-exec.d.ts +10 -5
  7. package/dist/agent-exec.d.ts.map +1 -1
  8. package/dist/agent-exec.js +1 -0
  9. package/dist/agent-exec.js.map +1 -1
  10. package/dist/autonomy-tick.d.ts +46 -0
  11. package/dist/autonomy-tick.d.ts.map +1 -1
  12. package/dist/autonomy-tick.js +67 -2
  13. package/dist/autonomy-tick.js.map +1 -1
  14. package/dist/bin.d.ts.map +1 -1
  15. package/dist/bin.js +82 -16
  16. package/dist/bin.js.map +1 -1
  17. package/dist/brain-advice.js +2 -2
  18. package/dist/brain-advice.js.map +1 -1
  19. package/dist/command-runner.d.ts.map +1 -1
  20. package/dist/command-runner.js +1 -0
  21. package/dist/command-runner.js.map +1 -1
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +24 -1
  24. package/dist/config.js.map +1 -1
  25. package/dist/connect-node.d.ts.map +1 -1
  26. package/dist/connect-node.js +8 -4
  27. package/dist/connect-node.js.map +1 -1
  28. package/dist/connect.d.ts +8 -0
  29. package/dist/connect.d.ts.map +1 -1
  30. package/dist/connect.js +22 -0
  31. package/dist/connect.js.map +1 -1
  32. package/dist/contained-node.d.ts +51 -0
  33. package/dist/contained-node.d.ts.map +1 -0
  34. package/dist/contained-node.js +116 -0
  35. package/dist/contained-node.js.map +1 -0
  36. package/dist/daemon-cmd.d.ts.map +1 -1
  37. package/dist/daemon-cmd.js +56 -1
  38. package/dist/daemon-cmd.js.map +1 -1
  39. package/dist/daemon.d.ts +23 -0
  40. package/dist/daemon.d.ts.map +1 -1
  41. package/dist/daemon.js +32 -4
  42. package/dist/daemon.js.map +1 -1
  43. package/dist/exec-domains.d.ts +4 -0
  44. package/dist/exec-domains.d.ts.map +1 -1
  45. package/dist/exec-domains.js +7 -2
  46. package/dist/exec-domains.js.map +1 -1
  47. package/dist/gate-draft.d.ts +99 -0
  48. package/dist/gate-draft.d.ts.map +1 -0
  49. package/dist/gate-draft.js +544 -0
  50. package/dist/gate-draft.js.map +1 -0
  51. package/dist/git-driver.d.ts.map +1 -1
  52. package/dist/git-driver.js +1 -0
  53. package/dist/git-driver.js.map +1 -1
  54. package/dist/init-detect.d.ts +20 -10
  55. package/dist/init-detect.d.ts.map +1 -1
  56. package/dist/init-detect.js +28 -86
  57. package/dist/init-detect.js.map +1 -1
  58. package/dist/model-routing.d.ts +52 -7
  59. package/dist/model-routing.d.ts.map +1 -1
  60. package/dist/model-routing.js +84 -14
  61. package/dist/model-routing.js.map +1 -1
  62. package/dist/model-settings.d.ts +67 -0
  63. package/dist/model-settings.d.ts.map +1 -0
  64. package/dist/model-settings.js +177 -0
  65. package/dist/model-settings.js.map +1 -0
  66. package/dist/model-transport.d.ts +8 -0
  67. package/dist/model-transport.d.ts.map +1 -1
  68. package/dist/model-transport.js +31 -11
  69. package/dist/model-transport.js.map +1 -1
  70. package/dist/notify-channel.d.ts.map +1 -1
  71. package/dist/notify-channel.js +1 -0
  72. package/dist/notify-channel.js.map +1 -1
  73. package/dist/provider-cmd.d.ts +65 -0
  74. package/dist/provider-cmd.d.ts.map +1 -0
  75. package/dist/provider-cmd.js +360 -0
  76. package/dist/provider-cmd.js.map +1 -0
  77. package/dist/provider-discovery.d.ts +75 -0
  78. package/dist/provider-discovery.d.ts.map +1 -0
  79. package/dist/provider-discovery.js +235 -0
  80. package/dist/provider-discovery.js.map +1 -0
  81. package/dist/provider-identity.d.ts +3 -3
  82. package/dist/provider-identity.d.ts.map +1 -1
  83. package/dist/provider-identity.js +4 -5
  84. package/dist/provider-identity.js.map +1 -1
  85. package/dist/provider-preflight.d.ts.map +1 -1
  86. package/dist/provider-preflight.js +1 -0
  87. package/dist/provider-preflight.js.map +1 -1
  88. package/dist/provider-ready.d.ts +79 -0
  89. package/dist/provider-ready.d.ts.map +1 -0
  90. package/dist/provider-ready.js +149 -0
  91. package/dist/provider-ready.js.map +1 -0
  92. package/dist/provider-state.d.ts.map +1 -1
  93. package/dist/provider-state.js +1 -0
  94. package/dist/provider-state.js.map +1 -1
  95. package/dist/providers.d.ts.map +1 -1
  96. package/dist/providers.js +4 -0
  97. package/dist/providers.js.map +1 -1
  98. package/dist/refresh-gates.d.ts +47 -0
  99. package/dist/refresh-gates.d.ts.map +1 -0
  100. package/dist/refresh-gates.js +128 -0
  101. package/dist/refresh-gates.js.map +1 -0
  102. package/dist/repo-init.d.ts +5 -1
  103. package/dist/repo-init.d.ts.map +1 -1
  104. package/dist/repo-init.js +19 -3
  105. package/dist/repo-init.js.map +1 -1
  106. package/dist/run-cmd.js +4 -4
  107. package/dist/run-cmd.js.map +1 -1
  108. package/dist/run-launcher.d.ts +13 -0
  109. package/dist/run-launcher.d.ts.map +1 -1
  110. package/dist/run-launcher.js +29 -0
  111. package/dist/run-launcher.js.map +1 -1
  112. package/dist/serve-cmd.d.ts.map +1 -1
  113. package/dist/serve-cmd.js +14 -5
  114. package/dist/serve-cmd.js.map +1 -1
  115. package/dist/serve.d.ts +19 -7
  116. package/dist/serve.d.ts.map +1 -1
  117. package/dist/serve.js +28 -12
  118. package/dist/serve.js.map +1 -1
  119. package/dist/server-client.d.ts +23 -2
  120. package/dist/server-client.d.ts.map +1 -1
  121. package/dist/server-client.js +18 -2
  122. package/dist/server-client.js.map +1 -1
  123. package/dist/session-checkpoint-runtime.d.ts.map +1 -1
  124. package/dist/session-checkpoint-runtime.js +1 -0
  125. package/dist/session-checkpoint-runtime.js.map +1 -1
  126. package/dist/setup.d.ts +7 -1
  127. package/dist/setup.d.ts.map +1 -1
  128. package/dist/setup.js +13 -13
  129. package/dist/setup.js.map +1 -1
  130. package/dist/shared-grants.js +2 -2
  131. package/dist/shared-grants.js.map +1 -1
  132. package/dist/toolchain-mirror.js +1 -1
  133. package/dist/toolchain-mirror.js.map +1 -1
  134. package/dist/untrusted-exec.d.ts +23 -0
  135. package/dist/untrusted-exec.d.ts.map +1 -1
  136. package/dist/untrusted-exec.js +18 -1
  137. package/dist/untrusted-exec.js.map +1 -1
  138. package/dist/untrusted-gate.d.ts.map +1 -1
  139. package/dist/untrusted-gate.js +4 -0
  140. package/dist/untrusted-gate.js.map +1 -1
  141. package/package.json +2 -2
  142. package/win/appcontainer.ps1 +97 -4
  143. package/win/contained-node.cjs +530 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Which checks `looply init` drafts as gates, and why — the part of the profile
3
+ * that decides whether a round can ever be kept.
4
+ *
5
+ * 🔴 THE FAILURE THIS EXISTS FOR (real user, 2026-10-15). A NestJS API at the
6
+ * root with two Vite+React apps in `web/` and `admin/`, each with its own
7
+ * package.json, node_modules, `tsc` and `vitest run`. The old draft looked at
8
+ * the root only, so the gates were the root typecheck and the ROOT jest suite —
9
+ * a 30-minute, single-threaded, database-backed suite (`--runInBand`, a 6 GB
10
+ * heap, a `globalSetup` that brings Postgres up). It could never pass inside a
11
+ * round, every round was discarded, and the safety brake stopped the pursuit.
12
+ * Meanwhile the change asked for was UI in `web/` and `admin/`, which no gate
13
+ * checked at all.
14
+ *
15
+ * So three rules, all of them visible in the file this writes:
16
+ *
17
+ * - **Every app gets checked.** Sub-apps one level down (and `packages/*`,
18
+ * `apps/*`, declared workspaces) are profiled like the root.
19
+ * - **A gate must fit inside a round.** A suite that is evidently slow or
20
+ * service-backed is drafted COMMENTED OUT with the reason, never as a gate
21
+ * that discards every round.
22
+ * - **Say why.** Each drafted line carries what it checks and why it was
23
+ * chosen or left out.
24
+ *
25
+ * Pure: the filesystem is injected. `listDirs` is optional — without it only
26
+ * the named common folders are probed.
27
+ */
28
+ export interface DraftFs {
29
+ /** The file's text, or null when it does not exist or cannot be read. */
30
+ readFile(path: string): string | null;
31
+ /** Child directory names of `path` ('' or '.' for the root); [] when unreadable. */
32
+ listDirs?(path: string): string[];
33
+ }
34
+ export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
35
+ export interface DetectedStep {
36
+ name: string;
37
+ run: string;
38
+ timeoutS: number;
39
+ }
40
+ export interface DraftGate extends DetectedStep {
41
+ /** Glob — the step runs only when a changed path matches it. */
42
+ when?: string;
43
+ required: boolean;
44
+ /** One line: what it checks and why it was chosen. Written above the gate. */
45
+ why: string;
46
+ /** Set when the gate is drafted commented out: the reason it was left out. */
47
+ disabled?: string;
48
+ }
49
+ /** Per-step budgets. A gate must finish well inside one round. */
50
+ export declare const TYPECHECK_TIMEOUT_S = 300;
51
+ export declare const UNIT_TEST_TIMEOUT_S = 600;
52
+ /**
53
+ * Source text with its comments removed — strings (", ' and `) kept intact.
54
+ *
55
+ * ⚠️ CONFIG PROBES MUST NOT READ COMMENTS. The real web/ app that motivated
56
+ * this file explains, in a vite.config comment, that the API's suite "boots a
57
+ * real PostgreSQL in globalSetup" — and the first version of the probe read
58
+ * that as the web suite having one, and left a fast suite out.
59
+ */
60
+ export declare function stripComments(text: string): string;
61
+ /** JSON with comments and trailing commas — what tsconfig files actually are. */
62
+ export declare function parseJsonc(text: string): unknown;
63
+ /** Script names projects actually use for this, in the order they are preferred. */
64
+ export declare const TYPECHECK_SCRIPTS: readonly ["typecheck", "type-check", "types", "tsc", "check-types"];
65
+ /** Directories one level down that hold an app or package with something to check. */
66
+ export declare function findSubPackages(fs: DraftFs, workspaces: readonly string[]): string[];
67
+ export interface GateDraft {
68
+ /** The root's own typecheck, when it has one. */
69
+ typecheck: DetectedStep | null;
70
+ /** The root's own suite, when it fits a round (null when left out). */
71
+ testRunner: DetectedStep | null;
72
+ subPackages: string[];
73
+ /** Every drafted gate in execution order, the left-out ones included. */
74
+ gates: DraftGate[];
75
+ }
76
+ export declare function draftGates(fs: DraftFs, opts: {
77
+ packageManager: PackageManager;
78
+ workspaces: readonly string[];
79
+ }): GateDraft;
80
+ export declare function gateLine(g: DraftGate): string;
81
+ /**
82
+ * What a gate is and what `required` means — said where the person edits them.
83
+ *
84
+ * ⚠️ `required: false` DOES NOT MAKE A GATE ADVISORY. Every step that runs must
85
+ * pass, and the first red one discards the round (`gate.ts`, `mayCommit`).
86
+ * `required` only adds that the step must have RUN: a required step a `when:`
87
+ * filter skipped blocks the commit too. A person who believed otherwise left a
88
+ * 30-minute suite in place as "not required" and lost every round to it.
89
+ */
90
+ export declare const GATES_HEADER: string;
91
+ /**
92
+ * The gate list as it appears in the file: one comment line per gate saying
93
+ * what it checks and why, left-out suites commented out with their reason.
94
+ *
95
+ * `header` is the comment block written directly above `gates:`, so the whole
96
+ * section can be re-drafted in place by `init --refresh-gates`.
97
+ */
98
+ export declare function renderGatesSection(gates: readonly DraftGate[], header: string): string;
99
+ //# sourceMappingURL=gate-draft.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-draft.d.ts","sourceRoot":"","sources":["../src/gate-draft.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,WAAW,OAAO;IACtB,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACtC,oFAAoF;IACpF,QAAQ,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAE7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAU,SAAQ,YAAY;IAC7C,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,kEAAkE;AAClE,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAiCvC;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA0BlD;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAkFD,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,qEAAsE,CAAC;AA6OrG,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CA0BpF;AAID,MAAM,WAAW,SAAS;IACxB,iDAAiD;IACjD,SAAS,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,uEAAuE;IACvE,UAAU,EAAE,YAAY,GAAG,IAAI,CAAC;IAChC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,yEAAyE;IACzE,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,wBAAgB,UAAU,CACxB,EAAE,EAAE,OAAO,EACX,IAAI,EAAE;IAAE,cAAc,EAAE,cAAc,CAAC;IAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACtE,SAAS,CAoEX;AAOD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAI7C;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,QAYb,CAAC;AAEb;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAiBtF"}
@@ -0,0 +1,544 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GATES_HEADER = exports.TYPECHECK_SCRIPTS = exports.UNIT_TEST_TIMEOUT_S = exports.TYPECHECK_TIMEOUT_S = void 0;
4
+ exports.stripComments = stripComments;
5
+ exports.parseJsonc = parseJsonc;
6
+ exports.findSubPackages = findSubPackages;
7
+ exports.draftGates = draftGates;
8
+ exports.gateLine = gateLine;
9
+ exports.renderGatesSection = renderGatesSection;
10
+ const glob_1 = require("./glob");
11
+ /** Per-step budgets. A gate must finish well inside one round. */
12
+ exports.TYPECHECK_TIMEOUT_S = 300;
13
+ exports.UNIT_TEST_TIMEOUT_S = 600;
14
+ /** What a slow suite would have needed; only ever written commented out. */
15
+ const SLOW_SUITE_TIMEOUT_S = 900;
16
+ /** Folders that hold an app of their own in real repositories. */
17
+ const COMMON_APP_DIRS = ['web', 'admin', 'app', 'client', 'frontend', 'server', 'api'];
18
+ /** Folders whose children are packages, whether or not workspaces declare them. */
19
+ const PACKAGE_PARENTS = ['packages', 'apps'];
20
+ /** Never an app: build output, dependencies, tooling state. */
21
+ const IGNORED_DIRS = new Set([
22
+ 'node_modules',
23
+ 'dist',
24
+ 'build',
25
+ 'out',
26
+ 'coverage',
27
+ 'tmp',
28
+ 'temp',
29
+ 'vendor',
30
+ 'target',
31
+ 'bin',
32
+ 'obj',
33
+ 'public',
34
+ 'static',
35
+ 'assets',
36
+ 'docs',
37
+ 'scripts',
38
+ ]);
39
+ /** More than this and the gate list stops being something a person reads. */
40
+ const MAX_SUB_PACKAGES = 12;
41
+ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
42
+ /**
43
+ * Source text with its comments removed — strings (", ' and `) kept intact.
44
+ *
45
+ * ⚠️ CONFIG PROBES MUST NOT READ COMMENTS. The real web/ app that motivated
46
+ * this file explains, in a vite.config comment, that the API's suite "boots a
47
+ * real PostgreSQL in globalSetup" — and the first version of the probe read
48
+ * that as the web suite having one, and left a fast suite out.
49
+ */
50
+ function stripComments(text) {
51
+ let out = '';
52
+ let quote = null;
53
+ for (let i = 0; i < text.length; i++) {
54
+ const c = text[i];
55
+ if (quote) {
56
+ out += c;
57
+ if (c === '\\') {
58
+ out += text[i + 1] ?? '';
59
+ i++;
60
+ }
61
+ else if (c === quote)
62
+ quote = null;
63
+ continue;
64
+ }
65
+ if (c === '"' || c === "'" || c === '`') {
66
+ quote = c;
67
+ out += c;
68
+ }
69
+ else if (c === '/' && text[i + 1] === '/') {
70
+ while (i < text.length && text[i] !== '\n')
71
+ i++;
72
+ out += '\n';
73
+ }
74
+ else if (c === '/' && text[i + 1] === '*') {
75
+ i += 2;
76
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
77
+ i++;
78
+ i++;
79
+ }
80
+ else
81
+ out += c;
82
+ }
83
+ return out;
84
+ }
85
+ /** JSON with comments and trailing commas — what tsconfig files actually are. */
86
+ function parseJsonc(text) {
87
+ return JSON.parse(stripComments(text).replace(/,(\s*[}\]])/g, '$1'));
88
+ }
89
+ function readJson(fs, path) {
90
+ const text = fs.readFile(path);
91
+ if (text === null)
92
+ return null;
93
+ try {
94
+ const v = parseJsonc(text);
95
+ return isObj(v) ? v : null;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ const join = (dir, file) => (dir ? `${dir}/${file}` : file);
102
+ function loadPkg(fs, dir) {
103
+ const json = readJson(fs, join(dir, 'package.json'));
104
+ const scripts = json && isObj(json['scripts']) ? json['scripts'] : {};
105
+ const dependencies = json?.['dependencies'];
106
+ const devDependencies = json?.['devDependencies'];
107
+ const deps = {
108
+ ...(isObj(dependencies) ? dependencies : {}),
109
+ ...(isObj(devDependencies) ? devDependencies : {}),
110
+ };
111
+ return { dir, json, scripts, deps };
112
+ }
113
+ const script = (pkg, name) => {
114
+ const s = pkg.scripts[name];
115
+ return typeof s === 'string' && s.trim() ? s.trim() : null;
116
+ };
117
+ /** `cd web && …` — gates run from the repository root, and the config has no cwd field. */
118
+ const inDir = (dir, cmd) => (dir ? `cd ${dir} && ${cmd}` : cmd);
119
+ /** How a local tool's entry point is reached from the package dir, or null when node_modules lacks it. */
120
+ const BIN_ENTRIES = {
121
+ tsc: 'typescript/bin/tsc',
122
+ vitest: 'vitest/vitest.mjs',
123
+ jest: 'jest/bin/jest.js',
124
+ };
125
+ function localBin(fs, dir, tool) {
126
+ const entry = `node_modules/${BIN_ENTRIES[tool]}`;
127
+ if (fs.readFile(join(dir, entry)) !== null)
128
+ return entry;
129
+ // Hoisted to the root by a workspace install.
130
+ if (dir && fs.readFile(entry) !== null) {
131
+ const up = dir.split('/').map(() => '..').join('/');
132
+ return `${up}/${entry}`;
133
+ }
134
+ return null;
135
+ }
136
+ /**
137
+ * `node <entry> <args>` when the drafting can see the tool's own entry point —
138
+ * it does not depend on npx resolution, which is fragile inside the Windows
139
+ * sandbox — and `npx --no-install <tool> <args>` when it cannot.
140
+ */
141
+ function toolCommand(fs, dir, tool, args) {
142
+ const bin = localBin(fs, dir, tool);
143
+ const tail = args ? ` ${args}` : '';
144
+ return bin ? { cmd: `node ${bin}${tail}`, local: true } : { cmd: `npx --no-install ${tool}${tail}`, local: false };
145
+ }
146
+ /** A script that is exactly one call of `tool`, with no shell around it: its args, else null. */
147
+ function plainToolArgs(text, tool) {
148
+ if (/[&|;<>`$]/.test(text))
149
+ return null;
150
+ const m = new RegExp(`^${tool}(?:\\s+(.*))?$`).exec(text);
151
+ return m ? (m[1] ?? '').trim() : null;
152
+ }
153
+ /* ─────────────────────────────── typecheck ─────────────────────────────── */
154
+ /** Script names projects actually use for this, in the order they are preferred. */
155
+ exports.TYPECHECK_SCRIPTS = ['typecheck', 'type-check', 'types', 'tsc', 'check-types'];
156
+ function tsconfigShape(fs, dir) {
157
+ const text = fs.readFile(join(dir, 'tsconfig.json'));
158
+ if (text === null)
159
+ return { exists: false, references: false, solution: false };
160
+ try {
161
+ const parsed = parseJsonc(text);
162
+ const refs = isObj(parsed) && Array.isArray(parsed['references']) && parsed['references'].length > 0;
163
+ const files = isObj(parsed) ? parsed['files'] : undefined;
164
+ const emptyFiles = Array.isArray(files) && files.length === 0;
165
+ return { exists: true, references: refs, solution: refs && emptyFiles };
166
+ }
167
+ catch {
168
+ // Unparseable even as JSONC: probe for the words rather than give up.
169
+ const refs = text.includes('"references"');
170
+ return { exists: true, references: refs, solution: refs && /"files"\s*:\s*\[\s*\]/.test(text) };
171
+ }
172
+ }
173
+ /** Whether a tsc argument string names a project or build mode. */
174
+ const namesProject = (args) => /(^|\s)(-p|--project|-b|--build)(\s|=|$)/.test(args);
175
+ /** The typecheck the tsconfig layout implies, for a package with no usable script. */
176
+ function tsconfigTypecheck(fs, pkg, shape) {
177
+ if (shape.solution) {
178
+ // A Vite-style solution file: `tsc --noEmit` against it reads no source and
179
+ // exits 0. Check the app project, or build the references.
180
+ if (fs.readFile(join(pkg.dir, 'tsconfig.app.json')) !== null) {
181
+ return {
182
+ args: '-p tsconfig.app.json --noEmit',
183
+ why: 'tsconfig.json is a solution file (files: []), so the app project tsconfig.app.json is the one checked',
184
+ };
185
+ }
186
+ return { args: '-b', why: 'tsconfig.json is a solution file (files: []), so its references are built' };
187
+ }
188
+ const build = script(pkg, 'build');
189
+ const buildArgs = build ? /(?:^|&&\s*)tsc\s+([^&|;]*)/.exec(build)?.[1]?.trim() : undefined;
190
+ if (buildArgs !== undefined && /(^|\s)(-b|--build)(\s|$)/.test(buildArgs)) {
191
+ return { args: '-b', why: 'the package builds with `tsc -b`, so the gate does the same' };
192
+ }
193
+ if (buildArgs !== undefined && /(^|\s)(-p|--project)\s+\S+/.test(buildArgs)) {
194
+ const project = /(?:-p|--project)\s+(\S+)/.exec(buildArgs)[1];
195
+ return { args: `-p ${project} --noEmit`, why: `the package builds with \`tsc -p ${project}\`, so that project is checked` };
196
+ }
197
+ if (shape.references)
198
+ return { args: '-b', why: 'tsconfig.json has project references, so they are built' };
199
+ return { args: '--noEmit', why: 'type errors in tsconfig.json' };
200
+ }
201
+ function draftTypecheck(fs, pkg) {
202
+ const where = pkg.dir ? `${pkg.dir}/` : 'the root';
203
+ const shape = tsconfigShape(fs, pkg.dir);
204
+ for (const name of exports.TYPECHECK_SCRIPTS) {
205
+ const text = script(pkg, name);
206
+ if (!text)
207
+ continue;
208
+ const args = plainToolArgs(text, 'tsc');
209
+ if (args === null) {
210
+ // vue-tsc, svelte-check, a composite script: the repo's own script is the
211
+ // source of truth and npm is how it is reached.
212
+ return {
213
+ step: { name: 'typecheck', run: inDir(pkg.dir, `npm run ${name} --silent`), timeoutS: exports.TYPECHECK_TIMEOUT_S },
214
+ why: `TypeScript in ${where}, through the package's own "${name}" script`,
215
+ };
216
+ }
217
+ if (shape.solution && !namesProject(args)) {
218
+ // The trap the real user hit in admin/: `tsc --noEmit` against a solution
219
+ // file checks nothing and always passes. Do not trust it.
220
+ const t = tsconfigTypecheck(fs, pkg, shape);
221
+ const { cmd } = toolCommand(fs, pkg.dir, 'tsc', t.args);
222
+ return {
223
+ step: { name: 'typecheck', run: inDir(pkg.dir, cmd), timeoutS: exports.TYPECHECK_TIMEOUT_S },
224
+ why: `TypeScript in ${where}; its "${name}" script (\`${text}\`) checks nothing — ${t.why}`,
225
+ };
226
+ }
227
+ const { cmd, local } = toolCommand(fs, pkg.dir, 'tsc', args);
228
+ return {
229
+ step: {
230
+ name: 'typecheck',
231
+ run: inDir(pkg.dir, local ? cmd : `npm run ${name} --silent`),
232
+ timeoutS: exports.TYPECHECK_TIMEOUT_S,
233
+ },
234
+ why: `TypeScript in ${where}: the package's own "${name}" script (\`${text}\`)${local ? ', run with its local tsc' : ''}`,
235
+ };
236
+ }
237
+ if (!shape.exists)
238
+ return null;
239
+ const t = tsconfigTypecheck(fs, pkg, shape);
240
+ const { cmd } = toolCommand(fs, pkg.dir, 'tsc', t.args);
241
+ return {
242
+ step: { name: 'tsc', run: inDir(pkg.dir, cmd), timeoutS: exports.TYPECHECK_TIMEOUT_S },
243
+ why: `TypeScript in ${where}: ${t.why}`,
244
+ };
245
+ }
246
+ /* ─────────────────────────────── tests ─────────────────────────────── */
247
+ const JEST_CONFIGS = ['jest.config.js', 'jest.config.ts', 'jest.config.cjs', 'jest.config.mjs', 'jest.config.json'];
248
+ const VITEST_CONFIGS = [
249
+ 'vitest.config.ts',
250
+ 'vitest.config.js',
251
+ 'vitest.config.mts',
252
+ 'vitest.config.mjs',
253
+ 'vite.config.ts',
254
+ 'vite.config.js',
255
+ 'vite.config.mts',
256
+ 'vite.config.mjs',
257
+ ];
258
+ /** Script names for a fast slice, tried when `test` itself is too slow for a round. */
259
+ const FAST_TEST_SCRIPTS = ['test:unit', 'unit', 'test:fast'];
260
+ /**
261
+ * Why a suite cannot be a per-round gate, or [] when nothing says so.
262
+ *
263
+ * Only what the repository states: the script's flags and name, the runner's
264
+ * config, and its dependencies. Each reason is one the person can check.
265
+ */
266
+ function slowSuiteReasons(fs, pkg, scriptText, runner) {
267
+ const reasons = [];
268
+ if (scriptText) {
269
+ if (/--runInBand\b|(^|\s)-i(\s|$)/.test(scriptText))
270
+ reasons.push('runs in band (one test file at a time)');
271
+ const heap = /--max-old-space-size=(\d+)/.exec(scriptText);
272
+ if (heap && Number(heap[1]) >= 4096)
273
+ reasons.push(`needs a ${heap[1]} MB heap`);
274
+ const kind = /\b(e2e|integration|realdb|playwright|cypress)\b/i.exec(scriptText);
275
+ if (kind)
276
+ reasons.push(`runs ${kind[1].toLowerCase()} tests`);
277
+ }
278
+ const configTexts = [];
279
+ if (runner === 'jest' || runner === null) {
280
+ if (isObj(pkg.json?.['jest']))
281
+ configTexts.push(JSON.stringify(pkg.json['jest']));
282
+ for (const f of JEST_CONFIGS) {
283
+ const t = fs.readFile(join(pkg.dir, f));
284
+ if (t !== null)
285
+ configTexts.push(stripComments(t));
286
+ }
287
+ }
288
+ if (runner === 'vitest' || runner === null) {
289
+ for (const f of VITEST_CONFIGS) {
290
+ const t = fs.readFile(join(pkg.dir, f));
291
+ if (t !== null)
292
+ configTexts.push(stripComments(t));
293
+ }
294
+ }
295
+ const config = configTexts.join('\n');
296
+ if (/\bglobalSetup\b/.test(config))
297
+ reasons.push('its config has a globalSetup that starts something before the suite');
298
+ if (/\bmaxWorkers['"]?\s*:\s*['"]?1\b/.test(config))
299
+ reasons.push('its config limits it to one worker');
300
+ if (/testcontainers|docker/i.test(config) || Object.keys(pkg.deps).some((d) => /testcontainers/.test(d))) {
301
+ reasons.push('it starts containers');
302
+ }
303
+ return [...new Set(reasons)];
304
+ }
305
+ function runnerOf(pkg, text) {
306
+ if (text && /\bvitest\b/.test(text))
307
+ return 'vitest';
308
+ if (text && /\bjest\b/.test(text))
309
+ return 'jest';
310
+ if ('jest' in pkg.deps)
311
+ return 'jest';
312
+ if ('vitest' in pkg.deps)
313
+ return 'vitest';
314
+ return null;
315
+ }
316
+ function draftTests(fs, pkg, pm) {
317
+ const where = pkg.dir ? `${pkg.dir}/` : 'the root';
318
+ let scriptName = 'test';
319
+ let text = script(pkg, 'test');
320
+ // `npm init` seeds a test script that always exits 1. A gate built on it
321
+ // would fail every round of every run — that is the absence of a runner.
322
+ if (text && text.includes('no test specified'))
323
+ text = null;
324
+ let runner = runnerOf(pkg, text);
325
+ if (!text && !runner)
326
+ return null;
327
+ let slow = slowSuiteReasons(fs, pkg, text, runner);
328
+ let fastNote = '';
329
+ if (slow.length) {
330
+ // The repository may already name a fast slice. Prefer it to nothing.
331
+ for (const name of FAST_TEST_SCRIPTS) {
332
+ const alt = script(pkg, name);
333
+ if (!alt)
334
+ continue;
335
+ const altRunner = runnerOf(pkg, alt);
336
+ if (slowSuiteReasons(fs, pkg, alt, altRunner).length)
337
+ continue;
338
+ fastNote = `; "test" is too slow for a round (${slow.join(', ')}), so the "${name}" slice is used`;
339
+ scriptName = name;
340
+ text = alt;
341
+ runner = altRunner;
342
+ slow = [];
343
+ break;
344
+ }
345
+ }
346
+ const name = runner ?? 'test';
347
+ const timeoutS = slow.length ? SLOW_SUITE_TIMEOUT_S : exports.UNIT_TEST_TIMEOUT_S;
348
+ let run;
349
+ let how;
350
+ const plain = text && runner ? plainToolArgs(text, runner) : null;
351
+ if (runner && (plain !== null || !text)) {
352
+ // The script is exactly one runner call (or there is no script): run the
353
+ // runner directly, in its non-watch, CI form.
354
+ let args = plain ?? '';
355
+ if (runner === 'vitest' && !/(^|\s)(run|--run)(\s|$)/.test(args))
356
+ args = `run${args ? ` ${args}` : ''}`;
357
+ if (runner === 'jest' && !/(^|\s)--ci(\s|$)/.test(args))
358
+ args = `${args ? `${args} ` : ''}--ci`;
359
+ // A forked worker needs an IPC pipe, which the Windows gate sandbox cannot
360
+ // give it (win/contained-node.cjs fails that fast); worker threads and an
361
+ // in-band run need none, and behave the same on every platform.
362
+ if (runner === 'vitest' && !/(^|\s)--pool(=|\s)/.test(args))
363
+ args = `${args} --pool=threads`;
364
+ if (runner === 'jest' && !/(^|\s)(-i|--runInBand|-w|--maxWorkers)(=|\s|$)/.test(args))
365
+ args = `${args} --runInBand`;
366
+ const t = toolCommand(fs, pkg.dir, runner, args.trim());
367
+ run = inDir(pkg.dir, t.cmd);
368
+ how = text ? `its "${scriptName}" script (\`${text}\`)${t.local ? `, run with its local ${runner}` : ''}` : `${runner}, from its dependencies`;
369
+ }
370
+ else {
371
+ run = inDir(pkg.dir, scriptName === 'test' ? `${pm} test` : `${pm} run ${scriptName}`);
372
+ how = `its "${scriptName}" script (\`${text}\`)`;
373
+ }
374
+ const step = { name, run, timeoutS };
375
+ if (slow.length) {
376
+ return {
377
+ step,
378
+ why: `unit tests in ${where}: ${how}`,
379
+ disabled: `left out — ${slow.join(', ')}; a suite like that cannot finish inside a round, so every round would be discarded. Run it yourself before you release.`,
380
+ };
381
+ }
382
+ return { step, why: `${runner ? `${runner} tests` : 'tests'} in ${where}: ${how}${fastNote}` };
383
+ }
384
+ /* ─────────────────────────────── sub-packages ─────────────────────────────── */
385
+ function usableDir(name) {
386
+ return !!name && !name.startsWith('.') && !IGNORED_DIRS.has(name);
387
+ }
388
+ /** Directories one level down that hold an app or package with something to check. */
389
+ function findSubPackages(fs, workspaces) {
390
+ const list = (p) => (fs.listDirs ? fs.listDirs(p).filter(usableDir) : []);
391
+ const candidates = new Set();
392
+ for (const d of list(''))
393
+ candidates.add(d);
394
+ for (const d of COMMON_APP_DIRS)
395
+ candidates.add(d);
396
+ const parents = new Set(PACKAGE_PARENTS);
397
+ for (const ws of workspaces) {
398
+ const clean = ws.replace(/^\.\//, '').replace(/\/+$/, '');
399
+ if (clean.endsWith('/*') && !clean.slice(0, -2).includes('*'))
400
+ parents.add(clean.slice(0, -2));
401
+ else if (!clean.includes('*') && clean)
402
+ candidates.add(clean);
403
+ }
404
+ for (const parent of parents) {
405
+ candidates.delete(parent);
406
+ for (const child of list(parent))
407
+ candidates.add(`${parent}/${child}`);
408
+ }
409
+ const found = [];
410
+ for (const dir of [...candidates].sort()) {
411
+ if (dir.split('/').some((seg) => !usableDir(seg)))
412
+ continue;
413
+ const pkg = loadPkg(fs, dir);
414
+ if (!pkg.json)
415
+ continue;
416
+ const hasScript = ['typecheck', 'type-check', 'build', 'test'].some((s) => script(pkg, s) !== null);
417
+ const hasTsconfig = fs.readFile(join(dir, 'tsconfig.json')) !== null;
418
+ if (hasScript || hasTsconfig)
419
+ found.push(dir);
420
+ }
421
+ return found;
422
+ }
423
+ function draftGates(fs, opts) {
424
+ const root = loadPkg(fs, '');
425
+ const rootTc = draftTypecheck(fs, root);
426
+ const rootTest = draftTests(fs, root, opts.packageManager);
427
+ const subPackages = findSubPackages(fs, opts.workspaces);
428
+ const allSubs = subPackages.slice(0, MAX_SUB_PACKAGES);
429
+ const typechecks = [];
430
+ const tests = [];
431
+ if (rootTc)
432
+ typechecks.push({ ...rootTc.step, required: true, why: rootTc.why });
433
+ for (const dir of allSubs) {
434
+ const pkg = loadPkg(fs, dir);
435
+ const member = opts.workspaces.some((w) => (0, glob_1.matchesGlob)(dir, w.replace(/^\.\//, '').replace(/\/+$/, '')));
436
+ // A workspace member is part of one build: a change anywhere can break it,
437
+ // so its checks are unconditional, and the root's own covers it when there
438
+ // is one. A standalone app (its own install, not a workspace) is checked
439
+ // when something inside it changed — an API-only round should not pay for
440
+ // a frontend build, and a UI round must not go unchecked.
441
+ const when = member ? undefined : `${dir}/**`;
442
+ const scopeNote = member
443
+ ? 'a workspace package'
444
+ : `its own app (own package.json); runs only when something under ${dir}/ changed`;
445
+ if (!(member && rootTc)) {
446
+ const tc = draftTypecheck(fs, pkg);
447
+ if (tc) {
448
+ typechecks.push({
449
+ ...tc.step,
450
+ name: `${dir}:${tc.step.name}`,
451
+ ...(when ? { when } : {}),
452
+ required: !when,
453
+ why: `${tc.why} — ${scopeNote}`,
454
+ });
455
+ }
456
+ }
457
+ if (!(member && rootTest && !rootTest.disabled)) {
458
+ const t = draftTests(fs, pkg, opts.packageManager);
459
+ if (t) {
460
+ tests.push({
461
+ ...t.step,
462
+ name: `${dir}:${t.step.name}`,
463
+ ...(when ? { when } : {}),
464
+ required: !when,
465
+ why: `${t.why} — ${scopeNote}`,
466
+ ...(t.disabled ? { disabled: t.disabled } : {}),
467
+ });
468
+ }
469
+ }
470
+ }
471
+ if (rootTest) {
472
+ tests.push({
473
+ ...rootTest.step,
474
+ required: true,
475
+ why: rootTest.why,
476
+ ...(rootTest.disabled ? { disabled: rootTest.disabled } : {}),
477
+ });
478
+ }
479
+ // Cheapest first — the order in the file is the execution order (DESIGN §4):
480
+ // a broken typecheck should not wait on a test suite.
481
+ return {
482
+ typecheck: rootTc?.step ?? null,
483
+ testRunner: rootTest && !rootTest.disabled ? rootTest.step : null,
484
+ subPackages,
485
+ gates: [...typechecks, ...tests],
486
+ };
487
+ }
488
+ /* ─────────────────────────────── rendering ─────────────────────────────── */
489
+ /** YAML double-quoted scalars are a superset of JSON strings, so this is safe. */
490
+ const yamlStr = (s) => JSON.stringify(s);
491
+ function gateLine(g) {
492
+ const when = g.when ? `, when: ${yamlStr(g.when)}` : '';
493
+ const req = g.required ? ', required: true' : '';
494
+ return `- { name: ${yamlStr(g.name)}, run: ${yamlStr(g.run)}, timeout_s: ${g.timeoutS}${when}${req} }`;
495
+ }
496
+ /**
497
+ * What a gate is and what `required` means — said where the person edits them.
498
+ *
499
+ * ⚠️ `required: false` DOES NOT MAKE A GATE ADVISORY. Every step that runs must
500
+ * pass, and the first red one discards the round (`gate.ts`, `mayCommit`).
501
+ * `required` only adds that the step must have RUN: a required step a `when:`
502
+ * filter skipped blocks the commit too. A person who believed otherwise left a
503
+ * 30-minute suite in place as "not required" and lost every round to it.
504
+ */
505
+ exports.GATES_HEADER = [
506
+ "# Gates decide whether a round's work is kept. They run top to bottom, cheapest",
507
+ '# first, and EVERY step that runs must pass: the first failure (or timeout)',
508
+ '# discards the round. There is no advisory gate.',
509
+ '#',
510
+ '# required: true adds one condition: the step must actually have run. A required',
511
+ '# step that its `when:` filter skipped blocks the commit, so steps with `when:`',
512
+ '# are not required. required: false does NOT make a step optional; to stop',
513
+ '# checking something, delete or comment out its line.',
514
+ '#',
515
+ '# Each gate must finish well inside one round. A suite that cannot (in band,',
516
+ '# huge heap, database or container setup) is drafted commented out, with why.',
517
+ ].join('\n');
518
+ /**
519
+ * The gate list as it appears in the file: one comment line per gate saying
520
+ * what it checks and why, left-out suites commented out with their reason.
521
+ *
522
+ * `header` is the comment block written directly above `gates:`, so the whole
523
+ * section can be re-drafted in place by `init --refresh-gates`.
524
+ */
525
+ function renderGatesSection(gates, header) {
526
+ const lines = [header, 'gates:'];
527
+ const active = gates.filter((g) => !g.disabled);
528
+ for (const g of gates) {
529
+ lines.push(` # ${g.name}: ${g.why}`);
530
+ if (g.disabled) {
531
+ lines.push(` # ${g.disabled}`, ` # ${gateLine(g)}`);
532
+ }
533
+ else {
534
+ lines.push(` ${gateLine(g)}`);
535
+ }
536
+ }
537
+ if (!active.length) {
538
+ // `gates:` with only comments under it reads as null, and the loader says
539
+ // so — a config with no gate must not look like one that has them.
540
+ lines.push(' # none detected that can run inside a round (nothing detected) — add at least one, or a green round proves nothing');
541
+ }
542
+ return lines.join('\n');
543
+ }
544
+ //# sourceMappingURL=gate-draft.js.map