@hominis/fireforge 0.39.0 → 0.41.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.
- package/CHANGELOG.md +100 -0
- package/README.md +35 -5
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +27 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +27 -9
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +30 -10
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +73 -51
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +26 -22
- package/dist/src/commands/manifest.js +3 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +152 -51
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +112 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +105 -0
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +11 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +123 -289
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +29 -23
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +48 -0
- package/dist/src/commands/status-check.js +106 -0
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +190 -170
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +59 -7
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/token.js +4 -1
- package/dist/src/commands/tree.d.ts +19 -0
- package/dist/src/commands/tree.js +320 -0
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +45 -30
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +49 -14
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.d.ts +2 -2
- package/dist/src/core/patch-export.js +4 -6
- package/dist/src/core/patch-lint-checkjs.d.ts +42 -2
- package/dist/src/core/patch-lint-checkjs.js +132 -8
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +2 -1
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +31 -0
- package/dist/src/core/patch-lint.js +114 -8
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +24 -16
- package/dist/src/core/smoke-patterns.js +26 -14
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +22 -1
- package/dist/src/core/status-classify.js +112 -70
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +48 -196
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +179 -0
- package/dist/src/core/tree-store.d.ts +129 -0
- package/dist/src/core/tree-store.js +452 -0
- package/dist/src/core/typecheck-shim.d.ts +14 -0
- package/dist/src/core/typecheck-shim.js +41 -2
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +119 -3
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/config.d.ts +19 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +33 -2
- package/dist/src/utils/validation.js +57 -2
- package/package.json +13 -13
|
@@ -5,10 +5,10 @@ import { FurnaceError } from '../errors/furnace.js';
|
|
|
5
5
|
import { toError } from '../utils/errors.js';
|
|
6
6
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
7
7
|
import { info, warn } from '../utils/logger.js';
|
|
8
|
-
import { escapeRegex } from '../utils/regex.js';
|
|
9
8
|
import { validateTokenName } from '../utils/validation.js';
|
|
10
9
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
11
10
|
import { loadFurnaceConfig } from './furnace-config.js';
|
|
11
|
+
import { assertTokenCategoryExists, categoryHeaderExists, declareCategoryBanner, findCategorySection, findTokenDeclarationInRoot, } from './token-category.js';
|
|
12
12
|
import { findDarkMediaCloseIndex, findDarkRootInsertionIndex } from './token-dark-mode.js';
|
|
13
13
|
import { addTokenToDocs } from './token-docs.js';
|
|
14
14
|
import { insertVariantDeclaration, validateVariantSelector, variantBlockExists, variantBlockHasToken, } from './token-variant.js';
|
|
@@ -83,6 +83,10 @@ function normalizeVariantOption(options) {
|
|
|
83
83
|
throw new InvalidArgumentError('Cannot combine --variant with --mode override; author the variant declaration with ' +
|
|
84
84
|
'--mode auto/static instead.', 'variant');
|
|
85
85
|
}
|
|
86
|
+
if (options.createCategory === true) {
|
|
87
|
+
throw new InvalidArgumentError('--create-category cannot be combined with --variant; variant declarations are routed ' +
|
|
88
|
+
'into a :root<selector> block, not a category section.', 'variant');
|
|
89
|
+
}
|
|
86
90
|
const result = validateVariantSelector(options.variant);
|
|
87
91
|
if (!result.ok) {
|
|
88
92
|
throw new InvalidArgumentError(`--variant ${result.reason}.`, 'variant');
|
|
@@ -113,95 +117,26 @@ async function addVariantTokenToCSS(engineDir, options, tokensCssPath, variant)
|
|
|
113
117
|
return { added: true };
|
|
114
118
|
}
|
|
115
119
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
120
|
+
* Evaluates base-`:root` idempotency for one add (FORGE G3): a token
|
|
121
|
+
* already declared in the TARGET category skips; a token declared
|
|
122
|
+
* anywhere else in the base block refuses loud — the pre-0.40.0
|
|
123
|
+
* whole-file substring check silently skipped (and discarded
|
|
124
|
+
* `--create-category`) in that case, exit 0.
|
|
119
125
|
*/
|
|
120
|
-
function
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
|
|
129
|
-
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
130
|
-
const blockLine = lines[j] ?? '';
|
|
131
|
-
if (new RegExp(escapedCategory).test(blockLine)) {
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
if (/\*\//.test(blockLine))
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
126
|
+
function evaluateBaseTokenIdempotency(lines, options, tokensCssPath) {
|
|
127
|
+
const existing = findTokenDeclarationInRoot(lines, options.tokenName);
|
|
128
|
+
if (!existing)
|
|
129
|
+
return { skipped: false };
|
|
130
|
+
if (existing.category === options.category) {
|
|
131
|
+
return { skipped: true, existing };
|
|
138
132
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const content = await readText(filePath);
|
|
147
|
-
const lines = content.split('\n');
|
|
148
|
-
if (categoryHeaderExists(lines, category))
|
|
149
|
-
return;
|
|
150
|
-
// The write path declares the banner in the same edit as the token
|
|
151
|
-
// insertion, so a missing category is fine when creation was requested.
|
|
152
|
-
if (createCategory)
|
|
153
|
-
return;
|
|
154
|
-
const discoveredCategories = discoverCategoryHeaders(lines);
|
|
155
|
-
const available = discoveredCategories.length > 0
|
|
156
|
-
? `Available categories in the file: ${discoveredCategories.map((name) => `"${name}"`).join(', ')}.`
|
|
157
|
-
: 'The file currently has no category headers.';
|
|
158
|
-
throw new GeneralError(`Category "${category}" not found in ${tokensCssPath}.\n\n` +
|
|
159
|
-
`${available}\n\n` +
|
|
160
|
-
'Categories are declared by comment headers. Single-line shape: /* = My Category = */. ' +
|
|
161
|
-
'Multi-line shape: /* =============\\n * My Category\\n * ============= */.\n\n' +
|
|
162
|
-
'Re-run with --create-category to declare the banner and insert the token in one step.');
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Scans a tokens CSS file for category header comments and returns the
|
|
166
|
-
* category names in document order. Used to enrich the "category not
|
|
167
|
-
* found" error body with concrete alternatives the operator can copy.
|
|
168
|
-
*
|
|
169
|
-
* Mirrors the shapes `findCategorySection` already recognises:
|
|
170
|
-
* - Single-line: `/* = Foo = *\/`
|
|
171
|
-
* - Multi-line: `/* =====` opening, `Foo` on any of the next ~5 lines,
|
|
172
|
-
* closing `*\/`.
|
|
173
|
-
*
|
|
174
|
-
* This helper exists as a pure inspector; it never throws on malformed
|
|
175
|
-
* headers and silently skips shapes it cannot parse.
|
|
176
|
-
*/
|
|
177
|
-
function discoverCategoryHeaders(lines) {
|
|
178
|
-
const categories = new Set();
|
|
179
|
-
const singleLinePattern = /\/\*\s*=+\s*(.+?)\s*=+\s*\*\//;
|
|
180
|
-
for (let i = 0; i < lines.length; i++) {
|
|
181
|
-
const line = lines[i] ?? '';
|
|
182
|
-
const singleMatch = singleLinePattern.exec(line);
|
|
183
|
-
if (singleMatch?.[1]) {
|
|
184
|
-
const extracted = singleMatch[1].trim();
|
|
185
|
-
if (extracted.length > 0)
|
|
186
|
-
categories.add(extracted);
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
|
|
190
|
-
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
191
|
-
const blockLine = lines[j] ?? '';
|
|
192
|
-
if (/\*\//.test(blockLine))
|
|
193
|
-
break;
|
|
194
|
-
const trimmed = blockLine.replace(/^\s*\*\s*/, '').trim();
|
|
195
|
-
if (trimmed.length === 0)
|
|
196
|
-
continue;
|
|
197
|
-
if (/^=+$/.test(trimmed))
|
|
198
|
-
continue;
|
|
199
|
-
categories.add(trimmed);
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return [...categories];
|
|
133
|
+
const sectionSuffix = existing.category === undefined ? '' : `, section "${existing.category}"`;
|
|
134
|
+
const remedy = existing.category === undefined
|
|
135
|
+
? 'remove the stray declaration first'
|
|
136
|
+
: `pass --category "${existing.category}" to target the existing declaration, or remove it first`;
|
|
137
|
+
throw new GeneralError(`Token "${options.tokenName}" is already declared outside category "${options.category}" ` +
|
|
138
|
+
`(${tokensCssPath}:${String(existing.line)}${sectionSuffix}). ` +
|
|
139
|
+
`A token belongs to exactly one category — ${remedy}.`);
|
|
205
140
|
}
|
|
206
141
|
/**
|
|
207
142
|
* Validates token-add inputs without mutating files.
|
|
@@ -214,10 +149,11 @@ export async function validateTokenAdd(root, options) {
|
|
|
214
149
|
validateTokenNameSyntax(options.tokenName);
|
|
215
150
|
await validateTokenPrefix(root, options);
|
|
216
151
|
validateDarkValue(options);
|
|
217
|
-
// Variant mode targets a `:root<attr>` block, not a category section
|
|
218
|
-
//
|
|
152
|
+
// Variant mode targets a `:root<attr>` block, not a category section —
|
|
153
|
+
// but the required --category must still name a REAL category rather
|
|
154
|
+
// than being silently discarded (FORGE G3 bypass 2).
|
|
219
155
|
if (normalizeVariantOption(options) !== undefined) {
|
|
220
|
-
await
|
|
156
|
+
await assertTokenCategoryExists(engineDir, tokensCssPath, options.category, false);
|
|
221
157
|
return;
|
|
222
158
|
}
|
|
223
159
|
await assertTokenCategoryExists(engineDir, tokensCssPath, options.category, options.createCategory === true);
|
|
@@ -249,14 +185,14 @@ export async function addToken(root, options) {
|
|
|
249
185
|
skipped,
|
|
250
186
|
};
|
|
251
187
|
}
|
|
252
|
-
const
|
|
253
|
-
const skipped = stripped.includes(options.tokenName + ':');
|
|
188
|
+
const { skipped, existing } = evaluateBaseTokenIdempotency(content.split('\n'), options, tokensCssPath);
|
|
254
189
|
return {
|
|
255
190
|
cssAdded: !skipped,
|
|
256
191
|
docsAdded: !skipped,
|
|
257
192
|
unmappedAdded: !skipped && !options.value.startsWith('var('),
|
|
258
193
|
countUpdated: !skipped,
|
|
259
194
|
skipped,
|
|
195
|
+
...(existing !== undefined ? { skippedExisting: existing } : {}),
|
|
260
196
|
};
|
|
261
197
|
}
|
|
262
198
|
// Variant overrides are CSS-only: route the declaration into the
|
|
@@ -264,6 +200,9 @@ export async function addToken(root, options) {
|
|
|
264
200
|
// docs row). Done before the base-CSS path so an override of an existing
|
|
265
201
|
// base token is not short-circuited by the global idempotency check.
|
|
266
202
|
if (normalizedVariant !== undefined) {
|
|
203
|
+
// The required --category must name a real category even though the
|
|
204
|
+
// declaration routes into the variant block (FORGE G3 bypass 2).
|
|
205
|
+
await assertTokenCategoryExists(engineDir, tokensCssPath, options.category, false);
|
|
267
206
|
const { added } = await addVariantTokenToCSS(engineDir, options, tokensCssPath, normalizedVariant);
|
|
268
207
|
return {
|
|
269
208
|
cssAdded: added,
|
|
@@ -274,7 +213,7 @@ export async function addToken(root, options) {
|
|
|
274
213
|
};
|
|
275
214
|
}
|
|
276
215
|
// --- CSS file ---
|
|
277
|
-
const { added: cssAdded, categoryCreated } = await addTokenToCSS(engineDir, options, tokensCssPath);
|
|
216
|
+
const { added: cssAdded, categoryCreated, existing, } = await addTokenToCSS(engineDir, options, tokensCssPath);
|
|
278
217
|
if (!cssAdded) {
|
|
279
218
|
return {
|
|
280
219
|
cssAdded: false,
|
|
@@ -282,6 +221,7 @@ export async function addToken(root, options) {
|
|
|
282
221
|
unmappedAdded: false,
|
|
283
222
|
countUpdated: false,
|
|
284
223
|
skipped: true,
|
|
224
|
+
...(existing !== undefined ? { skippedExisting: existing } : {}),
|
|
285
225
|
};
|
|
286
226
|
}
|
|
287
227
|
// --- Documentation ---
|
|
@@ -295,100 +235,6 @@ export async function addToken(root, options) {
|
|
|
295
235
|
categoryCreated,
|
|
296
236
|
};
|
|
297
237
|
}
|
|
298
|
-
/**
|
|
299
|
-
* Splices a new single-line category banner ("= Name =" comment shape, the
|
|
300
|
-
* same format `discoverCategoryHeaders` recognises) just before the closing
|
|
301
|
-
* brace of the `:root` block, making the new category the last section.
|
|
302
|
-
* Mutates `lines` in place.
|
|
303
|
-
*/
|
|
304
|
-
function declareCategoryBanner(lines, category, tokensCssPath) {
|
|
305
|
-
let rootOpen = -1;
|
|
306
|
-
for (let i = 0; i < lines.length; i++) {
|
|
307
|
-
if (/:root\s*\{/.test(lines[i] ?? '')) {
|
|
308
|
-
rootOpen = i;
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (rootOpen === -1) {
|
|
313
|
-
throw new GeneralError(`Cannot create category "${category}": no :root block found in ${tokensCssPath}. ` +
|
|
314
|
-
'Run "fireforge furnace init --force" to re-scaffold the tokens CSS file.');
|
|
315
|
-
}
|
|
316
|
-
let rootClose = -1;
|
|
317
|
-
for (let i = rootOpen + 1; i < lines.length; i++) {
|
|
318
|
-
if (/^\s*\}/.test(lines[i] ?? '')) {
|
|
319
|
-
rootClose = i;
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (rootClose === -1) {
|
|
324
|
-
throw new GeneralError(`Cannot create category "${category}": the :root block in ${tokensCssPath} never closes.`);
|
|
325
|
-
}
|
|
326
|
-
lines.splice(rootClose, 0, '', ` /* = ${category} = */`);
|
|
327
|
-
}
|
|
328
|
-
function findCategorySection(lines, category, tokensCssPath) {
|
|
329
|
-
const escapedCategory = escapeRegex(category);
|
|
330
|
-
const singleLinePattern = new RegExp(`\\/\\*\\s*=.*${escapedCategory}.*=\\s*\\*\\/`);
|
|
331
|
-
let categoryLine = -1;
|
|
332
|
-
for (let i = 0; i < lines.length; i++) {
|
|
333
|
-
const line = lines[i] ?? '';
|
|
334
|
-
// Check single-line format: /* = Category = */
|
|
335
|
-
if (singleLinePattern.test(line)) {
|
|
336
|
-
categoryLine = i;
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
// Check multi-line format: line opens a block comment with === but does NOT close it
|
|
340
|
-
// e.g., "/* ================================================================"
|
|
341
|
-
// (NOT "/* ================================================= */" which closes on the same line)
|
|
342
|
-
if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
|
|
343
|
-
// Look ahead within the comment block (up to 5 lines) for the category text
|
|
344
|
-
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
345
|
-
const blockLine = lines[j] ?? '';
|
|
346
|
-
if (new RegExp(escapedCategory).test(blockLine)) {
|
|
347
|
-
categoryLine = i;
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
// Stop if we've exited the comment block
|
|
351
|
-
if (/\*\//.test(blockLine))
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
if (categoryLine !== -1)
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
if (categoryLine === -1) {
|
|
359
|
-
const discoveredCategories = discoverCategoryHeaders(lines);
|
|
360
|
-
const available = discoveredCategories.length > 0
|
|
361
|
-
? `Available categories in the file: ${discoveredCategories.map((name) => `"${name}"`).join(', ')}.`
|
|
362
|
-
: 'The file currently has no category headers.';
|
|
363
|
-
throw new GeneralError(`Category "${category}" not found in ${tokensCssPath}.\n\n` +
|
|
364
|
-
`${available}\n\n` +
|
|
365
|
-
'Add a header by hand inside the :root block (format: "/* = My Category = */") or re-run "fireforge furnace init --force" to re-seed the default categories.');
|
|
366
|
-
}
|
|
367
|
-
// Find the end of this category section (next section header or closing })
|
|
368
|
-
// Handles both single-line (/* =...= */) and multi-line (/* ===...) section delimiters
|
|
369
|
-
// Skip past the current header block first
|
|
370
|
-
let scanStart = categoryLine + 1;
|
|
371
|
-
for (let i = categoryLine + 1; i < lines.length; i++) {
|
|
372
|
-
const line = lines[i] ?? '';
|
|
373
|
-
// Skip lines that are part of the current header comment block
|
|
374
|
-
if (/^\s*\/\*\s*=/.test(line) || /^\s*\*\s*=/.test(line) || /^\s*\*\//.test(line)) {
|
|
375
|
-
scanStart = i + 1;
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
break;
|
|
379
|
-
}
|
|
380
|
-
let sectionEnd = lines.length;
|
|
381
|
-
for (let i = scanStart; i < lines.length; i++) {
|
|
382
|
-
const line = lines[i] ?? '';
|
|
383
|
-
if (/\/\*\s*=.*=\s*\*\//.test(line) ||
|
|
384
|
-
(/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) ||
|
|
385
|
-
/^\s*\}/.test(line)) {
|
|
386
|
-
sectionEnd = i;
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return { categoryLine, sectionEnd };
|
|
391
|
-
}
|
|
392
238
|
/**
|
|
393
239
|
* Explicit theme-attribute selectors the override path keeps in sync with
|
|
394
240
|
* the `@media (prefers-color-scheme: dark)` block. Consumer scaffolds that
|
|
@@ -450,13 +296,20 @@ function insertDarkModeOverride(lines, options) {
|
|
|
450
296
|
async function addTokenToCSS(engineDir, options, tokensCssPath) {
|
|
451
297
|
const filePath = join(engineDir, tokensCssPath);
|
|
452
298
|
await assertTokenCategoryExists(engineDir, tokensCssPath, options.category, options.createCategory === true);
|
|
453
|
-
|
|
454
|
-
// Idempotency check — strip CSS block comments so we don't match inside them
|
|
455
|
-
const stripped = content.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
456
|
-
if (stripped.includes(options.tokenName + ':')) {
|
|
457
|
-
return { added: false, categoryCreated: false };
|
|
458
|
-
}
|
|
299
|
+
const content = await readText(filePath);
|
|
459
300
|
const lines = content.split('\n');
|
|
301
|
+
// Per-category idempotency (FORGE G3): a declaration in the TARGET
|
|
302
|
+
// category skips; a declaration elsewhere in the base :root block
|
|
303
|
+
// throws instead of silently skipping (and silently discarding
|
|
304
|
+
// --create-category).
|
|
305
|
+
const idempotency = evaluateBaseTokenIdempotency(lines, options, tokensCssPath);
|
|
306
|
+
if (idempotency.skipped) {
|
|
307
|
+
return {
|
|
308
|
+
added: false,
|
|
309
|
+
categoryCreated: false,
|
|
310
|
+
...(idempotency.existing !== undefined ? { existing: idempotency.existing } : {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
460
313
|
const annotation = getModeAnnotation(options.mode, options.value);
|
|
461
314
|
// Declare a missing category banner in the same in-memory edit as the
|
|
462
315
|
// token insertion — the file is written exactly once, so a failure
|
|
@@ -488,7 +341,6 @@ async function addTokenToCSS(engineDir, options, tokensCssPath) {
|
|
|
488
341
|
if (themeBlocksWritten.length > 0) {
|
|
489
342
|
info(`Override also written to ${themeBlocksWritten.join(' and ')}.`);
|
|
490
343
|
}
|
|
491
|
-
|
|
492
|
-
await writeText(filePath, content);
|
|
344
|
+
await writeText(filePath, lines.join('\n'));
|
|
493
345
|
return { added: true, categoryCreated };
|
|
494
346
|
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { execFile } from 'node:child_process';
|
|
31
31
|
import { homedir } from 'node:os';
|
|
32
32
|
import { join } from 'node:path';
|
|
33
|
+
import { toError } from '../utils/errors.js';
|
|
33
34
|
import { pathExists, readText } from '../utils/fs.js';
|
|
34
35
|
import { verbose } from '../utils/logger.js';
|
|
35
36
|
/**
|
|
@@ -133,28 +134,29 @@ export function formatMajorVersionHopNotice(previousVersion, newVersion) {
|
|
|
133
134
|
* yields undefined for that tool — never an error.
|
|
134
135
|
*/
|
|
135
136
|
export async function readDeclaredToolchainMinimums(engineDir) {
|
|
136
|
-
const
|
|
137
|
-
|
|
137
|
+
const entries = Object.entries(MINIMUM_DECLARATIONS);
|
|
138
|
+
const resolved = await Promise.all(entries.map(async ([tool, declaration]) => {
|
|
138
139
|
const filePath = join(engineDir, declaration.relPath);
|
|
139
140
|
if (!(await pathExists(filePath))) {
|
|
140
141
|
verbose(`Toolchain preflight: ${declaration.relPath} not found; skipping ${tool}.`);
|
|
141
|
-
|
|
142
|
+
return undefined;
|
|
142
143
|
}
|
|
143
144
|
try {
|
|
144
145
|
const content = await readText(filePath);
|
|
145
146
|
const match = declaration.pattern.exec(content);
|
|
146
147
|
if (match?.[1]) {
|
|
147
|
-
|
|
148
|
+
return [tool, match[1]];
|
|
148
149
|
}
|
|
149
150
|
else {
|
|
150
151
|
verbose(`Toolchain preflight: no ${tool} minimum declaration recognized in ${declaration.relPath}; skipping.`);
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
catch (error) {
|
|
154
|
-
verbose(`Toolchain preflight: could not read ${declaration.relPath} (${error
|
|
155
|
+
verbose(`Toolchain preflight: could not read ${declaration.relPath} (${toError(error).message}); skipping ${tool}.`);
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
return undefined;
|
|
158
|
+
}));
|
|
159
|
+
return Object.fromEntries(resolved.filter((entry) => entry !== undefined));
|
|
158
160
|
}
|
|
159
161
|
/** Runs `<binary> --version` and parses the leading version number. */
|
|
160
162
|
async function probeBinaryVersion(binary, versionPattern) {
|
|
@@ -185,17 +187,14 @@ async function probeHostToolCandidates(tool) {
|
|
|
185
187
|
const binaries = envOverride
|
|
186
188
|
? [envOverride]
|
|
187
189
|
: [...probe.stateDirRelPaths.map((rel) => join(mozbuildStateDir(), rel)), tool];
|
|
188
|
-
const candidates =
|
|
189
|
-
for (const binary of binaries) {
|
|
190
|
+
const candidates = await Promise.all(binaries.map(async (binary) => {
|
|
190
191
|
const version = await probeBinaryVersion(binary, probe.versionPattern);
|
|
191
|
-
if (version !== undefined)
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
return candidates;
|
|
192
|
+
if (version !== undefined)
|
|
193
|
+
return { binary, version };
|
|
194
|
+
verbose(`Toolchain preflight: "${binary} --version" failed or not found; skipping.`);
|
|
195
|
+
return undefined;
|
|
196
|
+
}));
|
|
197
|
+
return candidates.filter((candidate) => candidate !== undefined);
|
|
199
198
|
}
|
|
200
199
|
/**
|
|
201
200
|
* Compares the tree-declared toolchain minimums against the host binaries
|
|
@@ -210,31 +209,30 @@ async function probeHostToolCandidates(tool) {
|
|
|
210
209
|
*/
|
|
211
210
|
export async function runToolchainPreflight(engineDir) {
|
|
212
211
|
const minimums = await readDeclaredToolchainMinimums(engineDir);
|
|
213
|
-
const
|
|
214
|
-
for (const [tool, minimumVersion] of Object.entries(minimums)) {
|
|
212
|
+
const results = await Promise.all(Object.entries(minimums).map(async ([tool, minimumVersion]) => {
|
|
215
213
|
const minimum = parseVersionComponents(minimumVersion);
|
|
216
214
|
if (!minimum)
|
|
217
|
-
|
|
215
|
+
return undefined;
|
|
218
216
|
const candidates = await probeHostToolCandidates(tool);
|
|
219
217
|
const parsed = candidates.filter((c) => parseVersionComponents(c.version) !== undefined);
|
|
220
218
|
if (parsed.length === 0)
|
|
221
|
-
|
|
219
|
+
return undefined;
|
|
222
220
|
const satisfying = parsed.find((c) => {
|
|
223
221
|
const components = parseVersionComponents(c.version);
|
|
224
222
|
return components !== undefined && !isVersionLower(components, minimum);
|
|
225
223
|
});
|
|
226
224
|
if (satisfying) {
|
|
227
225
|
verbose(`Toolchain preflight: ${tool} ${satisfying.version} (${satisfying.binary}) satisfies minimum ${minimumVersion}.`);
|
|
228
|
-
|
|
226
|
+
return undefined;
|
|
229
227
|
}
|
|
230
|
-
|
|
228
|
+
return {
|
|
231
229
|
tool,
|
|
232
230
|
minimumVersion,
|
|
233
231
|
declaredIn: MINIMUM_DECLARATIONS[tool].relPath,
|
|
234
232
|
candidates: parsed,
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return
|
|
233
|
+
};
|
|
234
|
+
}));
|
|
235
|
+
return results.filter((mismatch) => mismatch !== undefined);
|
|
238
236
|
}
|
|
239
237
|
/**
|
|
240
238
|
* Formats the fail-fast message for definitive preflight mismatches,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Runs a clone command; throws on non-zero exit. */
|
|
2
|
+
export type CloneExecutor = (command: string, args: string[]) => Promise<void>;
|
|
3
|
+
/** How this host can materialise a tree. */
|
|
4
|
+
export type CowCapability = 'clonefile' | 'reflink' | 'none';
|
|
5
|
+
/** Per-platform `cp` argv prefix for a CoW copy of one entry. */
|
|
6
|
+
export declare function cowCopyArgs(capability: Exclude<CowCapability, 'none'>): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Probes CoW support in `dir` by cloning a tiny probe file there.
|
|
9
|
+
* Non-darwin/linux platforms report 'none' without probing.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectCowSupport(dir: string, platform?: NodeJS.Platform, executor?: CloneExecutor): Promise<CowCapability>;
|
|
12
|
+
/**
|
|
13
|
+
* Copies one directory entry (file or directory, recursively) from
|
|
14
|
+
* `sourcePath` to `destinationPath` using the given capability —
|
|
15
|
+
* clonefile/reflink when available, a plain physical copy under
|
|
16
|
+
* 'none' (the `--force-copy` path; the CALLER gates on the flag).
|
|
17
|
+
*/
|
|
18
|
+
export declare function cloneEntry(capability: CowCapability, sourcePath: string, destinationPath: string, platform?: NodeJS.Platform, executor?: CloneExecutor): Promise<void>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Copy-on-write cloning for `fireforge tree` (FORGE G15).
|
|
4
|
+
*
|
|
5
|
+
* Capability is probed BY DOING — a tiny `cp -c` (APFS clonefile) or
|
|
6
|
+
* `cp --reflink=always` (btrfs/XFS) in the actual destination directory —
|
|
7
|
+
* so the verdict is truthful for the volume the trees will live on. A
|
|
8
|
+
* filesystem without CoW support REFUSES honestly; `--force-copy` opts
|
|
9
|
+
* into a full physical copy but is never implied (a Firefox tree can be
|
|
10
|
+
* tens of gigabytes).
|
|
11
|
+
*
|
|
12
|
+
* The exec layer is injected so unit tests assert the exact `cp` argv per
|
|
13
|
+
* platform without touching the filesystem.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { toError } from '../utils/errors.js';
|
|
18
|
+
import { verbose } from '../utils/logger.js';
|
|
19
|
+
import { exec } from '../utils/process.js';
|
|
20
|
+
const defaultExecutor = async (command, args) => {
|
|
21
|
+
const result = await exec(command, args);
|
|
22
|
+
if (result.exitCode !== 0) {
|
|
23
|
+
throw new Error(`${command} ${args.join(' ')} failed (exit ${String(result.exitCode)}): ${result.stderr}`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** Per-platform `cp` argv prefix for a CoW copy of one entry. */
|
|
27
|
+
export function cowCopyArgs(capability) {
|
|
28
|
+
return capability === 'clonefile' ? ['-c', '-R', '-p'] : ['--reflink=always', '-a'];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Probes CoW support in `dir` by cloning a tiny probe file there.
|
|
32
|
+
* Non-darwin/linux platforms report 'none' without probing.
|
|
33
|
+
*/
|
|
34
|
+
export async function detectCowSupport(dir, platform = process.platform, executor = defaultExecutor) {
|
|
35
|
+
if (platform !== 'darwin' && platform !== 'linux')
|
|
36
|
+
return 'none';
|
|
37
|
+
const capability = platform === 'darwin' ? 'clonefile' : 'reflink';
|
|
38
|
+
let probeDir;
|
|
39
|
+
try {
|
|
40
|
+
probeDir = await mkdtemp(join(dir, '.fireforge-cow-probe-'));
|
|
41
|
+
const src = join(probeDir, 'src');
|
|
42
|
+
const dst = join(probeDir, 'dst');
|
|
43
|
+
await writeFile(src, 'probe');
|
|
44
|
+
const flag = capability === 'clonefile' ? '-c' : '--reflink=always';
|
|
45
|
+
await executor('cp', [flag, src, dst]);
|
|
46
|
+
return capability;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
verbose(`CoW probe in ${dir} failed: ${toError(error).message}`);
|
|
50
|
+
return 'none';
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (probeDir !== undefined) {
|
|
54
|
+
await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Copies one directory entry (file or directory, recursively) from
|
|
60
|
+
* `sourcePath` to `destinationPath` using the given capability —
|
|
61
|
+
* clonefile/reflink when available, a plain physical copy under
|
|
62
|
+
* 'none' (the `--force-copy` path; the CALLER gates on the flag).
|
|
63
|
+
*/
|
|
64
|
+
export async function cloneEntry(capability, sourcePath, destinationPath, platform = process.platform, executor = defaultExecutor) {
|
|
65
|
+
const args = capability === 'none'
|
|
66
|
+
? platform === 'darwin'
|
|
67
|
+
? ['-R', '-p']
|
|
68
|
+
: ['-a']
|
|
69
|
+
: cowCopyArgs(capability);
|
|
70
|
+
await executor('cp', [...args, sourcePath, destinationPath]);
|
|
71
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type TreeMarker } from './tree-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* Per-command verdicts. `allowed` runs unconditionally; `refused` never
|
|
4
|
+
* runs in a tree; `conditional` consults `TREE_CONDITIONAL_CHECKS` with
|
|
5
|
+
* the invoked subcommand chain, parsed options, and the tree marker.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TREE_COMMAND_VERDICTS: Readonly<Record<string, 'allowed' | 'refused' | 'conditional'>>;
|
|
8
|
+
interface ConditionalInput {
|
|
9
|
+
/** Subcommand chain below the top-level command (e.g. ['staged-dependency']). */
|
|
10
|
+
subcommands: readonly string[];
|
|
11
|
+
/** Merged options of the ACTION command (leaf). */
|
|
12
|
+
options: Readonly<Record<string, unknown>>;
|
|
13
|
+
/** Positional args of the action command. */
|
|
14
|
+
args: readonly unknown[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Throws when `commandName` (with its subcommand chain and options) must
|
|
18
|
+
* not run inside the tree described by `marker`. Exported for the
|
|
19
|
+
* preAction hook in cli.ts and for the guard tests.
|
|
20
|
+
*/
|
|
21
|
+
export declare function enforceTreeGuard(marker: TreeMarker, commandName: string, input: ConditionalInput): void;
|
|
22
|
+
/**
|
|
23
|
+
* Commander preAction hook body: locates the enclosing tree (if any) and
|
|
24
|
+
* enforces the verdict table for the invoked command chain.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runTreeGuardHook(rootCommandName: string | undefined, actionCommand: {
|
|
27
|
+
name: () => string;
|
|
28
|
+
optsWithGlobals: () => Record<string, unknown>;
|
|
29
|
+
args: unknown[];
|
|
30
|
+
parent: {
|
|
31
|
+
name: () => string;
|
|
32
|
+
parent: unknown;
|
|
33
|
+
} | null;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
export {};
|