@orkestrel/scaffold 0.0.101 → 0.0.102
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/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
- package/dist/bin/main.js +483 -444
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
- package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
- package/dist/host/claude/rules/architecture.md +2 -2
- package/dist/host/claude/rules/tests.md +8 -2
- package/dist/host/claude/rules/typescript.md +130 -0
- package/dist/host/claude/rules/workspace.md +30 -12
- package/dist/host/configs/helpers.ts +206 -133
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +35 -29
- package/dist/host/tests/config.test.ts +1220 -633
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1669 -985
- package/dist/src/core/index.cjs +1595 -1686
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +202 -18
- package/dist/src/core/index.d.ts +202 -18
- package/dist/src/core/index.js +1590 -1687
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +448 -392
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1182 -1052
- package/dist/src/server/index.d.ts +1182 -1052
- package/dist/src/server/index.js +447 -395
- package/dist/src/server/index.js.map +1 -1
- package/package.json +2 -2
package/dist/bin/main.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { align, renderTable, strip, stripControls, width } from "@orkestrel/console";
|
|
3
|
-
import { attempt, isArray, isError, isInstance, isRecord, isString, parseJSON } from "@orkestrel/contract";
|
|
4
|
-
import { AXES, BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFORMANCE_TEST_PATH, Compiler, DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, FRAMEWORKS, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, HOST_PATHS, INTEGRATION_TEST_PATH, JOURNEY_CONFIG_PATH, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, RESERVED_SHEET_NAMES, SERVICE_SETUP_PATH, SHEET_ENTRY_NAME, SHOWCASE_CONFIG_PATH, SHOWCASE_DEV_DEPENDENCIES, SHOWCASE_PAGES_PATH, SKILLS_CONFIG_PATH, STYLES_ENTRY_PATH, ScaffoldError, THEMES_BARREL_PATH, THEMES_ENTRY_PATH, blueprintToConfigArtifacts, blueprintToDevDependencies, blueprintToFaces, blueprintToManifestOverrides, blueprintToProjects, blueprintToScripts, blueprintToSheets, blueprintToTestArtifacts, blueprintToWritableScripts, compareVersions, createBlueprint, extractRangeMajor, extractVersion, isFloorPath, isScaffoldError, isSheetName, manifestToDependencies, manifestToName, nameToGuide, parseExtension, replaceManifestOverrides, replaceManifestScripts, replacePlanRanges } from "../src/core/index.js";
|
|
3
|
+
import { attempt, isArray, isDefined, isError, isInstance, isRecord, isString, parseJSON } from "@orkestrel/contract";
|
|
4
|
+
import { AXES, BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFORMANCE_TEST_PATH, Compiler, DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, FRAMEWORKS, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, HOST_PATHS, INTEGRATION_TEST_PATH, JOURNEY_CONFIG_PATH, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, RESERVED_SHEET_NAMES, SERVICE_SETUP_PATH, SHEET_ENTRY_NAME, SHOWCASE_CONFIG_PATH, SHOWCASE_DEV_DEPENDENCIES, SHOWCASE_PAGES_PATH, SKILLS_CONFIG_PATH, STYLES_ENTRY_PATH, ScaffoldError, THEMES_BARREL_PATH, THEMES_ENTRY_PATH, blueprintToConfigArtifacts, blueprintToDevDependencies, blueprintToFaces, blueprintToManifestOverrides, blueprintToProjects, blueprintToScripts, blueprintToSheets, blueprintToTestArtifacts, blueprintToWritableScripts, compareVersions, createBlueprint, extractRangeMajor, extractVersion, isFloorPath, isScaffoldError, isSheetName, iterateAnswers, manifestToDependencies, manifestToName, nameToGuide, parseExtension, replaceManifestOverrides, replaceManifestScripts, replacePlanRanges } from "../src/core/index.js";
|
|
5
5
|
import { MAX_INVENTORY_PATHS, Materializer, Upstream, filesToHost, isExactCaseFile, isPhysicalDirectory, isWorktree, listFiles, readFileText, readHostFloor, readHostManifest, readSnapshot, resolveContainedPath } from "../src/server/index.js";
|
|
6
|
-
import { createMarkdown, flattenText, isTableNode } from "@orkestrel/markdown";
|
|
7
6
|
import { createSession } from "@orkestrel/process/server";
|
|
7
|
+
import { createMarkdown, flattenText, isTableNode } from "@orkestrel/markdown";
|
|
8
8
|
import { readdirSync } from "node:fs";
|
|
9
9
|
import { normalize } from "node:path";
|
|
10
10
|
import { parseArgs } from "node:util";
|
|
11
11
|
var package_default = {
|
|
12
12
|
name: "@orkestrel/scaffold",
|
|
13
|
-
version: "0.0.
|
|
13
|
+
version: "0.0.102",
|
|
14
14
|
description: "Scaffold workspaces with commands: new, audit, repair, catalog, and overwrite.",
|
|
15
15
|
keywords: [
|
|
16
16
|
"audit",
|
|
@@ -114,7 +114,7 @@ var package_default = {
|
|
|
114
114
|
"@orkestrel/guide": "^0.0.24",
|
|
115
115
|
"@orkestrel/html": "^0.0.12",
|
|
116
116
|
"@orkestrel/probe": "^0.0.23",
|
|
117
|
-
"@orkestrel/test": "^0.0.
|
|
117
|
+
"@orkestrel/test": "^0.0.26",
|
|
118
118
|
"@types/node": "^26.6.4",
|
|
119
119
|
"@vitest/browser-playwright": "^4.1.11",
|
|
120
120
|
"oxfmt": "^0.72.0",
|
|
@@ -643,34 +643,28 @@ function auditToExit(audit) {
|
|
|
643
643
|
*/
|
|
644
644
|
function manifestToWritableDependencies(manifest, blueprint) {
|
|
645
645
|
const declared = manifestToDependencies(manifest);
|
|
646
|
-
const runtime = [...dependenciesToFleet(declared.runtime)];
|
|
647
|
-
const development = [...dependenciesToFleet(declared.development)];
|
|
648
646
|
const parsed = parseJSON(manifest);
|
|
649
647
|
if (!isRecord(parsed)) return {
|
|
650
|
-
runtime,
|
|
651
|
-
development
|
|
648
|
+
runtime: dependenciesToFleet(declared.runtime),
|
|
649
|
+
development: dependenciesToFleet(declared.development)
|
|
652
650
|
};
|
|
653
651
|
const runtimeRecord = isRecord(parsed.dependencies) ? parsed.dependencies : {};
|
|
654
652
|
const developmentRecord = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
653
|
+
const rows = Object.keys(blueprintToDevDependencies(blueprint)).sort().filter((name) => !name.startsWith("@orkestrel/")).map((name) => ({
|
|
654
|
+
name,
|
|
655
|
+
runtimeRange: runtimeRecord[name],
|
|
656
|
+
developmentRange: developmentRecord[name],
|
|
657
|
+
dual: blueprint.showcase && blueprint.app.includes("browser") && Object.hasOwn(SHOWCASE_DEV_DEPENDENCIES, name)
|
|
658
|
+
}));
|
|
659
|
+
return {
|
|
660
|
+
runtime: [...dependenciesToFleet(declared.runtime), ...rows.flatMap(({ name, runtimeRange }) => isString(runtimeRange) ? [{
|
|
661
|
+
name,
|
|
662
|
+
range: runtimeRange
|
|
663
|
+
}] : [])],
|
|
664
|
+
development: [...dependenciesToFleet(declared.development), ...rows.flatMap(({ name, runtimeRange, developmentRange, dual }) => isString(runtimeRange) && !dual || !isString(developmentRange) ? [] : [{
|
|
667
665
|
name,
|
|
668
666
|
range: developmentRange
|
|
669
|
-
})
|
|
670
|
-
}
|
|
671
|
-
return {
|
|
672
|
-
runtime,
|
|
673
|
-
development
|
|
667
|
+
}])]
|
|
674
668
|
};
|
|
675
669
|
}
|
|
676
670
|
/**
|
|
@@ -787,12 +781,8 @@ function manifestToWritableScripts(manifest, blueprint) {
|
|
|
787
781
|
const steps = declared.split("&&").map((step) => step.trim());
|
|
788
782
|
const expected = planned.split("&&").map((step) => step.trim());
|
|
789
783
|
if (steps.length >= expected.length) return writable;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
while (cursor < expected.length && expected[cursor] !== step) cursor += 1;
|
|
793
|
-
if (cursor === expected.length) return writable;
|
|
794
|
-
cursor += 1;
|
|
795
|
-
}
|
|
784
|
+
const resumed = expected.values();
|
|
785
|
+
if (!steps.every((step) => resumed.some((entry) => entry === step))) return writable;
|
|
796
786
|
const supplied = new Set(writable.map(({ name }) => name));
|
|
797
787
|
for (const step of expected) {
|
|
798
788
|
if (steps.includes(step)) continue;
|
|
@@ -822,17 +812,15 @@ function dependenciesToFleet(dependencies) {
|
|
|
822
812
|
* no concrete version.
|
|
823
813
|
*/
|
|
824
814
|
function dependenciesToFloors(dependencies) {
|
|
825
|
-
const
|
|
826
|
-
for (const dependency of dependencies) {
|
|
815
|
+
const floors = [...iterateAnswers(dependencies, (dependency) => {
|
|
827
816
|
const version = dependency.range.startsWith("^") || dependency.range.startsWith("~") ? dependency.range.slice(1) : dependency.range;
|
|
828
|
-
|
|
829
|
-
releases.push({
|
|
817
|
+
return extractVersion(version) === void 0 ? void 0 : {
|
|
830
818
|
...dependency,
|
|
831
819
|
lookup: "found",
|
|
832
820
|
latest: version
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
return
|
|
821
|
+
};
|
|
822
|
+
})];
|
|
823
|
+
return floors.every(isDefined) ? floors : void 0;
|
|
836
824
|
}
|
|
837
825
|
/**
|
|
838
826
|
* Reads the exit code registry release evidence earns.
|
|
@@ -851,27 +839,24 @@ function releasesToExit(releases) {
|
|
|
851
839
|
* @returns One non-blocking question for each stale floor and each crossed major.
|
|
852
840
|
*/
|
|
853
841
|
function releasesToQuestions(releases, served = releases) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (release.name.startsWith("@orkestrel/")) continue;
|
|
842
|
+
return Iterator.from(releases).flatMap((release) => {
|
|
843
|
+
if (release.name.startsWith("@orkestrel/")) return [];
|
|
857
844
|
const declared = extractRangeMajor(release.range);
|
|
858
845
|
const current = release.lookup === "found" ? release.latest : void 0;
|
|
859
846
|
const resolved = current === void 0 ? void 0 : extractVersion(current);
|
|
860
847
|
const floor = extractVersion(release.range.startsWith("^") || release.range.startsWith("~") ? release.range.slice(1) : release.range);
|
|
861
|
-
|
|
848
|
+
const alternate = served.find((candidate) => candidate.name === release.name);
|
|
849
|
+
const published = release.major ?? (alternate?.lookup === "found" ? extractVersion(alternate.latest)?.[0] : void 0);
|
|
850
|
+
return [...declared !== void 0 && current !== void 0 && resolved !== void 0 && floor !== void 0 && resolved[0] === declared && compareVersions(current, floor.join(".")) > 0 ? [{
|
|
862
851
|
field: "dependencies",
|
|
863
852
|
message: `${release.name} declares the floor ${release.range}, while the registry serves ${current} within major ${String(declared)}.`,
|
|
864
853
|
blocking: false
|
|
865
|
-
}
|
|
866
|
-
const alternate = served.find((candidate) => candidate.name === release.name);
|
|
867
|
-
const published = release.major ?? (alternate?.lookup === "found" ? extractVersion(alternate.latest)?.[0] : void 0);
|
|
868
|
-
if (declared !== void 0 && published !== void 0 && declared < published) questions.push({
|
|
854
|
+
}] : [], ...declared !== void 0 && published !== void 0 && declared < published ? [{
|
|
869
855
|
field: "dependencies",
|
|
870
856
|
message: `${release.name} declares major ${String(declared)}, while the registry serves major ${String(published)}.`,
|
|
871
857
|
blocking: false
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
return questions;
|
|
858
|
+
}] : []];
|
|
859
|
+
}).toArray();
|
|
875
860
|
}
|
|
876
861
|
/**
|
|
877
862
|
* Projects an audit into the human summary of its outcome.
|
|
@@ -1122,17 +1107,16 @@ function releasesToPins(releases, declared) {
|
|
|
1122
1107
|
const refused = releases.filter((release) => release.lookup !== "found");
|
|
1123
1108
|
if (refused.length > 0) throw new ScaffoldError("FETCH", `The registry named no release for ${refused.map((release) => release.name).join(", ")}.`, { names: refused.length });
|
|
1124
1109
|
const dependencies = [...declared.runtime, ...declared.development];
|
|
1125
|
-
const pins =
|
|
1126
|
-
for (let index = 0; index < releases.length; index += 1) {
|
|
1110
|
+
const pins = Array.from(releases.keys(), (index) => {
|
|
1127
1111
|
const release = releases[index];
|
|
1128
1112
|
const dependency = dependencies[index];
|
|
1129
1113
|
if (release === void 0 || dependency === void 0) throw new ScaffoldError("FETCH", "The release answer has no matching declaration.");
|
|
1130
1114
|
if (release.lookup !== "found") throw new ScaffoldError("FETCH", `The registry named no release for ${release.name}.`);
|
|
1131
|
-
|
|
1115
|
+
return {
|
|
1132
1116
|
name: dependency.name,
|
|
1133
1117
|
range: `^${release.latest}`
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1118
|
+
};
|
|
1119
|
+
});
|
|
1136
1120
|
if (pins.length !== dependencies.length) throw new ScaffoldError("FETCH", "The release answer does not match the declaration set.");
|
|
1137
1121
|
return {
|
|
1138
1122
|
runtime: pins.slice(0, declared.runtime.length),
|
|
@@ -1140,6 +1124,60 @@ function releasesToPins(releases, declared) {
|
|
|
1140
1124
|
};
|
|
1141
1125
|
}
|
|
1142
1126
|
/**
|
|
1127
|
+
* Scans a shell command for the words and operators it splits into.
|
|
1128
|
+
*
|
|
1129
|
+
* @param script - The shell command text to split.
|
|
1130
|
+
* @returns The words and the `&&`, `||`, `;`, `|`, `&`, `(`, and `)` operators in command order, or
|
|
1131
|
+
* `undefined` when a quote is never closed or the command ends in a backslash.
|
|
1132
|
+
*
|
|
1133
|
+
* @remarks
|
|
1134
|
+
* Quotes group a word without hiding it and a backslash outside single quotes escapes the next
|
|
1135
|
+
* character, so the word value carries neither. A `$`, a backtick, or a `%` that no backslash
|
|
1136
|
+
* escapes and no single quote encloses marks its word unresolved, because a shell expansion
|
|
1137
|
+
* decides what the word names. Operators are always resolved.
|
|
1138
|
+
*
|
|
1139
|
+
* @example
|
|
1140
|
+
* ```ts
|
|
1141
|
+
* import { scanScriptTokens } from './helpers.js'
|
|
1142
|
+
*
|
|
1143
|
+
* scanScriptTokens('vitest run --project "src:core"')?.map((token) => token.value)
|
|
1144
|
+
* // ['vitest', 'run', '--project', 'src:core']
|
|
1145
|
+
* ```
|
|
1146
|
+
*/
|
|
1147
|
+
function scanScriptTokens(script) {
|
|
1148
|
+
const matches = Array.from(script.matchAll(/(?<space>\s+)|(?<operator>&&|\|\||[;&|()])|(?<word>(?:[^\s;&|()"'\\]|\\[\s\S]|"(?:\\[\s\S]|[^"\\])*"|'[^']*')+)|[\s\S]/g), ({ groups }) => groups ?? {});
|
|
1149
|
+
if (matches.some(({ space, operator, word }) => space === void 0 && operator === void 0 && word === void 0)) return;
|
|
1150
|
+
return matches.flatMap(({ space, operator, word }) => {
|
|
1151
|
+
if (space !== void 0) return [];
|
|
1152
|
+
if (operator !== void 0) return [{
|
|
1153
|
+
value: operator,
|
|
1154
|
+
resolved: true
|
|
1155
|
+
}];
|
|
1156
|
+
const parts = Array.from((word ?? "").matchAll(/\\([\s\S])|"((?:\\[\s\S]|[^"\\])*)"|'([^']*)'|([\s\S])/g), ([, escaped, double, single, plain]) => {
|
|
1157
|
+
if (escaped !== void 0) return {
|
|
1158
|
+
text: escaped,
|
|
1159
|
+
resolved: true
|
|
1160
|
+
};
|
|
1161
|
+
if (single !== void 0) return {
|
|
1162
|
+
text: single,
|
|
1163
|
+
resolved: true
|
|
1164
|
+
};
|
|
1165
|
+
if (double !== void 0) return {
|
|
1166
|
+
text: double.replaceAll(/\\([\s\S])/g, "$1"),
|
|
1167
|
+
resolved: Array.from(double.matchAll(/\\[\s\S]|[$`%]/g)).every((found) => found[0].startsWith("\\"))
|
|
1168
|
+
};
|
|
1169
|
+
return {
|
|
1170
|
+
text: plain ?? "",
|
|
1171
|
+
resolved: plain === void 0 || !"$`%".includes(plain)
|
|
1172
|
+
};
|
|
1173
|
+
});
|
|
1174
|
+
return [{
|
|
1175
|
+
value: parts.map((part) => part.text).join(""),
|
|
1176
|
+
resolved: parts.every((part) => part.resolved)
|
|
1177
|
+
}];
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1143
1181
|
* Reads the literal Vitest projects, configuration paths, and npm run scripts a shell command names.
|
|
1144
1182
|
*
|
|
1145
1183
|
* @param script - The manifest script text to read.
|
|
@@ -1163,95 +1201,21 @@ function releasesToPins(releases, declared) {
|
|
|
1163
1201
|
* ```
|
|
1164
1202
|
*/
|
|
1165
1203
|
function scriptToInvocations(script, wrappers = /* @__PURE__ */ new Map()) {
|
|
1166
|
-
const tokens =
|
|
1167
|
-
|
|
1168
|
-
let resolved = true;
|
|
1169
|
-
let started = false;
|
|
1170
|
-
let quote;
|
|
1171
|
-
for (let index = 0; index < script.length; index += 1) {
|
|
1172
|
-
const character = script[index];
|
|
1173
|
-
if (character === void 0) return void 0;
|
|
1174
|
-
if (quote === void 0) {
|
|
1175
|
-
if (/\s/.test(character)) {
|
|
1176
|
-
if (started) tokens.push({
|
|
1177
|
-
value,
|
|
1178
|
-
resolved
|
|
1179
|
-
});
|
|
1180
|
-
value = "";
|
|
1181
|
-
resolved = true;
|
|
1182
|
-
started = false;
|
|
1183
|
-
continue;
|
|
1184
|
-
}
|
|
1185
|
-
if (";&|()".includes(character)) {
|
|
1186
|
-
if (started) tokens.push({
|
|
1187
|
-
value,
|
|
1188
|
-
resolved
|
|
1189
|
-
});
|
|
1190
|
-
const paired = script[index + 1] === character && (character === "&" || character === "|");
|
|
1191
|
-
tokens.push({
|
|
1192
|
-
value: paired ? `${character}${character}` : character,
|
|
1193
|
-
resolved: true
|
|
1194
|
-
});
|
|
1195
|
-
value = "";
|
|
1196
|
-
resolved = true;
|
|
1197
|
-
started = false;
|
|
1198
|
-
if (paired) index += 1;
|
|
1199
|
-
continue;
|
|
1200
|
-
}
|
|
1201
|
-
if (character === "\"" || character === "'") {
|
|
1202
|
-
quote = character;
|
|
1203
|
-
started = true;
|
|
1204
|
-
continue;
|
|
1205
|
-
}
|
|
1206
|
-
if (character === "\\") {
|
|
1207
|
-
const escaped = script[index + 1];
|
|
1208
|
-
if (escaped === void 0) return void 0;
|
|
1209
|
-
value += escaped;
|
|
1210
|
-
started = true;
|
|
1211
|
-
index += 1;
|
|
1212
|
-
continue;
|
|
1213
|
-
}
|
|
1214
|
-
if (character === "$" || character === "`" || character === "%") resolved = false;
|
|
1215
|
-
value += character;
|
|
1216
|
-
started = true;
|
|
1217
|
-
continue;
|
|
1218
|
-
}
|
|
1219
|
-
if (character === quote) {
|
|
1220
|
-
quote = void 0;
|
|
1221
|
-
continue;
|
|
1222
|
-
}
|
|
1223
|
-
if (character === "\\" && quote === "\"") {
|
|
1224
|
-
const escaped = script[index + 1];
|
|
1225
|
-
if (escaped === void 0) return void 0;
|
|
1226
|
-
value += escaped;
|
|
1227
|
-
index += 1;
|
|
1228
|
-
continue;
|
|
1229
|
-
}
|
|
1230
|
-
if (quote === "\"" && (character === "$" || character === "`" || character === "%")) resolved = false;
|
|
1231
|
-
value += character;
|
|
1232
|
-
started = true;
|
|
1233
|
-
}
|
|
1234
|
-
if (quote !== void 0) return void 0;
|
|
1235
|
-
if (started) tokens.push({
|
|
1236
|
-
value,
|
|
1237
|
-
resolved
|
|
1238
|
-
});
|
|
1239
|
-
const projects = [];
|
|
1240
|
-
const configs = [];
|
|
1241
|
-
const scripts = [];
|
|
1204
|
+
const tokens = scanScriptTokens(script);
|
|
1205
|
+
if (tokens === void 0) return void 0;
|
|
1242
1206
|
const configurations = new Map([...wrappers].map(([path, project]) => [normalize(path), project]));
|
|
1243
|
-
|
|
1244
|
-
for (
|
|
1245
|
-
const
|
|
1246
|
-
if (
|
|
1247
|
-
|
|
1207
|
+
const steps = [];
|
|
1208
|
+
for (const [index, token] of tokens.entries()) {
|
|
1209
|
+
const previous = steps.at(-1);
|
|
1210
|
+
if (previous !== void 0 && index <= previous.through) continue;
|
|
1211
|
+
const running = index === 0 || [
|
|
1248
1212
|
"&&",
|
|
1249
1213
|
"||",
|
|
1250
1214
|
";",
|
|
1251
1215
|
"|",
|
|
1252
1216
|
"&",
|
|
1253
1217
|
"("
|
|
1254
|
-
].includes(tokens[index - 1]?.value ?? "")
|
|
1218
|
+
].includes(tokens[index - 1]?.value ?? "") ? token.resolved && token.value === "vitest" && tokens[index + 1]?.value === "run" : previous?.running ?? false;
|
|
1255
1219
|
if (token.value === "npm" && tokens[index + 1]?.value === "run") {
|
|
1256
1220
|
const name = tokens[index + 2];
|
|
1257
1221
|
if (name === void 0 || !name.resolved || name.value.length === 0 || [
|
|
@@ -1263,8 +1227,13 @@ function scriptToInvocations(script, wrappers = /* @__PURE__ */ new Map()) {
|
|
|
1263
1227
|
"(",
|
|
1264
1228
|
")"
|
|
1265
1229
|
].includes(name.value)) return void 0;
|
|
1266
|
-
|
|
1267
|
-
|
|
1230
|
+
steps.push({
|
|
1231
|
+
projects: [],
|
|
1232
|
+
configs: [],
|
|
1233
|
+
scripts: [name.value],
|
|
1234
|
+
through: index + 2,
|
|
1235
|
+
running
|
|
1236
|
+
});
|
|
1268
1237
|
continue;
|
|
1269
1238
|
}
|
|
1270
1239
|
if (token.value === "--project" || token.value === "--config" || token.value === "-c") {
|
|
@@ -1278,28 +1247,88 @@ function scriptToInvocations(script, wrappers = /* @__PURE__ */ new Map()) {
|
|
|
1278
1247
|
"(",
|
|
1279
1248
|
")"
|
|
1280
1249
|
].includes(project.value)) return void 0;
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1250
|
+
const named = token.value === "--project";
|
|
1251
|
+
const face = running && !named ? configurations.get(normalize(project.value)) : void 0;
|
|
1252
|
+
steps.push({
|
|
1253
|
+
projects: [...named ? [project.value] : [], ...face === void 0 ? [] : [face]],
|
|
1254
|
+
configs: named ? [] : [project.value],
|
|
1255
|
+
scripts: [],
|
|
1256
|
+
through: index + 1,
|
|
1257
|
+
running
|
|
1258
|
+
});
|
|
1286
1259
|
continue;
|
|
1287
1260
|
}
|
|
1288
1261
|
if (token.value.startsWith("--project=") || token.value.startsWith("--config=")) {
|
|
1289
1262
|
const project = token.value.slice(token.value.indexOf("=") + 1);
|
|
1290
1263
|
if (!token.resolved || project.length === 0) return void 0;
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1264
|
+
const named = token.value.startsWith("--project=");
|
|
1265
|
+
const face = running && !named ? configurations.get(normalize(project)) : void 0;
|
|
1266
|
+
steps.push({
|
|
1267
|
+
projects: [...named ? [project] : [], ...face === void 0 ? [] : [face]],
|
|
1268
|
+
configs: named ? [] : [project],
|
|
1269
|
+
scripts: [],
|
|
1270
|
+
through: index,
|
|
1271
|
+
running
|
|
1272
|
+
});
|
|
1295
1273
|
continue;
|
|
1296
1274
|
}
|
|
1297
1275
|
if (!token.resolved && /--project|--config/.test(token.value)) return void 0;
|
|
1276
|
+
steps.push({
|
|
1277
|
+
projects: [],
|
|
1278
|
+
configs: [],
|
|
1279
|
+
scripts: [],
|
|
1280
|
+
through: index,
|
|
1281
|
+
running
|
|
1282
|
+
});
|
|
1298
1283
|
}
|
|
1299
1284
|
return {
|
|
1300
|
-
projects,
|
|
1301
|
-
configs,
|
|
1302
|
-
scripts
|
|
1285
|
+
projects: steps.flatMap(({ projects }) => projects),
|
|
1286
|
+
configs: steps.flatMap(({ configs }) => configs),
|
|
1287
|
+
scripts: steps.flatMap(({ scripts }) => scripts)
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Collects the scripts, Vitest projects, and unreadable scripts one gate reaches through `npm run`.
|
|
1292
|
+
*
|
|
1293
|
+
* @param gate - The script name the walk starts at.
|
|
1294
|
+
* @param scripts - The manifest's scripts by name; a value that is not a string names no command.
|
|
1295
|
+
* @param wrappers - Planned wrapper paths mapped to the face projects they run.
|
|
1296
|
+
* @returns The script names the walk visited, the Vitest projects those scripts name, and the
|
|
1297
|
+
* visited scripts whose command cannot be read literally.
|
|
1298
|
+
*
|
|
1299
|
+
* @remarks
|
|
1300
|
+
* The walk is breadth first and reads each script once, so a cycle ends where it closes. A script
|
|
1301
|
+
* the manifest does not declare as a string is visited and names nothing. The projects and the
|
|
1302
|
+
* unreadable names derive from one log of the reads, never from sets the walk fills.
|
|
1303
|
+
*
|
|
1304
|
+
* @example
|
|
1305
|
+
* ```ts
|
|
1306
|
+
* import { collectScriptChain } from './helpers.js'
|
|
1307
|
+
*
|
|
1308
|
+
* collectScriptChain('test', { test: 'npm run test:src', 'test:src': 'vitest run --project src:core' }, new Map())
|
|
1309
|
+
* // { visited: Set { 'test', 'test:src' }, projects: Set { 'src:core' }, unreadable: Set {} }
|
|
1310
|
+
* ```
|
|
1311
|
+
*/
|
|
1312
|
+
function collectScriptChain(gate, scripts, wrappers) {
|
|
1313
|
+
const queue = [gate];
|
|
1314
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1315
|
+
const reads = [];
|
|
1316
|
+
for (const name of queue) {
|
|
1317
|
+
if (visited.has(name)) continue;
|
|
1318
|
+
visited.add(name);
|
|
1319
|
+
const script = scripts[name];
|
|
1320
|
+
if (!isString(script)) continue;
|
|
1321
|
+
const invoked = scriptToInvocations(script, wrappers);
|
|
1322
|
+
reads.push({
|
|
1323
|
+
name,
|
|
1324
|
+
invoked
|
|
1325
|
+
});
|
|
1326
|
+
for (const called of invoked?.scripts ?? []) if (!visited.has(called)) queue.push(called);
|
|
1327
|
+
}
|
|
1328
|
+
return {
|
|
1329
|
+
visited,
|
|
1330
|
+
projects: new Set(reads.flatMap(({ invoked }) => invoked?.projects ?? [])),
|
|
1331
|
+
unreadable: new Set(reads.flatMap(({ name, invoked }) => invoked === void 0 ? [name] : []))
|
|
1303
1332
|
};
|
|
1304
1333
|
}
|
|
1305
1334
|
/**
|
|
@@ -1350,42 +1379,36 @@ function targetToFacts(target) {
|
|
|
1350
1379
|
* @throws `ScaffoldError` when sheet markers name an invalid or case-colliding face, or the directory cannot be read.
|
|
1351
1380
|
*/
|
|
1352
1381
|
function targetToExtensions(target) {
|
|
1353
|
-
const
|
|
1354
|
-
for (const name of FRAMEWORKS) {
|
|
1382
|
+
const browsers = FRAMEWORKS.flatMap((name) => {
|
|
1355
1383
|
const axes = AXES.filter((axis) => {
|
|
1356
1384
|
const path = resolveContainedPath(target, `${axis}/${name}`);
|
|
1357
1385
|
return path !== void 0 && isPhysicalDirectory(path);
|
|
1358
1386
|
});
|
|
1359
|
-
|
|
1387
|
+
return axes.length > 0 ? [{
|
|
1360
1388
|
surface: "browser",
|
|
1361
1389
|
name,
|
|
1362
1390
|
axes
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1391
|
+
}] : [];
|
|
1392
|
+
});
|
|
1365
1393
|
const source = resolveContainedPath(target, "src");
|
|
1366
|
-
if (source === void 0 || !isPhysicalDirectory(source)) return
|
|
1394
|
+
if (source === void 0 || !isPhysicalDirectory(source)) return browsers;
|
|
1367
1395
|
const entries = readdirSync(source, { withFileTypes: true });
|
|
1368
1396
|
if (entries.length > MAX_INVENTORY_PATHS) throw new ScaffoldError("TARGET", "The source directory exceeds the inventory limit.");
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1371
|
-
for (const entry of entries) {
|
|
1372
|
-
if (!entry.isDirectory() || RESERVED_SHEET_NAMES.includes(entry.name)) continue;
|
|
1397
|
+
const faces = entries.filter((entry) => {
|
|
1398
|
+
if (!entry.isDirectory() || RESERVED_SHEET_NAMES.includes(entry.name)) return false;
|
|
1373
1399
|
const sheet = resolveContainedPath(target, `src/${entry.name}/index.scss`);
|
|
1374
1400
|
const marker = resolveContainedPath(target, `src/${entry.name}/${SHEET_ENTRY_NAME}`);
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
}
|
|
1381
|
-
for (const name of names.sort()) {
|
|
1401
|
+
return sheet !== void 0 && marker !== void 0 && isExactCaseFile(sheet) && isExactCaseFile(marker);
|
|
1402
|
+
});
|
|
1403
|
+
const colliding = faces.find((face, index) => faces.findIndex((other) => other.name.toLowerCase() === face.name.toLowerCase()) < index);
|
|
1404
|
+
if (colliding !== void 0) throw new ScaffoldError("TARGET", `Case-colliding sheet face '${colliding.name}'.`);
|
|
1405
|
+
return [...browsers, ...faces.map((face) => face.name).sort().map((name) => {
|
|
1382
1406
|
if (!isSheetName(name)) throw new ScaffoldError("TARGET", `Invalid sheet face '${name}'.`);
|
|
1383
|
-
|
|
1407
|
+
return {
|
|
1384
1408
|
surface: "styles",
|
|
1385
1409
|
name
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
return extensions;
|
|
1410
|
+
};
|
|
1411
|
+
})];
|
|
1389
1412
|
}
|
|
1390
1413
|
/**
|
|
1391
1414
|
* Reads a creation extension list and assigns its selected browser axes.
|
|
@@ -1397,24 +1420,19 @@ function targetToExtensions(target) {
|
|
|
1397
1420
|
*/
|
|
1398
1421
|
function selectionToExtensions(selection, axes, styles) {
|
|
1399
1422
|
if (selection === void 0) return [];
|
|
1400
|
-
|
|
1401
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1402
|
-
for (const entry of selection.split(",")) {
|
|
1423
|
+
return selection.split(",").map((entry, index, entries) => {
|
|
1403
1424
|
const extension = parseExtension(entry);
|
|
1404
|
-
if (extension === void 0 ||
|
|
1405
|
-
seen.add(entry);
|
|
1425
|
+
if (extension === void 0 || entries.indexOf(entry) < index) throw new UsageError(`'--extend' refuses '${entry}': use distinct supported surface:name entries.`);
|
|
1406
1426
|
if (extension.surface === "browser") {
|
|
1407
1427
|
if (axes.length === 0) throw new UsageError("A browser extension requires browser in --src or --app.");
|
|
1408
|
-
|
|
1428
|
+
return {
|
|
1409
1429
|
...extension,
|
|
1410
1430
|
axes
|
|
1411
|
-
}
|
|
1412
|
-
} else {
|
|
1413
|
-
if (!styles) throw new UsageError("A styles extension requires --styles.");
|
|
1414
|
-
extensions.push(extension);
|
|
1431
|
+
};
|
|
1415
1432
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1433
|
+
if (!styles) throw new UsageError("A styles extension requires --styles.");
|
|
1434
|
+
return extension;
|
|
1435
|
+
});
|
|
1418
1436
|
}
|
|
1419
1437
|
/**
|
|
1420
1438
|
* Reads the packages a target's catalog table listed before this run.
|
|
@@ -1438,118 +1456,10 @@ function catalogToNames(target) {
|
|
|
1438
1456
|
const hex = isString(target) ? void 0 : target.bytes[CATALOG_AGENT_PATH];
|
|
1439
1457
|
const text = isString(target) ? readFileText(target, CATALOG_AGENT_PATH) : hex === void 0 ? void 0 : Buffer.from(hex, "hex").toString("utf8");
|
|
1440
1458
|
if (text === void 0) return [];
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
const name = cell.map(flattenText).join("").trim();
|
|
1446
|
-
if (DEPENDENCY_NAME_PATTERN.test(name)) names.push(name);
|
|
1447
|
-
}
|
|
1448
|
-
return names;
|
|
1449
|
-
}
|
|
1450
|
-
/**
|
|
1451
|
-
* Collects the complete NUL-delimited records from one raw git output chunk.
|
|
1452
|
-
*
|
|
1453
|
-
* @param records - The complete records retained in arrival order.
|
|
1454
|
-
* @param pending - The mutable cell carrying the unfinished record between chunks.
|
|
1455
|
-
* @param decoder - The streaming UTF-8 decoder shared by the query.
|
|
1456
|
-
* @param controller - The query controller aborted on a bound refusal.
|
|
1457
|
-
* @param target - The repository the query addresses.
|
|
1458
|
-
* @param chunk - The next owned output bytes.
|
|
1459
|
-
* @returns Nothing.
|
|
1460
|
-
*/
|
|
1461
|
-
function collectGitRecords(records, pending, decoder, controller, target, chunk) {
|
|
1462
|
-
if (controller.signal.aborted) return;
|
|
1463
|
-
const parts = `${pending[0] ?? ""}${decoder.decode(chunk, { stream: true })}`.split("\0");
|
|
1464
|
-
pending[0] = parts.pop() ?? "";
|
|
1465
|
-
for (const record of parts) {
|
|
1466
|
-
if (record.length === 0) continue;
|
|
1467
|
-
if (records.length >= MAX_INVENTORY_PATHS) {
|
|
1468
|
-
refuseGitRecords(controller, target, `The git query for target at ${target} exceeded the working-tree inventory limit.`);
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
records.push(record);
|
|
1472
|
-
}
|
|
1473
|
-
if ((pending[0] ?? "").length > MAX_PATH_LENGTH + 3) refuseGitRecords(controller, target, `The git query for target at ${target} returned an unfinished record beyond the supported path limit.`);
|
|
1474
|
-
}
|
|
1475
|
-
/**
|
|
1476
|
-
* Aborts a git query with its first target refusal.
|
|
1477
|
-
*
|
|
1478
|
-
* @param controller - The query controller whose reason retains the refusal.
|
|
1479
|
-
* @param target - The repository the query addresses.
|
|
1480
|
-
* @param message - The refusal message.
|
|
1481
|
-
* @returns Nothing.
|
|
1482
|
-
*/
|
|
1483
|
-
function refuseGitRecords(controller, target, message) {
|
|
1484
|
-
if (!controller.signal.aborted) controller.abort(new ScaffoldError("TARGET", message, { target }));
|
|
1485
|
-
}
|
|
1486
|
-
/**
|
|
1487
|
-
* Reads one git query as its NUL-separated records.
|
|
1488
|
-
*
|
|
1489
|
-
* @param target - The repository the query runs in.
|
|
1490
|
-
* @param args - The git arguments, without the program name.
|
|
1491
|
-
* @returns The non-empty records git wrote, in git's own order.
|
|
1492
|
-
* @throws `ScaffoldError('TARGET', …)` when the query fails or its output is
|
|
1493
|
-
* incomplete or outside the working-tree inventory bounds.
|
|
1494
|
-
*
|
|
1495
|
-
* @remarks
|
|
1496
|
-
* Git is asked rather than reimplemented, because the tracked set and the dirty
|
|
1497
|
-
* set are git's own answers and nothing else can give them. The raw session
|
|
1498
|
-
* streams complete NUL-delimited records without retaining the whole query as a
|
|
1499
|
-
* manifest-sized text buffer. Its initial hooks are installed before the eager
|
|
1500
|
-
* spawn, so no output or child fault can precede its observer.
|
|
1501
|
-
*
|
|
1502
|
-
* @example
|
|
1503
|
-
* ```ts
|
|
1504
|
-
* import { readGitRecords } from './helpers.js'
|
|
1505
|
-
*
|
|
1506
|
-
* await readGitRecords('./packages/router', ['ls-files', '-z']) // ['package.json', 'src/core/index.ts']
|
|
1507
|
-
* ```
|
|
1508
|
-
*/
|
|
1509
|
-
async function readGitRecords(target, args) {
|
|
1510
|
-
const records = [];
|
|
1511
|
-
const pending = [""];
|
|
1512
|
-
const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
1513
|
-
const controller = new AbortController();
|
|
1514
|
-
let session;
|
|
1515
|
-
try {
|
|
1516
|
-
session = createSession({
|
|
1517
|
-
command: {
|
|
1518
|
-
file: "git",
|
|
1519
|
-
arguments: [
|
|
1520
|
-
"-C",
|
|
1521
|
-
target,
|
|
1522
|
-
...args
|
|
1523
|
-
]
|
|
1524
|
-
},
|
|
1525
|
-
workspace: ".",
|
|
1526
|
-
signal: controller.signal,
|
|
1527
|
-
on: {
|
|
1528
|
-
stdout: collectGitRecords.bind(void 0, records, pending, decoder, controller, target),
|
|
1529
|
-
error: refuseGitRecords.bind(void 0, controller, target, `The git query for target at ${target} failed before it completed.`)
|
|
1530
|
-
},
|
|
1531
|
-
error: refuseGitRecords.bind(void 0, controller, target, `The git query for target at ${target} failed while reading its output.`)
|
|
1532
|
-
});
|
|
1533
|
-
const exit = await session.exit;
|
|
1534
|
-
pending[0] = `${pending[0] ?? ""}${decoder.decode()}`;
|
|
1535
|
-
if (controller.signal.aborted) throw controller.signal.reason;
|
|
1536
|
-
if (exit.signal !== null) throw new ScaffoldError("TARGET", `The git query for target at ${target} ended from signal ${exit.signal}.`, {
|
|
1537
|
-
target,
|
|
1538
|
-
signal: exit.signal
|
|
1539
|
-
});
|
|
1540
|
-
if (exit.code !== 0) throw new ScaffoldError("TARGET", `The git query for target at ${target} failed with exit code ${String(exit.code)}.`, {
|
|
1541
|
-
target,
|
|
1542
|
-
code: exit.code
|
|
1543
|
-
});
|
|
1544
|
-
if (!exit.drained) throw new ScaffoldError("TARGET", `The git query for target at ${target} ended before its output drained.`, { target });
|
|
1545
|
-
if ((pending[0] ?? "").length > 0) throw new ScaffoldError("TARGET", `The git query for target at ${target} returned an incomplete record.`, { target });
|
|
1546
|
-
return records;
|
|
1547
|
-
} catch (error) {
|
|
1548
|
-
if (isScaffoldError(error)) throw error;
|
|
1549
|
-
throw new ScaffoldError("TARGET", `The git query for target at ${target} could not start.`, { target });
|
|
1550
|
-
} finally {
|
|
1551
|
-
if (session !== void 0) await session.destroy();
|
|
1552
|
-
}
|
|
1459
|
+
return createMarkdown(text).filter(isTableNode).flatMap((table) => table.rows.flatMap(([cell]) => {
|
|
1460
|
+
const name = cell?.map(flattenText).join("").trim();
|
|
1461
|
+
return name !== void 0 && DEPENDENCY_NAME_PATTERN.test(name) ? [name] : [];
|
|
1462
|
+
}));
|
|
1553
1463
|
}
|
|
1554
1464
|
/**
|
|
1555
1465
|
* Reads the environments a comma-separated selection names.
|
|
@@ -1665,6 +1575,158 @@ function mergeResults(first, second) {
|
|
|
1665
1575
|
function resultToTally(result) {
|
|
1666
1576
|
return `${String(result.written.length)} written, ${String(result.skipped.length)} unchanged, ${String(result.removed.length)} removed in ${result.target}.`;
|
|
1667
1577
|
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Splits decoded git output into its complete records and its unfinished tail.
|
|
1580
|
+
*
|
|
1581
|
+
* @param text - The decoded output, led by the tail the previous chunk left.
|
|
1582
|
+
* @returns The non-empty records ended by a NUL, and the text after the last NUL.
|
|
1583
|
+
*
|
|
1584
|
+
* @example
|
|
1585
|
+
* ```ts
|
|
1586
|
+
* import { splitGitRecords } from './helpers.js'
|
|
1587
|
+
*
|
|
1588
|
+
* splitGitRecords('alpha\0\0br') // { records: ['alpha'], pending: 'br' }
|
|
1589
|
+
* ```
|
|
1590
|
+
*/
|
|
1591
|
+
function splitGitRecords(text) {
|
|
1592
|
+
const parts = text.split("\0");
|
|
1593
|
+
return {
|
|
1594
|
+
records: parts.slice(0, -1).filter((record) => record.length > 0),
|
|
1595
|
+
pending: parts.at(-1) ?? ""
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Finds the refusal one git output chunk earns.
|
|
1600
|
+
*
|
|
1601
|
+
* @param target - The repository the query runs in.
|
|
1602
|
+
* @param count - The records collected before the chunk.
|
|
1603
|
+
* @param chunk - The next split output.
|
|
1604
|
+
* @returns The inventory message when the records pass the working-tree inventory limit, else the
|
|
1605
|
+
* unfinished-record message when the tail passes the supported path limit, else `undefined`.
|
|
1606
|
+
*
|
|
1607
|
+
* @example
|
|
1608
|
+
* ```ts
|
|
1609
|
+
* import { findGitRefusal } from './helpers.js'
|
|
1610
|
+
*
|
|
1611
|
+
* findGitRefusal('./packages/router', 0, { records: ['package.json'], pending: '' }) // undefined
|
|
1612
|
+
* ```
|
|
1613
|
+
*/
|
|
1614
|
+
function findGitRefusal(target, count, chunk) {
|
|
1615
|
+
if (count + chunk.records.length > MAX_INVENTORY_PATHS) return `The git query for target at ${target} exceeded the working-tree inventory limit.`;
|
|
1616
|
+
return chunk.pending.length > MAX_PATH_LENGTH + 3 ? `The git query for target at ${target} returned an unfinished record beyond the supported path limit.` : void 0;
|
|
1617
|
+
}
|
|
1618
|
+
//#endregion
|
|
1619
|
+
//#region src/bin/GitQuery.ts
|
|
1620
|
+
/**
|
|
1621
|
+
* Represents one git query read as its NUL-separated records.
|
|
1622
|
+
*
|
|
1623
|
+
* @remarks
|
|
1624
|
+
* Git is asked rather than reimplemented, because the tracked set and the dirty set are git's own
|
|
1625
|
+
* answers and nothing else can give them. The raw session streams complete NUL-delimited records
|
|
1626
|
+
* without retaining the whole query as a manifest-sized text buffer. Its hooks are installed
|
|
1627
|
+
* before the eager spawn, so no output or child fault can precede its observer.
|
|
1628
|
+
*
|
|
1629
|
+
* A query is created for one run: the unfinished record, the retained records, and the abort
|
|
1630
|
+
* signal all belong to that run, and a second `read` is refused rather than reset. Only `read`
|
|
1631
|
+
* drives the intake, so no caller feeds a run its output.
|
|
1632
|
+
*
|
|
1633
|
+
* @example
|
|
1634
|
+
* ```ts
|
|
1635
|
+
* import { GitQuery } from './GitQuery.js'
|
|
1636
|
+
*
|
|
1637
|
+
* await new GitQuery('./packages/router', ['ls-files', '-z']).read()
|
|
1638
|
+
* // ['package.json', 'src/core/index.ts']
|
|
1639
|
+
* ```
|
|
1640
|
+
*/
|
|
1641
|
+
var GitQuery = class {
|
|
1642
|
+
#target;
|
|
1643
|
+
#args;
|
|
1644
|
+
#records = [];
|
|
1645
|
+
#decoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
1646
|
+
#controller = new AbortController();
|
|
1647
|
+
#pending = "";
|
|
1648
|
+
#started = false;
|
|
1649
|
+
/**
|
|
1650
|
+
* Creates a query over one repository.
|
|
1651
|
+
*
|
|
1652
|
+
* @param target - The repository the query runs in.
|
|
1653
|
+
* @param args - The git arguments, without the program name.
|
|
1654
|
+
*/
|
|
1655
|
+
constructor(target, args) {
|
|
1656
|
+
this.#target = target;
|
|
1657
|
+
this.#args = [...args];
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Runs the query and reads its records.
|
|
1661
|
+
*
|
|
1662
|
+
* @returns The non-empty records git wrote, in git's own order.
|
|
1663
|
+
* @throws {@link ScaffoldError} coded `TARGET` when the query fails, cannot start, or its output
|
|
1664
|
+
* is incomplete or outside the working-tree inventory bounds, and `INVALID` when the query
|
|
1665
|
+
* already ran.
|
|
1666
|
+
*/
|
|
1667
|
+
async read() {
|
|
1668
|
+
if (this.#started) throw new ScaffoldError("INVALID", "A git query reads once.");
|
|
1669
|
+
this.#started = true;
|
|
1670
|
+
const target = this.#target;
|
|
1671
|
+
const created = attempt(() => createSession({
|
|
1672
|
+
command: {
|
|
1673
|
+
file: "git",
|
|
1674
|
+
arguments: [
|
|
1675
|
+
"-C",
|
|
1676
|
+
target,
|
|
1677
|
+
...this.#args
|
|
1678
|
+
]
|
|
1679
|
+
},
|
|
1680
|
+
workspace: ".",
|
|
1681
|
+
signal: this.#controller.signal,
|
|
1682
|
+
on: {
|
|
1683
|
+
stdout: (chunk) => this.#collect(chunk),
|
|
1684
|
+
error: () => this.#refuse(`The git query for target at ${target} failed before it completed.`)
|
|
1685
|
+
},
|
|
1686
|
+
error: () => this.#refuse(`The git query for target at ${target} failed while reading its output.`)
|
|
1687
|
+
}));
|
|
1688
|
+
if (!created.success) {
|
|
1689
|
+
if (isScaffoldError(created.error)) throw created.error;
|
|
1690
|
+
throw new ScaffoldError("TARGET", `The git query for target at ${target} could not start.`, { target });
|
|
1691
|
+
}
|
|
1692
|
+
const session = created.value;
|
|
1693
|
+
try {
|
|
1694
|
+
const exit = await session.exit;
|
|
1695
|
+
this.#pending = `${this.#pending}${this.#decoder.decode()}`;
|
|
1696
|
+
if (this.#controller.signal.aborted) throw this.#controller.signal.reason;
|
|
1697
|
+
if (exit.signal !== null) throw new ScaffoldError("TARGET", `The git query for target at ${target} ended from signal ${exit.signal}.`, {
|
|
1698
|
+
target,
|
|
1699
|
+
signal: exit.signal
|
|
1700
|
+
});
|
|
1701
|
+
if (exit.code !== 0) throw new ScaffoldError("TARGET", `The git query for target at ${target} failed with exit code ${String(exit.code)}.`, {
|
|
1702
|
+
target,
|
|
1703
|
+
code: exit.code
|
|
1704
|
+
});
|
|
1705
|
+
if (!exit.drained) throw new ScaffoldError("TARGET", `The git query for target at ${target} ended before its output drained.`, { target });
|
|
1706
|
+
if (this.#pending.length > 0) throw new ScaffoldError("TARGET", `The git query for target at ${target} returned an incomplete record.`, { target });
|
|
1707
|
+
return [...this.#records];
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
if (isScaffoldError(error)) throw error;
|
|
1710
|
+
throw new ScaffoldError("TARGET", `The git query for target at ${target} could not start.`, { target });
|
|
1711
|
+
} finally {
|
|
1712
|
+
await session.destroy();
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
#collect(bytes) {
|
|
1716
|
+
if (this.#controller.signal.aborted) return;
|
|
1717
|
+
const chunk = splitGitRecords(`${this.#pending}${this.#decoder.decode(bytes, { stream: true })}`);
|
|
1718
|
+
const refusal = findGitRefusal(this.#target, this.#records.length, chunk);
|
|
1719
|
+
if (refusal !== void 0) {
|
|
1720
|
+
this.#refuse(refusal);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
for (const record of chunk.records) this.#records.push(record);
|
|
1724
|
+
this.#pending = chunk.pending;
|
|
1725
|
+
}
|
|
1726
|
+
#refuse(message) {
|
|
1727
|
+
if (!this.#controller.signal.aborted) this.#controller.abort(new ScaffoldError("TARGET", message, { target: this.#target }));
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1668
1730
|
//#endregion
|
|
1669
1731
|
//#region src/bin/CLI.ts
|
|
1670
1732
|
/**
|
|
@@ -1904,50 +1966,50 @@ var CLI = class {
|
|
|
1904
1966
|
const previous = catalogToNames(target);
|
|
1905
1967
|
const floor = host === void 0 ? readHostFloor() : readHostManifest(host) === void 0 ? host : readHostFloor(host);
|
|
1906
1968
|
const materializer = new Materializer({ host: floor });
|
|
1907
|
-
|
|
1969
|
+
const outcome = await this.#assemble(command, target, floor, materializer, previous);
|
|
1970
|
+
if (command.json === true) this.#report(outcome);
|
|
1971
|
+
else this.#recount(outcome);
|
|
1972
|
+
return outcome.membership === void 0 || outcome.provenance.guides === "floor" ? 1 : 0;
|
|
1973
|
+
}
|
|
1974
|
+
async #assemble(command, target, floor, materializer, previous) {
|
|
1908
1975
|
try {
|
|
1909
1976
|
const fetched = await this.#fetch(target, command.all === true, floor);
|
|
1910
1977
|
const guides = fetched.mirrors.length === 0 ? void 0 : fetched.mirrors.some((mirror) => mirror.lookup !== "found") ? "floor" : "live";
|
|
1911
|
-
if (fetched.entries === void 0)
|
|
1978
|
+
if (fetched.entries === void 0) return {
|
|
1912
1979
|
...materializer.mirror(fetched.mirrors, target),
|
|
1913
1980
|
mirrors: fetched.mirrors,
|
|
1914
1981
|
provenance: { ...guides === void 0 ? {} : { guides } },
|
|
1915
1982
|
...fetched.note === void 0 ? {} : { note: fetched.note }
|
|
1916
1983
|
};
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
};
|
|
1944
|
-
}
|
|
1984
|
+
const entries = fetched.entries;
|
|
1985
|
+
const declarations = manifestToDependencies(this.#manifest(target));
|
|
1986
|
+
const writable = {
|
|
1987
|
+
runtime: dependenciesToFleet(declarations.runtime),
|
|
1988
|
+
development: dependenciesToFleet(declarations.development)
|
|
1989
|
+
};
|
|
1990
|
+
const releases = entriesToReleases([...writable.runtime, ...writable.development], fetched.entries);
|
|
1991
|
+
const pins = releasesToPins(releases, writable);
|
|
1992
|
+
const refusal = fetchToRefusal(fetched.entries, fetched.mirrors);
|
|
1993
|
+
if (refusal !== void 0) throw refusal;
|
|
1994
|
+
return {
|
|
1995
|
+
...mergeResults(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare({
|
|
1996
|
+
pins,
|
|
1997
|
+
scripts: []
|
|
1998
|
+
}, target)),
|
|
1999
|
+
membership: {
|
|
2000
|
+
entries,
|
|
2001
|
+
dropped: previous.filter((name) => !entries.some((entry) => entry.name === name)),
|
|
2002
|
+
releases
|
|
2003
|
+
},
|
|
2004
|
+
mirrors: fetched.mirrors,
|
|
2005
|
+
provenance: {
|
|
2006
|
+
versions: "live",
|
|
2007
|
+
...guides === void 0 ? {} : { guides }
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
1945
2010
|
} finally {
|
|
1946
2011
|
materializer.destroy();
|
|
1947
2012
|
}
|
|
1948
|
-
if (command.json === true) this.#report(outcome);
|
|
1949
|
-
else this.#recount(outcome);
|
|
1950
|
-
return outcome.membership === void 0 || outcome.provenance.guides === "floor" ? 1 : 0;
|
|
1951
2013
|
}
|
|
1952
2014
|
async #replace(command) {
|
|
1953
2015
|
const target = command.target ?? ".";
|
|
@@ -2047,15 +2109,18 @@ var CLI = class {
|
|
|
2047
2109
|
}
|
|
2048
2110
|
async #reconcile(materializer, target, declared, host, hostForced, blueprint) {
|
|
2049
2111
|
const previous = catalogToNames(target);
|
|
2050
|
-
let releases = [];
|
|
2051
|
-
let provenance = { ...host === void 0 ? {} : { host } };
|
|
2052
2112
|
try {
|
|
2053
2113
|
const versions = await this.#versions(declared.pins, false, blueprint);
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2114
|
+
return await this.#confirm(materializer, target, declared, {
|
|
2115
|
+
baseline: host,
|
|
2116
|
+
forced: hostForced
|
|
2117
|
+
}, versions, previous);
|
|
2118
|
+
} catch (error) {
|
|
2119
|
+
return this.#unfinished(target, [], { ...host === void 0 ? {} : { host } }, error);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
async #confirm(materializer, target, declared, host, versions, previous) {
|
|
2123
|
+
try {
|
|
2059
2124
|
const refused = versionsToRefusal(versions);
|
|
2060
2125
|
if (refused !== void 0) throw refused;
|
|
2061
2126
|
const fetched = await this.#fetch(target, false);
|
|
@@ -2064,44 +2129,53 @@ var CLI = class {
|
|
|
2064
2129
|
const incomplete = fetchToRefusal(fetched.entries, fetched.mirrors);
|
|
2065
2130
|
if (incomplete !== void 0) throw incomplete;
|
|
2066
2131
|
const guides = fetched.mirrors.length === 0 ? void 0 : fetched.mirrors.some((mirror) => mirror.lookup === "failed") ? "floor" : "live";
|
|
2067
|
-
provenance = {
|
|
2132
|
+
const provenance = {
|
|
2068
2133
|
...versions.baseline === void 0 ? {} : { versions: versions.baseline },
|
|
2069
2134
|
...guides === void 0 ? {} : { guides },
|
|
2070
|
-
...host === void 0 ? {} : { host }
|
|
2071
|
-
};
|
|
2072
|
-
const written = mergeResults(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare({
|
|
2073
|
-
...declared,
|
|
2074
|
-
pins: versions.pins
|
|
2075
|
-
}, target));
|
|
2076
|
-
const floors = [];
|
|
2077
|
-
if (hostForced) floors.push("host");
|
|
2078
|
-
if (versions.forced) floors.push("versions");
|
|
2079
|
-
if (guides === "floor") floors.push("guides");
|
|
2080
|
-
return {
|
|
2081
|
-
...written,
|
|
2082
|
-
membership: {
|
|
2083
|
-
entries,
|
|
2084
|
-
dropped: previous.filter((name) => !entries.some((entry) => entry.name === name)),
|
|
2085
|
-
releases: entriesToReleases([...dependenciesToFleet(declared.pins.runtime), ...dependenciesToFleet(declared.pins.development)], entries)
|
|
2086
|
-
},
|
|
2087
|
-
mirrors: fetched.mirrors,
|
|
2088
|
-
releases,
|
|
2089
|
-
provenance,
|
|
2090
|
-
...floors.length === 0 ? {} : { note: `The ${floors.join(", ")} step used the distributed floor after its upstream read did not complete.` }
|
|
2135
|
+
...host.baseline === void 0 ? {} : { host: host.baseline }
|
|
2091
2136
|
};
|
|
2137
|
+
const landed = attempt(() => {
|
|
2138
|
+
const written = mergeResults(this.#publish(materializer, target, entries, fetched.mirrors), materializer.declare({
|
|
2139
|
+
...declared,
|
|
2140
|
+
pins: versions.pins
|
|
2141
|
+
}, target));
|
|
2142
|
+
const floors = [];
|
|
2143
|
+
if (host.forced) floors.push("host");
|
|
2144
|
+
if (versions.forced) floors.push("versions");
|
|
2145
|
+
if (guides === "floor") floors.push("guides");
|
|
2146
|
+
return {
|
|
2147
|
+
...written,
|
|
2148
|
+
membership: {
|
|
2149
|
+
entries,
|
|
2150
|
+
dropped: previous.filter((name) => !entries.some((entry) => entry.name === name)),
|
|
2151
|
+
releases: entriesToReleases([...dependenciesToFleet(declared.pins.runtime), ...dependenciesToFleet(declared.pins.development)], entries)
|
|
2152
|
+
},
|
|
2153
|
+
mirrors: fetched.mirrors,
|
|
2154
|
+
releases: versions.releases,
|
|
2155
|
+
provenance,
|
|
2156
|
+
...floors.length === 0 ? {} : { note: `The ${floors.join(", ")} step used the distributed floor after its upstream read did not complete.` }
|
|
2157
|
+
};
|
|
2158
|
+
});
|
|
2159
|
+
return landed.success ? landed.value : this.#unfinished(target, versions.releases, provenance, landed.error);
|
|
2092
2160
|
} catch (error) {
|
|
2093
|
-
return {
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
removed: [],
|
|
2098
|
-
mirrors: [],
|
|
2099
|
-
releases,
|
|
2100
|
-
provenance,
|
|
2101
|
-
note: `The catalog step did not complete: ${errorToEnvelope(error).error.message}`
|
|
2102
|
-
};
|
|
2161
|
+
return this.#unfinished(target, versions.releases, {
|
|
2162
|
+
...versions.baseline === void 0 ? {} : { versions: versions.baseline },
|
|
2163
|
+
...host.baseline === void 0 ? {} : { host: host.baseline }
|
|
2164
|
+
}, error);
|
|
2103
2165
|
}
|
|
2104
2166
|
}
|
|
2167
|
+
#unfinished(target, releases, provenance, error) {
|
|
2168
|
+
return {
|
|
2169
|
+
target,
|
|
2170
|
+
written: [],
|
|
2171
|
+
skipped: [],
|
|
2172
|
+
removed: [],
|
|
2173
|
+
mirrors: [],
|
|
2174
|
+
releases,
|
|
2175
|
+
provenance,
|
|
2176
|
+
note: `The catalog step did not complete: ${errorToEnvelope(error).error.message}`
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2105
2179
|
async #host(from, target, offline) {
|
|
2106
2180
|
if (from !== void 0) return {
|
|
2107
2181
|
materializer: new Materializer({ host: from }),
|
|
@@ -2264,14 +2338,10 @@ var CLI = class {
|
|
|
2264
2338
|
].map((dependency) => dependency.name))];
|
|
2265
2339
|
const upstream = new Upstream(this.#upstream);
|
|
2266
2340
|
try {
|
|
2267
|
-
|
|
2268
|
-
let note;
|
|
2269
|
-
try {
|
|
2270
|
-
entries = await upstream.catalog();
|
|
2271
|
-
} catch (error) {
|
|
2341
|
+
const { entries, note } = await upstream.catalog().then((entries) => ({ entries }), (error) => {
|
|
2272
2342
|
if (floor === void 0) throw error;
|
|
2273
|
-
note
|
|
2274
|
-
}
|
|
2343
|
+
return { note: `The catalog membership read did not complete; performed a guide-only refresh: ${errorToEnvelope(error).error.message}` };
|
|
2344
|
+
});
|
|
2275
2345
|
const names = (all ? entries === void 0 ? catalogToNames(floor ?? target) : entries.map((entry) => entry.name) : declared).filter((name) => name !== own);
|
|
2276
2346
|
const mirrors = await upstream.fetch(names, readSnapshot(target, names.map(nameToGuide)));
|
|
2277
2347
|
return {
|
|
@@ -2323,12 +2393,18 @@ var CLI = class {
|
|
|
2323
2393
|
const global = resolveContainedPath(target, GLOBAL_SETUP_PATH);
|
|
2324
2394
|
const journey = resolveContainedPath(target, JOURNEY_CONFIG_PATH);
|
|
2325
2395
|
const skills = resolveContainedPath(target, SKILLS_CONFIG_PATH);
|
|
2326
|
-
const
|
|
2327
|
-
|
|
2328
|
-
if (
|
|
2396
|
+
const files = tests === void 0 ? [] : listFiles(tests);
|
|
2397
|
+
const studies = files.some((path) => {
|
|
2398
|
+
if (tests === void 0 || !path.startsWith("studies/") || !path.endsWith(".test.ts")) return false;
|
|
2399
|
+
if (path.split("/").some((segment) => segment === "node_modules" || segment === ".git")) return false;
|
|
2400
|
+
const study = resolveContainedPath(tests, path);
|
|
2401
|
+
return study !== void 0 && isExactCaseFile(study);
|
|
2402
|
+
});
|
|
2403
|
+
const setup = new Set(files.flatMap((path) => {
|
|
2404
|
+
if (tests === void 0 || path.includes("/") || !path.startsWith("setup") || !path.endsWith(".test.ts")) return [];
|
|
2329
2405
|
const proof = resolveContainedPath(tests, path);
|
|
2330
|
-
|
|
2331
|
-
}
|
|
2406
|
+
return proof !== void 0 && isExactCaseFile(proof) ? [path === "setupBrowser.test.ts" || path === "setupStyles.test.ts" ? "browser" : "node"] : [];
|
|
2407
|
+
}));
|
|
2332
2408
|
return createBlueprint(declared.slice(declared.lastIndexOf("/") + 1), {
|
|
2333
2409
|
...targetToFacts(target),
|
|
2334
2410
|
extensions: targetToExtensions(target),
|
|
@@ -2341,6 +2417,7 @@ var CLI = class {
|
|
|
2341
2417
|
integration: integration !== void 0 && isExactCaseFile(integration),
|
|
2342
2418
|
conformance: conformance !== void 0 && isExactCaseFile(conformance),
|
|
2343
2419
|
service: service !== void 0 && isExactCaseFile(service),
|
|
2420
|
+
studies,
|
|
2344
2421
|
global: global !== void 0 && isExactCaseFile(global),
|
|
2345
2422
|
journey: journey !== void 0 && isExactCaseFile(journey),
|
|
2346
2423
|
skills: skills !== void 0 && isExactCaseFile(skills)
|
|
@@ -2355,18 +2432,9 @@ var CLI = class {
|
|
|
2355
2432
|
const scripts = isRecord(parsed) && isRecord(parsed.scripts) ? parsed.scripts : {};
|
|
2356
2433
|
const gates = !isRecord(parsed) || parsed.private !== true ? ["test", "prepublishOnly"] : ["test"];
|
|
2357
2434
|
const gateNames = gates.join(" or ");
|
|
2358
|
-
const
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
if (!isString(script) || !script.includes("vitest")) continue;
|
|
2362
|
-
const invoked = scriptToInvocations(script);
|
|
2363
|
-
if (invoked === void 0) {
|
|
2364
|
-
unresolved = true;
|
|
2365
|
-
continue;
|
|
2366
|
-
}
|
|
2367
|
-
for (const project of invoked.projects) if (!planned.has(project)) absent.add(project);
|
|
2368
|
-
}
|
|
2369
|
-
const projects = [...absent].sort();
|
|
2435
|
+
const readings = Object.values(scripts).filter((script) => isString(script) && script.includes("vitest")).map((script) => scriptToInvocations(script));
|
|
2436
|
+
const unresolved = readings.some((invocations) => invocations === void 0);
|
|
2437
|
+
const projects = [...new Set(readings.flatMap((invocations) => invocations?.projects ?? []).filter((project) => !planned.has(project)))].sort();
|
|
2370
2438
|
if (unresolved) return {
|
|
2371
2439
|
field: "projects",
|
|
2372
2440
|
message: `The manifest at ${target} contains a Vitest project expression that cannot be resolved statically.${projects.length === 0 ? "" : ` It also names projects the planned vite.config.ts does not register: ${projects.join(", ")}.`} ${writing ? "The configs group is blocked. Replace the expression with a literal --project value before selecting configs, or exclude configs from --groups." : "Replace it with a literal --project value before relying on the planned configuration."}`,
|
|
@@ -2381,17 +2449,13 @@ var CLI = class {
|
|
|
2381
2449
|
};
|
|
2382
2450
|
if (!writing) {
|
|
2383
2451
|
const paths = new Set(blueprintToConfigArtifacts(blueprint).map((artifact) => resolveContainedPath(target, artifact.path)));
|
|
2384
|
-
const configurations = []
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
const invoked = scriptToInvocations(script);
|
|
2388
|
-
if (invoked === void 0) continue;
|
|
2389
|
-
for (const config of invoked.configs) {
|
|
2452
|
+
const configurations = Object.entries(scripts).flatMap(([name, script]) => {
|
|
2453
|
+
if (!name.startsWith("test:") || !isString(script) || !script.includes("vitest")) return [];
|
|
2454
|
+
return (scriptToInvocations(script)?.configs ?? []).flatMap((config) => {
|
|
2390
2455
|
const path = resolveContainedPath(target, config);
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
}
|
|
2456
|
+
return path !== void 0 && (paths.has(path) || isExactCaseFile(path)) ? [] : [`${name} --config ${config}`];
|
|
2457
|
+
});
|
|
2458
|
+
});
|
|
2395
2459
|
if (configurations.length > 0) return {
|
|
2396
2460
|
field: "projects",
|
|
2397
2461
|
message: `The manifest at ${target} names a Vitest configuration the plan does not emit and the target does not hold: ${configurations.join(", ")}. Add the configuration, or remove the script that names it and its invocation from the test chain.`,
|
|
@@ -2400,48 +2464,23 @@ var CLI = class {
|
|
|
2400
2464
|
};
|
|
2401
2465
|
}
|
|
2402
2466
|
const expected = blueprintToScripts(blueprint);
|
|
2403
|
-
const expectedLines =
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
const invoked = scriptToInvocations(script);
|
|
2407
|
-
if (invoked === void 0) continue;
|
|
2408
|
-
for (const project of invoked.projects) {
|
|
2409
|
-
if (project === "probe") continue;
|
|
2467
|
+
const expectedLines = new Map(Object.entries(expected).flatMap(([name, script]) => {
|
|
2468
|
+
if (!name.startsWith("test:")) return [];
|
|
2469
|
+
return (scriptToInvocations(script)?.projects ?? []).flatMap((project) => {
|
|
2410
2470
|
const direct = `test:${project}`;
|
|
2411
2471
|
const command = expected[direct];
|
|
2412
|
-
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2472
|
+
return project === "probe" || command === void 0 ? [] : [[project, `"${direct}": ${JSON.stringify(command)},`]];
|
|
2473
|
+
});
|
|
2474
|
+
}));
|
|
2415
2475
|
const faces = /* @__PURE__ */ new Set([...blueprintToSheets(blueprint).map((name) => `src:${name}`), ...blueprintToFaces(blueprint).flatMap(({ name, axes }) => axes.map((axis) => `${axis}:${name}`))]);
|
|
2416
|
-
const wrappers =
|
|
2417
|
-
for (const artifact of blueprintToConfigArtifacts(blueprint)) {
|
|
2476
|
+
const wrappers = new Map(blueprintToConfigArtifacts(blueprint).flatMap((artifact) => {
|
|
2418
2477
|
const match = /^configs\/(src|app)\/vite\.([^.]+)\.config\.ts$/u.exec(artifact.path);
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
const reachable =
|
|
2424
|
-
|
|
2425
|
-
for (const gate of gates) {
|
|
2426
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2427
|
-
chains.set(gate, visited);
|
|
2428
|
-
const pending = [gate];
|
|
2429
|
-
while (pending.length > 0) {
|
|
2430
|
-
const name = pending.shift();
|
|
2431
|
-
if (name === void 0 || visited.has(name)) continue;
|
|
2432
|
-
visited.add(name);
|
|
2433
|
-
const script = scripts[name];
|
|
2434
|
-
if (!isString(script)) continue;
|
|
2435
|
-
const invoked = scriptToInvocations(script, wrappers);
|
|
2436
|
-
if (invoked === void 0) {
|
|
2437
|
-
unresolved = true;
|
|
2438
|
-
continue;
|
|
2439
|
-
}
|
|
2440
|
-
for (const project of invoked.projects) reachable.add(project);
|
|
2441
|
-
for (const called of invoked.scripts) if (!visited.has(called)) pending.push(called);
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
if (unresolved) return {
|
|
2478
|
+
const project = match === null ? void 0 : `${match[1]}:${match[2]}`;
|
|
2479
|
+
return project !== void 0 && faces.has(project) ? [[artifact.path, project]] : [];
|
|
2480
|
+
}));
|
|
2481
|
+
const chains = new Map(gates.map((gate) => [gate, collectScriptChain(gate, scripts, wrappers)]));
|
|
2482
|
+
const reachable = new Set([...chains.values()].flatMap((chain) => [...chain.projects]));
|
|
2483
|
+
if (new Set([...chains.values()].flatMap((chain) => [...chain.unreadable])).size > 0) return {
|
|
2445
2484
|
field: "projects",
|
|
2446
2485
|
message: `The manifest at ${target} contains a ${gateNames} chain that cannot be resolved statically. ${writing ? "The configs group is blocked. Replace it with literal npm run script names before selecting configs, or exclude configs from --groups." : "Replace it with literal npm run script names before relying on the planned configuration."}`,
|
|
2447
2486
|
blocking: false,
|
|
@@ -2449,7 +2488,7 @@ var CLI = class {
|
|
|
2449
2488
|
};
|
|
2450
2489
|
const ungated = [...expectedLines].filter(([project]) => !reachable.has(project)).sort(([left], [right]) => left.localeCompare(right)).filter(([project]) => isString(scripts[`test:${project}`]));
|
|
2451
2490
|
if (ungated.length === 0) {
|
|
2452
|
-
if (!writing && expected["test:journey"] !== void 0 && !chains.get("test")?.has("test:journey")) return {
|
|
2491
|
+
if (!writing && expected["test:journey"] !== void 0 && !chains.get("test")?.visited.has("test:journey")) return {
|
|
2453
2492
|
field: "projects",
|
|
2454
2493
|
message: `The manifest at ${target} does not invoke npm run test:journey from its test chain. Add npm run test:journey after npm run test:app.`,
|
|
2455
2494
|
blocking: false,
|
|
@@ -2643,13 +2682,13 @@ var CLI = class {
|
|
|
2643
2682
|
return manifest;
|
|
2644
2683
|
}
|
|
2645
2684
|
async #worktree(target) {
|
|
2646
|
-
const tracked = await
|
|
2647
|
-
const dirty = (await
|
|
2685
|
+
const tracked = await new GitQuery(target, ["ls-files", "-z"]).read();
|
|
2686
|
+
const dirty = (await new GitQuery(target, [
|
|
2648
2687
|
"status",
|
|
2649
2688
|
"--porcelain=v1",
|
|
2650
2689
|
"--untracked-files=all",
|
|
2651
2690
|
"-z"
|
|
2652
|
-
])).map((record) => record.length > 3 && record[2] === " " ? record.slice(3) : record);
|
|
2691
|
+
]).read()).map((record) => record.length > 3 && record[2] === " " ? record.slice(3) : record);
|
|
2653
2692
|
const state = {
|
|
2654
2693
|
tracked,
|
|
2655
2694
|
dirty
|