@hominis/fireforge 0.27.1 → 0.27.3

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 (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -5
  3. package/dist/src/cli.js +5 -1
  4. package/dist/src/commands/build.js +61 -1
  5. package/dist/src/commands/doctor-working-tree.js +5 -1
  6. package/dist/src/commands/download.js +178 -112
  7. package/dist/src/commands/export-all.js +3 -2
  8. package/dist/src/commands/export-flow.d.ts +2 -0
  9. package/dist/src/commands/export-flow.js +2 -0
  10. package/dist/src/commands/export.js +5 -4
  11. package/dist/src/commands/import.js +2 -1
  12. package/dist/src/commands/re-export.js +6 -6
  13. package/dist/src/commands/rebase/continue.js +2 -0
  14. package/dist/src/commands/rebase/index.d.ts +2 -2
  15. package/dist/src/commands/rebase/index.js +9 -4
  16. package/dist/src/commands/rebase/patch-loop.js +5 -5
  17. package/dist/src/commands/rebase/summary.js +7 -2
  18. package/dist/src/commands/resolve.js +2 -1
  19. package/dist/src/commands/status-output.d.ts +13 -0
  20. package/dist/src/commands/status-output.js +186 -0
  21. package/dist/src/commands/status.js +4 -247
  22. package/dist/src/commands/verify.js +32 -16
  23. package/dist/src/core/build-prepare.js +12 -4
  24. package/dist/src/core/firefox-archive.js +7 -3
  25. package/dist/src/core/firefox-cache.d.ts +1 -1
  26. package/dist/src/core/firefox-cache.js +12 -5
  27. package/dist/src/core/firefox.js +1 -1
  28. package/dist/src/core/git.js +7 -2
  29. package/dist/src/core/ownership-table.d.ts +3 -1
  30. package/dist/src/core/ownership-table.js +31 -7
  31. package/dist/src/core/patch-export.d.ts +4 -0
  32. package/dist/src/core/patch-export.js +4 -0
  33. package/dist/src/core/patch-manifest-consistency.d.ts +1 -1
  34. package/dist/src/core/patch-manifest-consistency.js +4 -2
  35. package/dist/src/core/patch-manifest-query.d.ts +4 -3
  36. package/dist/src/core/patch-manifest-query.js +12 -4
  37. package/dist/src/core/patch-manifest-validate.js +22 -4
  38. package/dist/src/core/patch-source-metadata.d.ts +8 -0
  39. package/dist/src/core/patch-source-metadata.js +17 -0
  40. package/dist/src/core/rebase-session.d.ts +8 -3
  41. package/dist/src/core/rebase-session.js +1 -1
  42. package/dist/src/core/status-classify.d.ts +4 -1
  43. package/dist/src/core/status-classify.js +4 -5
  44. package/dist/src/errors/download.d.ts +11 -0
  45. package/dist/src/errors/download.js +33 -1
  46. package/dist/src/types/commands/patches.d.ts +9 -1
  47. package/package.json +1 -1
@@ -370,13 +370,13 @@ export async function reExportCommand(projectRoot, patches, options) {
370
370
  // which we refuse to version-stamp through.
371
371
  const shouldStamp = options.stamp === true && !isDryRun && reExported > 0 && reExported === selectedPatches.length;
372
372
  if (shouldStamp) {
373
- await stampPatchVersions(paths.patches, reExportedFilenames, config.firefox.version);
373
+ await stampPatchVersions(paths.patches, reExportedFilenames, config.firefox.version, config.firefox.product);
374
374
  }
375
375
  if (isDryRun) {
376
376
  progress.stop('Dry run complete');
377
377
  success(`[dry-run] Would re-export ${reExported} of ${selectedPatches.length} patch(es)`);
378
378
  if (options.stamp === true) {
379
- info(`[dry-run] Would stamp sourceEsrVersion=${config.firefox.version} on ${reExported} patch(es)`);
379
+ info(`[dry-run] Would stamp sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${reExported} patch(es)`);
380
380
  }
381
381
  outro('Dry run complete');
382
382
  }
@@ -384,7 +384,7 @@ export async function reExportCommand(projectRoot, patches, options) {
384
384
  progress.stop('Re-export complete');
385
385
  success(`Re-exported ${reExported} of ${selectedPatches.length} patch(es)`);
386
386
  if (shouldStamp) {
387
- success(`Stamped sourceEsrVersion=${config.firefox.version} on ${reExportedFilenames.length} patch(es)`);
387
+ success(`Stamped sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${reExportedFilenames.length} patch(es)`);
388
388
  }
389
389
  else if (options.stamp === true && reExported !== selectedPatches.length) {
390
390
  warn('--stamp was requested but some patches failed or were skipped; refusing to stamp a partial set.');
@@ -397,8 +397,8 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
397
397
  program
398
398
  .command('re-export [patches...]')
399
399
  .description('Refresh existing patch bodies (and filesAffected with --scan) from the current engine ' +
400
- 'state. Does NOT change sourceEsrVersion by default — use --stamp or run rebase for ' +
401
- 'version stamping.')
400
+ 'state. Does NOT change sourceVersion/sourceProduct by default — use --stamp or run ' +
401
+ 'rebase for source metadata stamping.')
402
402
  .option('-a, --all', 'Re-export all patches')
403
403
  .option('-s, --scan', 'Scan directories for new/removed files and update filesAffected')
404
404
  .option('--scan-file <path>', 'With --scan, add this explicit engine-relative file to one target patch without collecting adjacent files. Repeatable.', (value, prev) => [...prev, value], [])
@@ -411,7 +411,7 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
411
411
  .option('--allow-shrink', 'Allow --files to remove paths currently owned by the patch. Required before --yes can bypass the shrink confirmation.')
412
412
  .option('-y, --yes', 'Skip confirmation prompts (required for non-TTY destructive writes)')
413
413
  .option('--force-unsafe', 'Bypass cross-patch lint refusal when --files shrinks a patch')
414
- .option('--stamp', "After every selected patch refreshes cleanly, stamp each re-exported patch's sourceEsrVersion in patches.json to firefox.version from fireforge.json. No effect on a partial run.")
414
+ .option('--stamp', "After every selected patch refreshes cleanly, stamp each re-exported patch's sourceVersion/sourceProduct in patches.json to firefox.version/firefox.product from fireforge.json. No effect on a partial run.")
415
415
  .addOption(new Option('--tier <tier>', 'Force a tier override on the selected patch (only "branding" recognised). Mutually exclusive with --all.').choices(['branding']))
416
416
  .option('--lint-ignore <check-id>', 'Append a lint check ID to the patch\'s PatchMetadata.lintIgnore (union, de-duped, repeatable). Mutually exclusive with --all. Use "fireforge patch lint-ignore" for --remove / --clear.', (value, prev) => [...prev, value], [])
417
417
  .action(withErrorHandling(async (patches, options) => {
@@ -73,6 +73,8 @@ export async function handleContinue(projectRoot, maxFuzz) {
73
73
  // v0.14.0 resolve.ts fix.
74
74
  await updatePatchAndMetadata(paths.patches, currentPatch.filename, diffContent, {
75
75
  sourceEsrVersion: session.toVersion,
76
+ sourceVersion: session.toVersion,
77
+ ...(session.toProduct !== undefined ? { sourceProduct: session.toProduct } : {}),
76
78
  });
77
79
  }
78
80
  finally {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `fireforge rebase` — semi-automated ESR version upgrade.
2
+ * `fireforge rebase` — semi-automated Firefox source version upgrade.
3
3
  *
4
4
  * Orchestrates the full patch-rebase workflow:
5
5
  * 1. Reset engine to baseline
@@ -13,7 +13,7 @@ import { Command } from 'commander';
13
13
  import type { CommandContext } from '../../types/cli.js';
14
14
  import type { RebaseOptions } from '../../types/commands/index.js';
15
15
  /**
16
- * Runs the rebase command to orchestrate an ESR version upgrade.
16
+ * Runs the rebase command to orchestrate a Firefox source version upgrade.
17
17
  * @param projectRoot - Root directory of the project
18
18
  * @param options - Rebase options
19
19
  */
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * `fireforge rebase` — semi-automated ESR version upgrade.
3
+ * `fireforge rebase` — semi-automated Firefox source version upgrade.
4
4
  *
5
5
  * Orchestrates the full patch-rebase workflow:
6
6
  * 1. Reset engine to baseline
@@ -15,6 +15,7 @@ import { getFurnacePaths, updateFurnaceState } from '../../core/furnace-config.j
15
15
  import { getHead, isGitRepository, isMissingHeadError, resetChanges } from '../../core/git.js';
16
16
  import { discoverPatches } from '../../core/patch-files.js';
17
17
  import { loadPatchesManifest } from '../../core/patch-manifest.js';
18
+ import { getPatchSourceProduct, getPatchSourceVersion } from '../../core/patch-source-metadata.js';
18
19
  import { hasActiveRebaseSession, saveRebaseSession } from '../../core/rebase-session.js';
19
20
  import { GeneralError } from '../../errors/base.js';
20
21
  import { RebaseSessionExistsError } from '../../errors/rebase.js';
@@ -65,9 +66,11 @@ async function handleFreshStart(projectRoot, options) {
65
66
  throw new GeneralError('No patches found in manifest. Nothing to rebase.');
66
67
  }
67
68
  // Determine the "from" version from the patches
68
- const patchVersions = new Set(manifest.patches.map((p) => p.sourceEsrVersion));
69
+ const patchVersions = new Set(manifest.patches.map((p) => getPatchSourceVersion(p)));
70
+ const patchProducts = new Set(manifest.patches.map((p) => getPatchSourceProduct(p)).filter(Boolean));
69
71
  const sortedVersions = [...patchVersions].sort();
70
72
  const fromVersion = sortedVersions[0] ?? currentVersion;
73
+ const fromProduct = [...patchProducts].sort()[0] ?? config.firefox.product;
71
74
  if (patchVersions.size === 1 && fromVersion === currentVersion) {
72
75
  info('All patches already match the current Firefox version. Nothing to rebase.');
73
76
  outro('Rebase not needed');
@@ -110,6 +113,8 @@ async function handleFreshStart(projectRoot, options) {
110
113
  const allPatches = await discoverPatches(paths.patches);
111
114
  const session = {
112
115
  startedAt: new Date().toISOString(),
116
+ fromProduct,
117
+ toProduct: config.firefox.product,
113
118
  fromVersion,
114
119
  toVersion: currentVersion,
115
120
  preRebaseCommit,
@@ -125,7 +130,7 @@ async function handleFreshStart(projectRoot, options) {
125
130
  }
126
131
  // ── Public API ──
127
132
  /**
128
- * Runs the rebase command to orchestrate an ESR version upgrade.
133
+ * Runs the rebase command to orchestrate a Firefox source version upgrade.
129
134
  * @param projectRoot - Root directory of the project
130
135
  * @param options - Rebase options
131
136
  */
@@ -142,7 +147,7 @@ export async function rebaseCommand(projectRoot, options = {}) {
142
147
  export function registerRebase(program, { getProjectRoot, withErrorHandling }) {
143
148
  program
144
149
  .command('rebase')
145
- .description('Semi-automated ESR version upgrade — apply patches with fuzz and re-export')
150
+ .description('Semi-automated Firefox source version upgrade — apply patches with fuzz and re-export')
146
151
  .option('--continue', 'Resume after manually resolving a failed patch')
147
152
  .option('--abort', 'Cancel the rebase and restore engine to pre-rebase state')
148
153
  .option('--dry-run', 'Show what would happen without modifying anything')
@@ -143,16 +143,16 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
143
143
  .filter((p) => p.status === 'applied-clean' || p.status === 'applied-fuzz' || p.status === 'resolved')
144
144
  .map((p) => p.filename);
145
145
  if (appliedFilenames.length > 0) {
146
- await stampPatchVersions(paths.patches, appliedFilenames, session.toVersion);
146
+ await stampPatchVersions(paths.patches, appliedFilenames, session.toVersion, session.toProduct);
147
147
  }
148
148
  // Stamp every Furnace override's `baseVersion` to match the rebased
149
- // Firefox version. Before this stamp, a successful ESR bump left
149
+ // Firefox source version. Before this stamp, a successful source bump left
150
150
  // overrides in a doctor-failing drift state (each override still
151
- // claimed the pre-rebase ESR as its baseline) and every subsequent
151
+ // claimed the pre-rebase source as its baseline) and every subsequent
152
152
  // `fireforge doctor` failed `Furnace component validation`. The
153
153
  // stamp is unconditional per the helper's contract: rebase already
154
154
  // succeeded on the patch side, so the operator is committing to the
155
- // new ESR baseline; per-component health checking stays with
155
+ // new source baseline; per-component health checking stays with
156
156
  // `fireforge furnace validate` / `doctor --repair-furnace`.
157
157
  try {
158
158
  const overridesStamped = await stampFurnaceOverrideBaseVersions(projectRoot, session.toVersion);
@@ -176,7 +176,7 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
176
176
  return next;
177
177
  });
178
178
  info('');
179
- success(`All patches re-exported with sourceEsrVersion=${session.toVersion}`);
179
+ success(`All patches re-exported with sourceVersion=${session.toVersion}`);
180
180
  outro('Rebase complete!');
181
181
  }
182
182
  async function reExportAppliedPatches(session, paths) {
@@ -27,7 +27,11 @@ export function statusLabel(status, fuzzFactor) {
27
27
  */
28
28
  export function printSummary(session) {
29
29
  info('');
30
- info(`ESR Rebase Summary: ${session.fromVersion} → ${session.toVersion}`);
30
+ const from = session.fromProduct
31
+ ? `${session.fromProduct} ${session.fromVersion}`
32
+ : session.fromVersion;
33
+ const to = session.toProduct ? `${session.toProduct} ${session.toVersion}` : session.toVersion;
34
+ info(`Source Rebase Summary: ${from} → ${to}`);
31
35
  info('='.repeat(55));
32
36
  for (const patch of session.patches) {
33
37
  const label = statusLabel(patch.status, patch.fuzzFactor);
@@ -37,7 +41,8 @@ export function printSummary(session) {
37
41
  const fuzz = session.patches.filter((p) => p.status === 'applied-fuzz').length;
38
42
  const resolved = session.patches.filter((p) => p.status === 'resolved').length;
39
43
  const failed = session.patches.filter((p) => p.status === 'failed').length;
44
+ const total = session.patches.length;
40
45
  info('');
41
- info(`Results: ${clean} clean, ${fuzz} fuzz-applied, ${resolved} manually resolved, ${failed} failed`);
46
+ info(`Results: ${total} total: ${clean} clean, ${fuzz} fuzz-applied, ${resolved} manually resolved, ${failed} failed`);
42
47
  }
43
48
  //# sourceMappingURL=summary.js.map
@@ -8,6 +8,7 @@ import { stageFiles, unstageFiles } from '../core/git-file-ops.js';
8
8
  import { extractAffectedFiles } from '../core/patch-apply.js';
9
9
  import { updatePatchAndMetadata } from '../core/patch-export.js';
10
10
  import { loadPatchesManifest } from '../core/patch-manifest.js';
11
+ import { buildPatchSourceMetadata } from '../core/patch-source-metadata.js';
11
12
  import { GeneralError, ResolutionError } from '../errors/base.js';
12
13
  import { toError } from '../utils/errors.js';
13
14
  import { pathExists } from '../utils/fs.js';
@@ -132,7 +133,7 @@ export async function resolveCommand(projectRoot, options = {}) {
132
133
  const config = await loadConfig(projectRoot);
133
134
  await updatePatchAndMetadata(paths.patches, patchFilename, diffContent, {
134
135
  filesAffected: diffFilesAffected,
135
- sourceEsrVersion: config.firefox.version,
136
+ ...buildPatchSourceMetadata(config.firefox),
136
137
  });
137
138
  // Cleanup: Clear pendingResolution from state.json transactionally so
138
139
  // we don't clobber concurrent updates to unrelated keys (e.g. another
@@ -0,0 +1,13 @@
1
+ import type { ClassifiedFile } from '../core/status-classify.js';
2
+ export interface ClassifiedBuckets {
3
+ conflict: ClassifiedFile[];
4
+ unmanaged: ClassifiedFile[];
5
+ patchBacked: ClassifiedFile[];
6
+ patchOwnedDrift: ClassifiedFile[];
7
+ branding: ClassifiedFile[];
8
+ furnace: ClassifiedFile[];
9
+ }
10
+ /** Renders the unmanaged-only status view and registration hints. */
11
+ export declare function renderUnmanagedOnly(unmanagedFiles: ClassifiedFile[], totalModified: number, projectRoot: string, binaryName: string): Promise<void>;
12
+ /** Renders the default classified status buckets. */
13
+ export declare function renderDefaultStatus(totalModified: number, buckets: ClassifiedBuckets, projectRoot: string, binaryName: string): Promise<void>;
@@ -0,0 +1,186 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { isFileRegistered, matchesRegistrablePattern } from '../core/manifest-rules.js';
3
+ import { GeneralError } from '../errors/base.js';
4
+ import { info, outro, warn } from '../utils/logger.js';
5
+ const STATUS_DESCRIPTIONS = {
6
+ M: 'modified',
7
+ A: 'added',
8
+ D: 'deleted',
9
+ R: 'renamed',
10
+ C: 'copied',
11
+ U: 'unmerged',
12
+ '?': 'untracked',
13
+ '!': 'ignored',
14
+ };
15
+ function getStatusDescription(code) {
16
+ return STATUS_DESCRIPTIONS[code] ?? 'changed';
17
+ }
18
+ function getPrimaryStatusCode(status) {
19
+ if (status.includes('?'))
20
+ return '?';
21
+ if (status.includes('!'))
22
+ return '!';
23
+ for (const code of status) {
24
+ if (code !== ' ')
25
+ return code;
26
+ }
27
+ return status;
28
+ }
29
+ function isNewFileStatus(status) {
30
+ const code = getPrimaryStatusCode(status);
31
+ return code === '?' || code === 'A';
32
+ }
33
+ function groupFilesByStatus(files) {
34
+ const grouped = new Map();
35
+ for (const { status, file } of files) {
36
+ const code = getPrimaryStatusCode(status);
37
+ const existing = grouped.get(code) ?? [];
38
+ existing.push(file);
39
+ grouped.set(code, existing);
40
+ }
41
+ return grouped;
42
+ }
43
+ function printStatusGroups(files) {
44
+ const grouped = groupFilesByStatus(files);
45
+ for (const [status, fileList] of grouped) {
46
+ warn(`${getStatusDescription(status)}:`);
47
+ for (const file of fileList)
48
+ info(` ${file}`);
49
+ }
50
+ }
51
+ async function printUnregisteredWarnings(files, projectRoot, binaryName) {
52
+ const newFiles = files.filter((f) => isNewFileStatus(f.status));
53
+ if (newFiles.length === 0)
54
+ return;
55
+ const registrableFiles = newFiles.filter((f) => matchesRegistrablePattern(f.file, binaryName));
56
+ const registrationChecks = await Promise.all(registrableFiles.map(async (f) => {
57
+ try {
58
+ return {
59
+ file: f.file,
60
+ registered: await isFileRegistered(projectRoot, f.file),
61
+ manifestMissing: false,
62
+ manifestMissingMessage: undefined,
63
+ };
64
+ }
65
+ catch (err) {
66
+ if (err instanceof GeneralError && /^Manifest not found:/i.test(err.message)) {
67
+ return {
68
+ file: f.file,
69
+ registered: false,
70
+ manifestMissing: true,
71
+ manifestMissingMessage: err.message,
72
+ };
73
+ }
74
+ throw err;
75
+ }
76
+ }));
77
+ const unregistered = registrationChecks.filter((f) => !f.registered && !f.manifestMissing);
78
+ const manifestMissing = registrationChecks.filter((f) => f.manifestMissing);
79
+ if (unregistered.length > 0) {
80
+ info('');
81
+ warn('Potentially unregistered files:');
82
+ for (const f of unregistered)
83
+ info(` ${f.file} — run 'fireforge register ${f.file}'`);
84
+ }
85
+ if (manifestMissing.length > 0) {
86
+ info('');
87
+ warn('Files whose registration manifest does not exist yet:');
88
+ for (const f of manifestMissing) {
89
+ info(` ${f.file} — ${f.manifestMissingMessage}`);
90
+ info(` Create the parent manifest, then run 'fireforge register ${f.file}'.`);
91
+ }
92
+ }
93
+ }
94
+ /** Renders the unmanaged-only status view and registration hints. */
95
+ export async function renderUnmanagedOnly(unmanagedFiles, totalModified, projectRoot, binaryName) {
96
+ info(`${unmanagedFiles.length} unmanaged file${unmanagedFiles.length === 1 ? '' : 's'} (${totalModified} total modified):\n`);
97
+ if (unmanagedFiles.length > 0) {
98
+ printStatusGroups(unmanagedFiles);
99
+ await printUnregisteredWarnings(unmanagedFiles, projectRoot, binaryName);
100
+ }
101
+ else {
102
+ info('No unmanaged changes');
103
+ }
104
+ outro(unmanagedFiles.length === 0
105
+ ? 'No unmanaged changes'
106
+ : `${unmanagedFiles.length} unmanaged change${unmanagedFiles.length === 1 ? '' : 's'}`);
107
+ }
108
+ /** Renders the default classified status buckets. */
109
+ export async function renderDefaultStatus(totalModified, buckets, projectRoot, binaryName) {
110
+ const { conflict, unmanaged, patchBacked, patchOwnedDrift, branding, furnace } = buckets;
111
+ info(`${totalModified} modified file${totalModified === 1 ? '' : 's'}:\n`);
112
+ if (conflict.length > 0) {
113
+ warn('Cross-patch ownership conflicts (same file claimed by multiple patches):');
114
+ printStatusGroups(conflict);
115
+ for (const entry of conflict) {
116
+ if (entry.claimedBy && entry.claimedBy.length > 0) {
117
+ info(` ${entry.file} — claimed by ${entry.claimedBy.join(', ')}`);
118
+ }
119
+ }
120
+ info('Run "fireforge status --ownership" for the full conflict table, then repartition with "fireforge re-export --files <paths> <patch>".');
121
+ }
122
+ if (unmanaged.length > 0) {
123
+ if (conflict.length > 0)
124
+ info('');
125
+ warn('Unmanaged changes:');
126
+ printStatusGroups(unmanaged);
127
+ await printUnregisteredWarnings(unmanaged, projectRoot, binaryName);
128
+ }
129
+ if (patchBacked.length > 0) {
130
+ if (conflict.length > 0 || unmanaged.length > 0)
131
+ info('');
132
+ warn('Patch-backed materialized changes:');
133
+ printStatusGroups(patchBacked);
134
+ }
135
+ if (patchOwnedDrift.length > 0) {
136
+ if (conflict.length > 0 || unmanaged.length > 0 || patchBacked.length > 0)
137
+ info('');
138
+ warn('Patch-owned drift:');
139
+ printStatusGroups(patchOwnedDrift);
140
+ info('These files are claimed by exactly one patch, but engine/ no longer matches that patch output. Re-export the owning patch after reviewing the manual resolution.');
141
+ }
142
+ if (branding.length > 0) {
143
+ if (conflict.length > 0 ||
144
+ unmanaged.length > 0 ||
145
+ patchBacked.length > 0 ||
146
+ patchOwnedDrift.length > 0) {
147
+ info('');
148
+ }
149
+ warn('Tool-managed branding changes:');
150
+ printStatusGroups(branding);
151
+ }
152
+ if (furnace.length > 0) {
153
+ if (conflict.length > 0 ||
154
+ unmanaged.length > 0 ||
155
+ patchBacked.length > 0 ||
156
+ patchOwnedDrift.length > 0 ||
157
+ branding.length > 0) {
158
+ info('');
159
+ }
160
+ warn('Furnace-managed component changes:');
161
+ printStatusGroups(furnace);
162
+ }
163
+ if (conflict.length === 0 &&
164
+ unmanaged.length === 0 &&
165
+ patchBacked.length === 0 &&
166
+ patchOwnedDrift.length === 0 &&
167
+ branding.length === 0 &&
168
+ furnace.length === 0) {
169
+ info('No changes');
170
+ }
171
+ const parts = [];
172
+ if (conflict.length > 0)
173
+ parts.push(`${conflict.length} conflict`);
174
+ if (unmanaged.length > 0)
175
+ parts.push(`${unmanaged.length} unmanaged`);
176
+ if (patchBacked.length > 0)
177
+ parts.push(`${patchBacked.length} patch-backed`);
178
+ if (patchOwnedDrift.length > 0)
179
+ parts.push(`${patchOwnedDrift.length} patch-owned drift`);
180
+ if (branding.length > 0)
181
+ parts.push(`${branding.length} branding`);
182
+ if (furnace.length > 0)
183
+ parts.push(`${furnace.length} furnace`);
184
+ outro(parts.join(', '));
185
+ }
186
+ //# sourceMappingURL=status-output.js.map