@hominis/fireforge 0.47.0 → 0.48.0

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 (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/bin/fireforge.js +11 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
  5. package/dist/src/commands/doctor-macos-sdk.js +156 -0
  6. package/dist/src/commands/doctor.js +2 -0
  7. package/dist/src/commands/download.js +64 -3
  8. package/dist/src/commands/import.js +5 -2
  9. package/dist/src/commands/lint-per-patch.js +44 -6
  10. package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
  11. package/dist/src/commands/lint-per-run-checkjs.js +1 -0
  12. package/dist/src/commands/lint.js +10 -0
  13. package/dist/src/commands/re-export-lint.d.ts +17 -2
  14. package/dist/src/commands/re-export-lint.js +33 -4
  15. package/dist/src/commands/re-export.js +4 -2
  16. package/dist/src/commands/rebase/confirm.d.ts +12 -1
  17. package/dist/src/commands/rebase/confirm.js +6 -3
  18. package/dist/src/commands/rebase/index.js +77 -13
  19. package/dist/src/commands/run.js +43 -7
  20. package/dist/src/commands/status.js +5 -3
  21. package/dist/src/commands/test-harness-teardown.d.ts +62 -0
  22. package/dist/src/commands/test-harness-teardown.js +128 -0
  23. package/dist/src/commands/test-register.js +2 -1
  24. package/dist/src/commands/test-run.d.ts +2 -0
  25. package/dist/src/commands/test-run.js +36 -7
  26. package/dist/src/commands/test-verdict.d.ts +19 -4
  27. package/dist/src/commands/test-verdict.js +22 -4
  28. package/dist/src/commands/test.js +18 -10
  29. package/dist/src/commands/typecheck.d.ts +5 -0
  30. package/dist/src/commands/typecheck.js +7 -1
  31. package/dist/src/core/build-prepare.d.ts +2 -0
  32. package/dist/src/core/build-prepare.js +67 -35
  33. package/dist/src/core/config-paths.d.ts +1 -1
  34. package/dist/src/core/config-paths.js +2 -0
  35. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  36. package/dist/src/core/config-validate.js +7 -0
  37. package/dist/src/core/discard-baseline.js +2 -1
  38. package/dist/src/core/file-lock.js +16 -9
  39. package/dist/src/core/forward-registrations.d.ts +33 -0
  40. package/dist/src/core/forward-registrations.js +16 -0
  41. package/dist/src/core/git-file-ops.d.ts +21 -0
  42. package/dist/src/core/git-file-ops.js +75 -0
  43. package/dist/src/core/git-status.d.ts +14 -0
  44. package/dist/src/core/git-status.js +41 -2
  45. package/dist/src/core/harness-helper-pids.d.ts +98 -0
  46. package/dist/src/core/harness-helper-pids.js +240 -0
  47. package/dist/src/core/harness-orphans.d.ts +53 -8
  48. package/dist/src/core/harness-orphans.js +125 -44
  49. package/dist/src/core/mach-mozconfig.d.ts +2 -1
  50. package/dist/src/core/mach-mozconfig.js +2 -0
  51. package/dist/src/core/mach.d.ts +16 -0
  52. package/dist/src/core/mach.js +6 -1
  53. package/dist/src/core/marionette-preflight.js +9 -3
  54. package/dist/src/core/parent-exit-watchdog.d.ts +62 -0
  55. package/dist/src/core/parent-exit-watchdog.js +69 -0
  56. package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
  57. package/dist/src/core/patch-apply-fuzz.js +27 -6
  58. package/dist/src/core/patch-apply.d.ts +13 -0
  59. package/dist/src/core/patch-apply.js +27 -2
  60. package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
  61. package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
  62. package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
  63. package/dist/src/core/patch-lint-checkjs.js +8 -1
  64. package/dist/src/core/patch-lint-cross.d.ts +7 -1
  65. package/dist/src/core/patch-lint-cross.js +13 -12
  66. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
  67. package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
  68. package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
  69. package/dist/src/core/patch-lint-forward-registration.js +83 -37
  70. package/dist/src/core/patch-lint.d.ts +11 -0
  71. package/dist/src/core/patch-lint.js +20 -2
  72. package/dist/src/core/patch-transform.d.ts +14 -13
  73. package/dist/src/core/patch-transform.js +29 -17
  74. package/dist/src/core/rebase-dry-run.d.ts +48 -0
  75. package/dist/src/core/rebase-dry-run.js +105 -0
  76. package/dist/src/core/run-profile.d.ts +50 -0
  77. package/dist/src/core/run-profile.js +131 -0
  78. package/dist/src/core/status-binary.d.ts +1 -1
  79. package/dist/src/core/status-binary.js +3 -1
  80. package/dist/src/core/status-classify.js +29 -2
  81. package/dist/src/core/toolchain-preflight.d.ts +6 -0
  82. package/dist/src/core/toolchain-preflight.js +1 -1
  83. package/dist/src/core/typecheck.d.ts +11 -1
  84. package/dist/src/core/typecheck.js +85 -11
  85. package/dist/src/errors/rebase.d.ts +9 -0
  86. package/dist/src/errors/rebase.js +20 -0
  87. package/dist/src/index.d.ts +3 -1
  88. package/dist/src/index.js +2 -1
  89. package/dist/src/types/commands/options.d.ts +26 -1
  90. package/dist/src/types/config.d.ts +17 -0
  91. package/dist/src/utils/process-group.d.ts +5 -1
  92. package/dist/src/utils/process-group.js +7 -3
  93. package/dist/src/utils/process-input.d.ts +27 -0
  94. package/dist/src/utils/process-input.js +49 -0
  95. package/dist/src/utils/process.d.ts +20 -0
  96. package/dist/src/utils/process.js +16 -3
  97. package/dist/src/utils/validation.d.ts +11 -0
  98. package/dist/src/utils/validation.js +43 -0
  99. package/docs/configuration.md +88 -1
  100. package/docs/exit-codes.md +1 -1
  101. package/docs/lifecycle-invariants.md +21 -4
  102. package/docs/machine-output.md +22 -9
  103. package/docs/patch-workflow.md +35 -1
  104. package/docs/testing.md +68 -4
  105. package/package.json +7 -3
@@ -11,17 +11,20 @@
11
11
  * removing the declaration produced identical silence, so the gap was
12
12
  * invisible rather than merely absent.
13
13
  *
14
- * Scope is narrow on purpose: test-manifest `support-files` only. A
15
- * jar.mn packaging line or an actor/customElements registration names its
16
- * target through indirection the linter cannot resolve without the build
17
- * system, and a rule that guesses there would refuse correct queues on a
18
- * Firefox-sized tree. The staged-registration declaration remains the
19
- * documented escape hatch for a deliberate stage.
14
+ * Scope is narrow by default: test-manifest `support-files` only. The
15
+ * carriers whose target resolves without the build system (jar.mn pair
16
+ * lines, moz.build path tokens, test-manifest sections, customElements.js
17
+ * chrome URLs) are opt-in through `patchLint.forwardRegistration:
18
+ * "extended"` (see `patch-lint-forward-registration-extended.ts`), since a
19
+ * queue that predates them can carry such edges on a ratchet and a new
20
+ * error on install would red its gate. The staged-registration declaration
21
+ * remains the documented escape hatch for a deliberate stage.
20
22
  */
21
23
  import { basename, dirname, posix } from 'node:path';
22
24
  import { normalizePathSlashes } from '../utils/paths.js';
23
25
  import { escapeRegex } from '../utils/regex.js';
24
- import { isLaterOwner, quoteRegistrationLine } from './patch-lint-staged-registration.js';
26
+ import { findExtendedForwardRegistrations, formatRegistrationDeclaration, } from './patch-lint-forward-registration-extended.js';
27
+ import { isLaterOwner } from './patch-lint-staged-registration.js';
25
28
  /**
26
29
  * Manifest basenames whose `support-files` key registers auxiliary files
27
30
  * for a test suite. Closed and documented on purpose: a pattern like
@@ -142,15 +145,15 @@ function isDeclared(entry, manifestPath, createdPath) {
142
145
  normalizePathSlashes(registration.creates) === normalizePathSlashes(createdPath));
143
146
  }
144
147
  /**
145
- * Flags every `support-files` registration a patch introduces that names a
148
+ * Finds every `support-files` registration a patch introduces that names a
146
149
  * file only a later-ordered patch creates, unless the staged dependency is
147
150
  * declared.
148
151
  *
149
152
  * @param ctx - Projected queue in application order
150
- * @returns One issue per undeclared forward registration
153
+ * @returns One record per (patch, manifest, entry, created file)
151
154
  */
152
- export function lintPatchQueueForwardRegistrations(ctx) {
153
- const issues = [];
155
+ function findSupportFileForwardRegistrations(ctx) {
156
+ const found = [];
154
157
  for (const entry of ctx.entries) {
155
158
  // Both arms: a manifest this patch creates and one it modifies can each
156
159
  // introduce a registration line.
@@ -163,17 +166,6 @@ export function lintPatchQueueForwardRegistrations(ctx) {
163
166
  const matches = buildSupportFileMatcher(manifestPath, raw);
164
167
  if (matches === undefined)
165
168
  continue;
166
- // `--line` is the line as the patch adds it, which is what the
167
- // declared arm compares and what `--remove` matches on. Synthesising
168
- // `support-files = ["<entry>"]` instead only ever matched a
169
- // single-line single-entry array, so the pasted command left the
170
- // declaring patch red with staged-dependency-unused.
171
- const declaredLine = quoteRegistrationLine(addedLine);
172
- const ambiguity = occurrences > 1
173
- ? ` (the entry is spelled out on ${occurrences} added lines here; ` +
174
- 'the first is quoted, and one declaration covers the file either way ' +
175
- 'because the declared check keys on --file and --creates.)'
176
- : '';
177
169
  for (const later of ctx.entries) {
178
170
  // Same predicate the declared-registration arm validates against,
179
171
  // tiebreak included: two spellings of "later" would let one rule
@@ -185,26 +177,80 @@ export function lintPatchQueueForwardRegistrations(ctx) {
185
177
  continue;
186
178
  if (isDeclared(entry, manifestPath, created))
187
179
  continue;
188
- issues.push({
180
+ found.push({
181
+ patch: entry.filename,
182
+ kind: 'support-files',
189
183
  file: manifestPath,
190
- check: 'forward-registration',
191
- patches: [entry.filename],
192
- fingerprint: `forward-registration|${entry.filename}|${manifestPath}|${raw}|${created}|${later.filename}`,
193
- message: `${manifestPath} in ${entry.filename} registers "${raw}" as a support file, ` +
194
- `but ${created} is created by the later patch ${later.filename}. ` +
195
- 'Applying the queue up to this patch leaves the registration dangling. ' +
196
- 'Reorder the patches so the file is created first, move the registration into ' +
197
- 'the later patch, or declare the intentional staged dependency with: ' +
198
- `fireforge patch staged-dependency ${entry.filename} --add --kind registration ` +
199
- `--file ${manifestPath} --line "${declaredLine}" ` +
200
- `--creates ${created} --owner ${later.filename}` +
201
- ambiguity,
202
- severity: 'error',
184
+ // `--line` is the line as the patch adds it, which is what the
185
+ // declared arm compares and what `--remove` matches on.
186
+ // Synthesising `support-files = ["<entry>"]` instead only ever
187
+ // matched a single-line single-entry array, so the pasted
188
+ // command left the declaring patch red with
189
+ // staged-dependency-unused.
190
+ line: addedLine,
191
+ token: raw,
192
+ creates: created,
193
+ owner: later.filename,
194
+ occurrences,
195
+ command: formatRegistrationDeclaration(entry.filename, manifestPath, addedLine, created, later.filename),
203
196
  });
204
197
  }
205
198
  }
206
199
  }
207
200
  }
208
201
  }
209
- return issues;
202
+ return found;
203
+ }
204
+ /**
205
+ * Every undeclared forward registration in the queue, over the carriers
206
+ * `scope` selects: `support-files` (the default) or `extended` (adds
207
+ * jar.mn, moz.build, test-manifest sections and customElements.js; see
208
+ * `patch-lint-forward-registration-extended.ts`).
209
+ *
210
+ * @param ctx - Queue in application order
211
+ * @param scope - Which carriers to check
212
+ */
213
+ export function collectForwardRegistrations(ctx, scope = 'support-files') {
214
+ const found = findSupportFileForwardRegistrations(ctx);
215
+ if (scope === 'extended')
216
+ found.push(...findExtendedForwardRegistrations(ctx));
217
+ return found;
218
+ }
219
+ const KIND_PHRASES = {
220
+ 'support-files': 'as a support file',
221
+ 'jar-mn': 'for packaging',
222
+ 'moz-build': 'in the build',
223
+ 'test-manifest': 'as a test',
224
+ 'custom-elements': 'as a custom element',
225
+ };
226
+ /**
227
+ * Flags every forward registration the queue's scope selects, unless the
228
+ * staged dependency is declared.
229
+ *
230
+ * @param ctx - Projected queue in application order, with its
231
+ * `forwardRegistration` scope (default `support-files`)
232
+ * @returns One issue per undeclared forward registration
233
+ */
234
+ export function lintPatchQueueForwardRegistrations(ctx) {
235
+ return collectForwardRegistrations(ctx, ctx.forwardRegistration).map((found) => {
236
+ const ambiguity = found.occurrences > 1
237
+ ? ` (the entry is spelled out on ${found.occurrences} added lines here; ` +
238
+ 'the first is quoted, and one declaration covers the file either way ' +
239
+ 'because the declared check keys on --file and --creates.)'
240
+ : '';
241
+ return {
242
+ file: found.file,
243
+ check: 'forward-registration',
244
+ patches: [found.patch],
245
+ fingerprint: `forward-registration|${found.patch}|${found.file}|${found.token}|${found.creates}|${found.owner}`,
246
+ message: `${found.file} in ${found.patch} registers "${found.token}" ${KIND_PHRASES[found.kind]}, ` +
247
+ `but ${found.creates} is created by the later patch ${found.owner}. ` +
248
+ 'Applying the queue up to this patch leaves the registration dangling. ' +
249
+ 'Reorder the patches so the file is created first, move the registration into ' +
250
+ 'the later patch, or declare the intentional staged dependency with: ' +
251
+ found.command +
252
+ ambiguity,
253
+ severity: 'error',
254
+ };
255
+ });
210
256
  }
@@ -25,6 +25,17 @@ export declare function countNonBinaryDiffLines(diffContent: string): {
25
25
  total: number;
26
26
  textLines: number;
27
27
  };
28
+ /**
29
+ * The files the `file-too-large` rule measures for a patch: the JS files
30
+ * (`.js`, `.mjs`, `.sys.mjs`, …) the patch creates. A modified file is
31
+ * never measured, and neither is a new file of another kind (CSS, FTL,
32
+ * markup), however long. This is the rule's own predicate, not a mirror
33
+ * of it: the rule selects its files through the same function.
34
+ *
35
+ * @param patchText - Unified-diff body of one patch
36
+ * @returns Engine-relative paths the rule would size-check
37
+ */
38
+ export declare function filesMeasuredByFileTooLarge(patchText: string): Set<string>;
28
39
  /**
29
40
  * Returns true if the file path looks like a test file.
30
41
  * Matches paths containing `/test/` or filenames starting with
@@ -189,6 +189,23 @@ function isBrandingOnlyPatch(files) {
189
189
  function isJsFile(file) {
190
190
  return JS_EXTENSIONS.some((ext) => file.endsWith(ext));
191
191
  }
192
+ /** The subset of a patch's new files that `file-too-large` measures. */
193
+ function measuredByFileTooLarge(newFiles) {
194
+ return new Set([...newFiles].filter(isJsFile));
195
+ }
196
+ /**
197
+ * The files the `file-too-large` rule measures for a patch: the JS files
198
+ * (`.js`, `.mjs`, `.sys.mjs`, …) the patch creates. A modified file is
199
+ * never measured, and neither is a new file of another kind (CSS, FTL,
200
+ * markup), however long. This is the rule's own predicate, not a mirror
201
+ * of it: the rule selects its files through the same function.
202
+ *
203
+ * @param patchText - Unified-diff body of one patch
204
+ * @returns Engine-relative paths the rule would size-check
205
+ */
206
+ export function filesMeasuredByFileTooLarge(patchText) {
207
+ return measuredByFileTooLarge(detectNewFilesInDiff(patchText));
208
+ }
192
209
  /**
193
210
  * Returns true if the file path looks like a test file.
194
211
  * Matches paths containing `/test/` or filenames starting with
@@ -387,6 +404,7 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
387
404
  return [];
388
405
  const issues = [];
389
406
  const binaryName = config.binaryName.toLowerCase();
407
+ const sizeMeasured = measuredByFileTooLarge(newFiles);
390
408
  for (const file of jsFiles) {
391
409
  const filePath = join(repoDir, file);
392
410
  if (!(await pathExists(filePath)))
@@ -404,8 +422,8 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
404
422
  severity: 'error',
405
423
  });
406
424
  }
407
- // 2. File size check (new files only)
408
- if (isNew) {
425
+ // 2. File size check (new JS files only; see filesMeasuredByFileTooLarge)
426
+ if (sizeMeasured.has(file)) {
409
427
  const lineCount = countContentLines(content);
410
428
  const isTest = isTestFile(file);
411
429
  const thresholds = resolveFileSizeThresholds(config.patchLint?.fileSizeThresholds, isTest);
@@ -4,10 +4,8 @@
4
4
  */
5
5
  /**
6
6
  * Extracts the complete file content from a "new file" patch given a raw
7
- * diff string already in memory. Callers with a patch file path should
8
- * prefer {@link extractNewFileContent}. This helper exists for code paths
9
- * that already hold the diff (e.g. the in-flight export planner) and do
10
- * not want to round-trip through the filesystem.
7
+ * diff string already in memory. A caller with only a patch file path
8
+ * reads it first; every caller holds the diff already.
11
9
  *
12
10
  * @param diff - Raw unified-diff content
13
11
  * @param targetFile - Optional target file to scope extraction to
@@ -18,6 +16,18 @@
18
16
  * binary new-file conflicts as unresolvable rather than auto-resolving.
19
17
  */
20
18
  export declare function extractNewFileContentFromDiff(diff: string, targetFile?: string): string;
19
+ /**
20
+ * {@link extractNewFileContentFromDiff} for many targets over one parse of
21
+ * the diff. Each target gets exactly what the single-target call returns;
22
+ * a target whose sections include a binary one (which that call refuses)
23
+ * is left out of the map and named in `refused`.
24
+ * @param diff - Raw unified-diff content
25
+ * @param targetFiles - Files whose created content to extract
26
+ */
27
+ export declare function extractNewFileContentsFromDiff(diff: string, targetFiles: Iterable<string>): {
28
+ contents: Map<string, string>;
29
+ refused: string[];
30
+ };
21
31
  /**
22
32
  * Projects every text new file a diff creates into a path → content map, for
23
33
  * the cross-patch lint projections that build a synthetic patch-queue entry.
@@ -39,15 +49,6 @@ export declare function extractNewFileContentFromDiff(diff: string, targetFile?:
39
49
  * call sites previously did.
40
50
  */
41
51
  export declare function buildNewFileTextProjection(diff: string): Map<string, string>;
42
- /**
43
- * Extracts the complete file content from a "new file" patch.
44
- * When targetFile is provided, only extracts content for that file
45
- * (required for multi-file patches).
46
- * @param patchPath - Path to the patch file
47
- * @param targetFile - Optional target file to scope extraction to
48
- * @returns The file content that the patch would create
49
- */
50
- export declare function extractNewFileContent(patchPath: string, targetFile?: string): Promise<string>;
51
52
  /**
52
53
  * Applies an already-read patch body to content. Batched callers that hold
53
54
  * many (patch, file) pairs use this to read each patch body once instead of
@@ -4,14 +4,11 @@
4
4
  * These operate on file content strings without filesystem side effects.
5
5
  */
6
6
  import { PatchError } from '../errors/patch.js';
7
- import { readText } from '../utils/fs.js';
8
7
  import { isNewFileInPatch, parseDiffSections, parseHunksForFile } from './patch-parse.js';
9
8
  /**
10
9
  * Extracts the complete file content from a "new file" patch given a raw
11
- * diff string already in memory. Callers with a patch file path should
12
- * prefer {@link extractNewFileContent}. This helper exists for code paths
13
- * that already hold the diff (e.g. the in-flight export planner) and do
14
- * not want to round-trip through the filesystem.
10
+ * diff string already in memory. A caller with only a patch file path
11
+ * reads it first; every caller holds the diff already.
15
12
  *
16
13
  * @param diff - Raw unified-diff content
17
14
  * @param targetFile - Optional target file to scope extraction to
@@ -29,6 +26,33 @@ export function extractNewFileContentFromDiff(diff, targetFile) {
29
26
  }
30
27
  return contentFromSections(sections);
31
28
  }
29
+ /**
30
+ * {@link extractNewFileContentFromDiff} for many targets over one parse of
31
+ * the diff. Each target gets exactly what the single-target call returns;
32
+ * a target whose sections include a binary one (which that call refuses)
33
+ * is left out of the map and named in `refused`.
34
+ * @param diff - Raw unified-diff content
35
+ * @param targetFiles - Files whose created content to extract
36
+ */
37
+ export function extractNewFileContentsFromDiff(diff, targetFiles) {
38
+ const sectionsByTarget = new Map();
39
+ for (const section of parseDiffSections(diff)) {
40
+ const list = sectionsByTarget.get(section.targetPath) ?? [];
41
+ list.push(section);
42
+ sectionsByTarget.set(section.targetPath, list);
43
+ }
44
+ const contents = new Map();
45
+ const refused = [];
46
+ for (const target of targetFiles) {
47
+ const sections = sectionsByTarget.get(target) ?? [];
48
+ if (sections.some((section) => section.isBinary)) {
49
+ refused.push(target);
50
+ continue;
51
+ }
52
+ contents.set(target, contentFromSections(sections));
53
+ }
54
+ return { contents, refused };
55
+ }
32
56
  /**
33
57
  * Walks the added (`+`) lines of already-parsed sections into file content.
34
58
  * Shared by {@link extractNewFileContentFromDiff} and
@@ -89,18 +113,6 @@ export function buildNewFileTextProjection(diff) {
89
113
  }
90
114
  return newFiles;
91
115
  }
92
- /**
93
- * Extracts the complete file content from a "new file" patch.
94
- * When targetFile is provided, only extracts content for that file
95
- * (required for multi-file patches).
96
- * @param patchPath - Path to the patch file
97
- * @param targetFile - Optional target file to scope extraction to
98
- * @returns The file content that the patch would create
99
- */
100
- export async function extractNewFileContent(patchPath, targetFile) {
101
- const content = await readText(patchPath);
102
- return extractNewFileContentFromDiff(content, targetFile);
103
- }
104
116
  /**
105
117
  * Applies an already-read patch body to content. Batched callers that hold
106
118
  * many (patch, file) pairs use this to read each patch body once instead of
@@ -0,0 +1,48 @@
1
+ /** A patch to replay, in queue order. */
2
+ export interface DryRunPatch {
3
+ filename: string;
4
+ path: string;
5
+ }
6
+ /** What the real rebase loop would do with one patch. */
7
+ export type DryRunVerdict = {
8
+ filename: string;
9
+ outcome: 'clean';
10
+ } | {
11
+ filename: string;
12
+ outcome: 'reduced-context';
13
+ step: number;
14
+ contextArg: string;
15
+ } | {
16
+ filename: string;
17
+ outcome: 'reject';
18
+ /** Files git named as not applying. Empty when stderr named none. */
19
+ files: string[];
20
+ /** git's own explanation, from the exact-context `--check`. */
21
+ detail: string;
22
+ };
23
+ /** Result of a full queue replay. */
24
+ export interface DryRunReplay {
25
+ verdicts: DryRunVerdict[];
26
+ /**
27
+ * Index of the first reject, or `undefined`. Verdicts after it were
28
+ * computed without that patch applied, so a reject there may cascade from
29
+ * the earlier one rather than be a conflict of its own.
30
+ */
31
+ firstRejectIndex: number | undefined;
32
+ }
33
+ /**
34
+ * Parses the file names out of `git apply --check` failure output. Git
35
+ * reports a context mismatch as `error: patch failed: <file>:<line>` and
36
+ * `error: <file>: patch does not apply`, and a missing or pre-existing
37
+ * target as `error: <file>: does not exist in index` /
38
+ * `error: <file>: already exists in index`.
39
+ */
40
+ export declare function parseApplyCheckFailures(stderr: string): string[];
41
+ /**
42
+ * Replays `patches` onto the engine's HEAD tree in a private index.
43
+ * @param engineDir - Engine repository root
44
+ * @param patches - Patches in queue order
45
+ * @param maxFuzz - Maximum context-reduction steps, as for the real rebase
46
+ * @returns One verdict per patch
47
+ */
48
+ export declare function replayQueueIndexOnly(engineDir: string, patches: readonly DryRunPatch[], maxFuzz: number): Promise<DryRunReplay>;
@@ -0,0 +1,105 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `rebase --dry-run`: replays the patch queue against the engine's base
4
+ * commit without touching `engine/`.
5
+ *
6
+ * The replay runs on a private index seeded from HEAD (`GIT_INDEX_FILE`
7
+ * plus `git read-tree HEAD`) and applies each patch there with
8
+ * `git apply --cached`, in queue order, so a later patch sees what the
9
+ * earlier ones did, exactly as the real loop's worktree does. Each patch
10
+ * walks the same context-reduction ladder as {@link applyPatchWithFuzz}.
11
+ * The worktree and the real index are never written. The private index is
12
+ * deleted when the replay ends.
13
+ */
14
+ import { mkdtemp, rm } from 'node:fs/promises';
15
+ import { tmpdir } from 'node:os';
16
+ import { join } from 'node:path';
17
+ import { GitError } from '../errors/git.js';
18
+ import { exec } from '../utils/process.js';
19
+ import { ensureGit } from './git-base.js';
20
+ import { assertValidMaxFuzz, contextReductionSteps } from './patch-apply-fuzz.js';
21
+ /**
22
+ * Parses the file names out of `git apply --check` failure output. Git
23
+ * reports a context mismatch as `error: patch failed: <file>:<line>` and
24
+ * `error: <file>: patch does not apply`, and a missing or pre-existing
25
+ * target as `error: <file>: does not exist in index` /
26
+ * `error: <file>: already exists in index`.
27
+ */
28
+ export function parseApplyCheckFailures(stderr) {
29
+ const files = new Set();
30
+ for (const line of stderr.split('\n')) {
31
+ const failed = /^error: patch failed: (.+):\d+$/.exec(line);
32
+ if (failed?.[1]) {
33
+ files.add(failed[1]);
34
+ continue;
35
+ }
36
+ const named = /^error: (.+?): (?:patch does not apply|does not exist in index|already exists in index)$/.exec(line);
37
+ if (named?.[1])
38
+ files.add(named[1]);
39
+ }
40
+ return [...files];
41
+ }
42
+ /**
43
+ * Replays `patches` onto the engine's HEAD tree in a private index.
44
+ * @param engineDir - Engine repository root
45
+ * @param patches - Patches in queue order
46
+ * @param maxFuzz - Maximum context-reduction steps, as for the real rebase
47
+ * @returns One verdict per patch
48
+ */
49
+ export async function replayQueueIndexOnly(engineDir, patches, maxFuzz) {
50
+ assertValidMaxFuzz(maxFuzz);
51
+ await ensureGit();
52
+ const scratch = await mkdtemp(join(tmpdir(), 'fireforge-rebase-dry-run-'));
53
+ const env = { GIT_INDEX_FILE: join(scratch, 'index') };
54
+ try {
55
+ const seeded = await exec('git', ['read-tree', 'HEAD'], { cwd: engineDir, env });
56
+ if (seeded.exitCode !== 0) {
57
+ throw new GitError(seeded.stderr.trim() || 'Could not seed a private index from HEAD', 'read-tree HEAD');
58
+ }
59
+ const ladder = contextReductionSteps(maxFuzz);
60
+ const verdicts = [];
61
+ let firstRejectIndex;
62
+ for (const patch of patches) {
63
+ let verdict;
64
+ let exactFailure = '';
65
+ for (const [step, contextArgs] of ladder.entries()) {
66
+ const check = await exec('git', ['apply', '--cached', '--check', ...contextArgs, '--', patch.path], { cwd: engineDir, env });
67
+ if (check.exitCode !== 0) {
68
+ if (step === 0)
69
+ exactFailure = check.stderr;
70
+ continue;
71
+ }
72
+ const apply = await exec('git', ['apply', '--cached', ...contextArgs, '--', patch.path], {
73
+ cwd: engineDir,
74
+ env,
75
+ });
76
+ if (apply.exitCode !== 0)
77
+ continue;
78
+ verdict =
79
+ step === 0
80
+ ? { filename: patch.filename, outcome: 'clean' }
81
+ : {
82
+ filename: patch.filename,
83
+ outcome: 'reduced-context',
84
+ step,
85
+ contextArg: contextArgs[0] ?? '',
86
+ };
87
+ break;
88
+ }
89
+ if (verdict === undefined) {
90
+ verdict = {
91
+ filename: patch.filename,
92
+ outcome: 'reject',
93
+ files: parseApplyCheckFailures(exactFailure),
94
+ detail: exactFailure.trim(),
95
+ };
96
+ firstRejectIndex ??= verdicts.length;
97
+ }
98
+ verdicts.push(verdict);
99
+ }
100
+ return { verdicts, firstRejectIndex };
101
+ }
102
+ finally {
103
+ await rm(scratch, { recursive: true, force: true });
104
+ }
105
+ }
@@ -0,0 +1,50 @@
1
+ /** Directory-name prefix of every FireForge-owned temporary run profile. */
2
+ export declare const RUN_PROFILE_PREFIX = "fireforge-run-profile-";
3
+ /** The profile a run launches with, and how to release it afterwards. */
4
+ export interface RunProfile {
5
+ /** Program parameters to append to `mach run` (empty for mach's default). */
6
+ args: string[];
7
+ /** Absolute profile directory, when one was chosen. */
8
+ dir?: string;
9
+ /** Whether FireForge created the directory and will delete it. */
10
+ temporary: boolean;
11
+ /** Removes a temporary profile. A no-op otherwise. Never throws. */
12
+ dispose: () => Promise<void>;
13
+ }
14
+ /** Inputs to {@link resolveRunProfile}. */
15
+ export interface RunProfileRequest {
16
+ /** `--profile <path>`: launch with this directory, left in place. */
17
+ profile?: string | undefined;
18
+ /** `--temp-profile`: launch with a fresh directory removed afterwards. */
19
+ tempProfile?: boolean | undefined;
20
+ /**
21
+ * Whether this is a `--smoke-exit` run. A smoke run with neither flag gets
22
+ * a temporary profile: the step exists to prove what the build does on a
23
+ * clean start, and the developer's profile-default carries whatever state
24
+ * the last session left.
25
+ */
26
+ smoke: boolean;
27
+ /** Base directory for temporary profiles (defaults to the OS tmpdir). */
28
+ tempRoot?: string | undefined;
29
+ }
30
+ /**
31
+ * Removes every temporary run profile this process still holds. Called by
32
+ * the CLI entry point's signal handler after child shutdown. Never throws.
33
+ */
34
+ export declare function removeActiveRunProfiles(): Promise<void>;
35
+ /**
36
+ * Chooses the profile for a run.
37
+ * @param request - The run's profile flags
38
+ * @returns The profile arguments and their release handle
39
+ */
40
+ export declare function resolveRunProfile(request: RunProfileRequest): Promise<RunProfile>;
41
+ /**
42
+ * Removes temporary run profiles whose owning FireForge process is gone.
43
+ * The `finally` that releases a profile covers a clean exit, a failed run
44
+ * and a forwarded SIGINT/SIGTERM, but not a SIGKILL of FireForge itself;
45
+ * this sweep is the backstop for that case. A directory whose marker is
46
+ * missing or unreadable is left alone: nothing proves it is abandoned.
47
+ * @param tempRoot - Base directory to sweep (defaults to the OS tmpdir)
48
+ * @returns The number of directories removed
49
+ */
50
+ export declare function sweepAbandonedRunProfiles(tempRoot?: string): Promise<number>;