@abloh/repository 1.0.1 → 1.0.2-staging.34831899691
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/index.d.ts +365 -155
- package/dist/index.js +1963 -1721
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
// src/aggregator-chain.ts
|
|
2
|
+
import { join as join9 } from "path";
|
|
3
|
+
|
|
1
4
|
// src/repo-access.ts
|
|
2
5
|
import { realpathSync as realpathSync2 } from "fs";
|
|
3
6
|
import { relative, resolve as resolve2 } from "path";
|
|
4
7
|
import {
|
|
5
8
|
normalizeRepoPath as normalizeRepoPath2,
|
|
9
|
+
overlayRepoReader,
|
|
6
10
|
readPackageManifest
|
|
7
11
|
} from "@abloh/core";
|
|
8
12
|
|
|
@@ -116,6 +120,10 @@ function diskRepoAccess(repoDir) {
|
|
|
116
120
|
}
|
|
117
121
|
};
|
|
118
122
|
}
|
|
123
|
+
function overlayRepoAccess(base, files) {
|
|
124
|
+
const reader = overlayRepoReader(base.reader, files);
|
|
125
|
+
return reader === base.reader ? base : { ...base, reader };
|
|
126
|
+
}
|
|
119
127
|
function repoPath(ctx, absolute) {
|
|
120
128
|
const target = resolve2(absolute);
|
|
121
129
|
for (const base of ctx.roots) {
|
|
@@ -147,6 +155,50 @@ function accessSubdirectories(ctx, absolute) {
|
|
|
147
155
|
return ctx.reader.list(path).filter((name) => ctx.reader.entryKind(path === "" ? name : `${path}/${name}`) === "directory");
|
|
148
156
|
}
|
|
149
157
|
|
|
158
|
+
// src/detect.ts
|
|
159
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
160
|
+
import { dirname as dirname4, isAbsolute as isAbsolute4, join as join8, relative as relative5, resolve as resolve6, sep as sep4 } from "path";
|
|
161
|
+
import { SCRIPT_NAMED_RUNNERS as SCRIPT_NAMED_RUNNERS3, angularRunnerName, commandTokens, compiledOutputEvidence, debugPreparationOverflowStack, readsCompiledOutput, detectModuleLayout, directRunnerCommand, localBinaryPrefix, manifestScripts as manifestScripts3, parseLiteralArguments, parsePreparedTestCommand, planCommand as planCommand3, nextActionSentence, readPackageManagerDeclaration, readPackageManagerPin, resolveCommandPlan, runnerNamesInScript, scriptRuns as scriptRuns2, selectAngularTestTarget, nameDrivenRunners, summaryThenAction, watchModeRefusal, withWatchModeDisabled, withoutWatchDisableFlags } from "@abloh/core";
|
|
162
|
+
|
|
163
|
+
// src/ci-recipe.ts
|
|
164
|
+
import { basename, isAbsolute, relative as relative3, resolve as resolve3 } from "path";
|
|
165
|
+
import { parse } from "yaml";
|
|
166
|
+
import {
|
|
167
|
+
declaredDependencies as declaredDependencies2,
|
|
168
|
+
manifestScripts,
|
|
169
|
+
normalizeRepoPath as normalizeRepoPath3,
|
|
170
|
+
readPackageManifest as readPackageManifest2,
|
|
171
|
+
runIsDroppable,
|
|
172
|
+
scriptRuns
|
|
173
|
+
} from "@abloh/core";
|
|
174
|
+
import {
|
|
175
|
+
admitsMainlineBranch,
|
|
176
|
+
assignServicePorts,
|
|
177
|
+
environmentValueProblem,
|
|
178
|
+
isEnvironmentName,
|
|
179
|
+
isReservedCustomerVariable,
|
|
180
|
+
MAX_ENVIRONMENT_SERVICES,
|
|
181
|
+
serviceNameProblem,
|
|
182
|
+
SCRIPT_NAMED_RUNNERS,
|
|
183
|
+
UNDRIVABLE_RUNNERS,
|
|
184
|
+
undrivableRunner,
|
|
185
|
+
matchesCredentialShape,
|
|
186
|
+
matrixValueText as matrixValueText2,
|
|
187
|
+
nothingToMeasurePathFilter,
|
|
188
|
+
planCommand,
|
|
189
|
+
preparedCommandProblem,
|
|
190
|
+
readFailFastDeclaration,
|
|
191
|
+
refusalLines,
|
|
192
|
+
messageFromFacts,
|
|
193
|
+
renderRefusal,
|
|
194
|
+
reservedComposeProblem,
|
|
195
|
+
reservedComposition,
|
|
196
|
+
MAX_SYSTEM_PACKAGES,
|
|
197
|
+
runnerImageVocabulary,
|
|
198
|
+
sameMatrixValue,
|
|
199
|
+
systemPackageProblem
|
|
200
|
+
} from "@abloh/core";
|
|
201
|
+
|
|
150
202
|
// src/browser-lane.ts
|
|
151
203
|
import { dirname as dirname2, join as join2, relative as relative2, sep as sep2 } from "path";
|
|
152
204
|
function repoPathOrEmpty(ctx, absolute) {
|
|
@@ -312,44 +364,6 @@ function readBrowserLane(repoDir, ctx = diskRepoAccess(repoDir)) {
|
|
|
312
364
|
return null;
|
|
313
365
|
}
|
|
314
366
|
|
|
315
|
-
// src/ci-recipe.ts
|
|
316
|
-
import { basename, isAbsolute, relative as relative3, resolve as resolve3 } from "path";
|
|
317
|
-
import { parse } from "yaml";
|
|
318
|
-
import {
|
|
319
|
-
declaredDependencies as declaredDependencies2,
|
|
320
|
-
manifestScripts,
|
|
321
|
-
normalizeRepoPath as normalizeRepoPath3,
|
|
322
|
-
readPackageManifest as readPackageManifest2,
|
|
323
|
-
scriptRuns
|
|
324
|
-
} from "@abloh/core";
|
|
325
|
-
import {
|
|
326
|
-
admitsMainlineBranch,
|
|
327
|
-
assignServicePorts,
|
|
328
|
-
environmentValueProblem,
|
|
329
|
-
isEnvironmentName,
|
|
330
|
-
isReservedCustomerVariable,
|
|
331
|
-
MAX_ENVIRONMENT_SERVICES,
|
|
332
|
-
serviceNameProblem,
|
|
333
|
-
SCRIPT_NAMED_RUNNERS,
|
|
334
|
-
UNDRIVABLE_RUNNERS,
|
|
335
|
-
undrivableRunner,
|
|
336
|
-
matchesCredentialShape,
|
|
337
|
-
matrixValueText as matrixValueText2,
|
|
338
|
-
nothingToMeasurePathFilter,
|
|
339
|
-
planCommand,
|
|
340
|
-
preparedCommandProblem,
|
|
341
|
-
readFailFastDeclaration,
|
|
342
|
-
refusalLines,
|
|
343
|
-
messageFromFacts,
|
|
344
|
-
renderRefusal,
|
|
345
|
-
reservedComposeProblem,
|
|
346
|
-
reservedComposition,
|
|
347
|
-
MAX_SYSTEM_PACKAGES,
|
|
348
|
-
runnerImageVocabulary,
|
|
349
|
-
sameMatrixValue,
|
|
350
|
-
systemPackageProblem
|
|
351
|
-
} from "@abloh/core";
|
|
352
|
-
|
|
353
367
|
// src/workflow-environment.ts
|
|
354
368
|
import { matrixValueText } from "@abloh/core";
|
|
355
369
|
function layerEnvironment(scopes) {
|
|
@@ -1037,7 +1051,7 @@ function readStep(step, manifest, reader) {
|
|
|
1037
1051
|
const script = workspaceFirst[2];
|
|
1038
1052
|
if (TEST_NAMES.test(script)) return { kind: "test", command };
|
|
1039
1053
|
if (BUILD_NAMES.test(script)) return { kind: "build", command, env };
|
|
1040
|
-
return { kind: "other" };
|
|
1054
|
+
return { kind: "other", command, why: "abloh could not read what it does" };
|
|
1041
1055
|
}
|
|
1042
1056
|
const scoped = /--workspace[= ]([^\s]+)|--filter[= ]([^\s]+)/u.test(tail);
|
|
1043
1057
|
if (manifest.scripts[name] !== void 0 || scoped) {
|
|
@@ -1050,7 +1064,7 @@ function readStep(step, manifest, reader) {
|
|
|
1050
1064
|
if (manifest.scripts[name] !== void 0 && scriptRunsBuild(manifest, name)) {
|
|
1051
1065
|
return { kind: "build", command, env };
|
|
1052
1066
|
}
|
|
1053
|
-
return { kind: "other" };
|
|
1067
|
+
return { kind: "other", command, why: scriptNotABuildBecause(manifest, name) };
|
|
1054
1068
|
}
|
|
1055
1069
|
if (TEST_BINARIES.has(name)) return { kind: "test", command };
|
|
1056
1070
|
if (BUILD_BINARIES.has(name)) {
|
|
@@ -1170,7 +1184,20 @@ function scriptRunsBuild(manifest, name) {
|
|
|
1170
1184
|
lifecycle: false
|
|
1171
1185
|
});
|
|
1172
1186
|
if (runs.length === 0) return false;
|
|
1173
|
-
return runs.every((run) => run.kind === "preparation" &&
|
|
1187
|
+
return runs.every((run) => run.kind === "preparation" && run.role === "build");
|
|
1188
|
+
}
|
|
1189
|
+
function scriptNotABuildBecause(manifest, name) {
|
|
1190
|
+
const runs = scriptRuns({
|
|
1191
|
+
scripts: manifest.scripts,
|
|
1192
|
+
dependencies: manifest.dependencies,
|
|
1193
|
+
script: name,
|
|
1194
|
+
lifecycle: false
|
|
1195
|
+
});
|
|
1196
|
+
if (runs.length > 0 && runs.every((run) => runIsDroppable(run) && run.kind !== "test")) {
|
|
1197
|
+
return "it checks the tree rather than writing into it";
|
|
1198
|
+
}
|
|
1199
|
+
const unknown = runs.find((run) => run.kind === "preparation" && run.role === "tool");
|
|
1200
|
+
return unknown === void 0 ? "abloh could not read what it does" : `abloh has no reading of ${unknown.name}, so it cannot say the suite needs it`;
|
|
1174
1201
|
}
|
|
1175
1202
|
function clip(command) {
|
|
1176
1203
|
return command.length > 70 ? `${command.slice(0, 70)}\u2026` : command;
|
|
@@ -1334,6 +1361,7 @@ function emptyCiRecipe(refusal) {
|
|
|
1334
1361
|
browserLane: null,
|
|
1335
1362
|
unreadableJobCalls: [],
|
|
1336
1363
|
refusal,
|
|
1364
|
+
stepsNotCopied: [],
|
|
1337
1365
|
notices: []
|
|
1338
1366
|
};
|
|
1339
1367
|
}
|
|
@@ -1359,6 +1387,7 @@ function deriveCiRecipe(input) {
|
|
|
1359
1387
|
browserLane: null,
|
|
1360
1388
|
unreadableJobCalls: [],
|
|
1361
1389
|
refusal: `your CI configuration could not be read (${error instanceof Error ? error.message : "unknown error"})`,
|
|
1390
|
+
stepsNotCopied: [],
|
|
1362
1391
|
notices: []
|
|
1363
1392
|
};
|
|
1364
1393
|
}
|
|
@@ -1385,6 +1414,7 @@ function derive(input) {
|
|
|
1385
1414
|
runnerImage: null,
|
|
1386
1415
|
browserLane: null,
|
|
1387
1416
|
unreadableJobCalls: calls,
|
|
1417
|
+
stepsNotCopied: [],
|
|
1388
1418
|
refusal,
|
|
1389
1419
|
notices: [...bounds, ...notices]
|
|
1390
1420
|
});
|
|
@@ -1598,20 +1628,21 @@ function assemble(input) {
|
|
|
1598
1628
|
for (const refused of chosen.refusals) {
|
|
1599
1629
|
if (!unread.some((seen2) => seen2.step === refused.step && seen2.why === refused.why)) unread.push(refused);
|
|
1600
1630
|
}
|
|
1601
|
-
const
|
|
1602
|
-
kind: "setup-ci-steps",
|
|
1603
|
-
unread,
|
|
1631
|
+
const stepsRead = {
|
|
1604
1632
|
workflow: chosen.workflowPath,
|
|
1605
1633
|
job: chosen.jobId,
|
|
1606
1634
|
/* THE MAINTAINER'S OWN FILE SAID IT, and `at` is the receipt spelling every other notice in
|
|
1607
1635
|
this function uses, so a reader meets one name for the job on every line. */
|
|
1608
1636
|
at: `${chosen.file}::${chosen.jobId}`
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1637
|
+
};
|
|
1638
|
+
const sayStepsRead = (facts2) => {
|
|
1639
|
+
const outcome = messageFromFacts({ kind: "setup-ci-steps", ...stepsRead, ...facts2 });
|
|
1640
|
+
if (outcome.kind !== "message") return;
|
|
1641
|
+
const rendered = renderRefusal(outcome.refusal, "terminal");
|
|
1613
1642
|
if (rendered !== null) notices.push(refusalLines(rendered).join("\n"));
|
|
1614
|
-
}
|
|
1643
|
+
};
|
|
1644
|
+
sayStepsRead({ unread, notCopied: [] });
|
|
1645
|
+
sayStepsRead({ unread: [], notCopied: [...chosen.notCopied] });
|
|
1615
1646
|
const drafted = [];
|
|
1616
1647
|
const seen = /* @__PURE__ */ new Set();
|
|
1617
1648
|
const push = (step, source) => {
|
|
@@ -1838,6 +1869,11 @@ function assemble(input) {
|
|
|
1838
1869
|
runnerImage,
|
|
1839
1870
|
browserLane,
|
|
1840
1871
|
unreadableJobCalls: input.unreadableJobCalls,
|
|
1872
|
+
/* THE STEPS THE CHOSEN JOB RUNS THAT THE RECIPE LEFT OUT. See {@link CiRecipe.stepsNotCopied}:
|
|
1873
|
+
this is the chosen candidate's own list and never a pooling of every job's, because the
|
|
1874
|
+
recipe is derived from one job and a sentence about another job's steps would name edits to a
|
|
1875
|
+
file abloh is not reading. */
|
|
1876
|
+
stepsNotCopied: chosen.notCopied,
|
|
1841
1877
|
refusal: null,
|
|
1842
1878
|
notices
|
|
1843
1879
|
};
|
|
@@ -2052,6 +2088,7 @@ function blankCandidate(file, jobId, label) {
|
|
|
2052
2088
|
testSteps: [],
|
|
2053
2089
|
installs: false,
|
|
2054
2090
|
refusals: [],
|
|
2091
|
+
notCopied: [],
|
|
2055
2092
|
refusedTestSteps: [],
|
|
2056
2093
|
/* A `.travis.yml` or a CircleCI job declares no `env:` block and no `services:` this module has
|
|
2057
2094
|
a preset for, so the three keys those produce are empty here by construction. */
|
|
@@ -2489,6 +2526,7 @@ function readJob(reader, manifest, file, jobId, job, workflowEnv, bounds = []) {
|
|
|
2489
2526
|
testSteps: [],
|
|
2490
2527
|
installs: false,
|
|
2491
2528
|
refusals: [],
|
|
2529
|
+
notCopied: [],
|
|
2492
2530
|
refusedTestSteps: [],
|
|
2493
2531
|
node: null,
|
|
2494
2532
|
nodeEnv: NO_NODE_ENVIRONMENT,
|
|
@@ -2595,6 +2633,15 @@ function readJob(reader, manifest, file, jobId, job, workflowEnv, bounds = []) {
|
|
|
2595
2633
|
candidate.refusedTestSteps.push(classified.why);
|
|
2596
2634
|
}
|
|
2597
2635
|
break;
|
|
2636
|
+
case "other":
|
|
2637
|
+
if (!candidate.notCopied.some((seen) => seen.command === classified.command)) {
|
|
2638
|
+
candidate.notCopied.push({
|
|
2639
|
+
step: stepNumber(step),
|
|
2640
|
+
command: classified.command,
|
|
2641
|
+
why: classified.why
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
break;
|
|
2598
2645
|
default:
|
|
2599
2646
|
break;
|
|
2600
2647
|
}
|
|
@@ -2626,1108 +2673,277 @@ function ciRefusalIsActionable(refusal) {
|
|
|
2626
2673
|
}
|
|
2627
2674
|
var __testing = { classifyRepoDefinedBuild, readAptStep, readCiNodeDeclaration };
|
|
2628
2675
|
|
|
2629
|
-
// src/
|
|
2630
|
-
import {
|
|
2631
|
-
import {
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
* throw sites. This field is for the refusals where the class covers SEVERAL keys, which today is
|
|
2663
|
-
* the answers document: `invalid-answers-file` is about whichever key the document got wrong, and
|
|
2664
|
-
* a fix depends on knowing which.
|
|
2665
|
-
*/
|
|
2666
|
-
questionKey;
|
|
2667
|
-
/**
|
|
2668
|
-
* THIS REFUSAL'S OWN REMEDY, where the class table's is not true of this instance.
|
|
2669
|
-
*
|
|
2670
|
-
* Null on almost every throw, and that is still right: a per-class remedy is what stops forty
|
|
2671
|
-
* throw sites carrying forty drifting copies of the same sentence. But a CLASS is not always one
|
|
2672
|
-
* situation, and `repository-too-large` is where that broke (external refusal review, rank 15).
|
|
2673
|
-
* Its four throw sites all tell the human to pass `--environment-image`, because the scan they
|
|
2674
|
-
* abandon is the Dockerfile scan and an image is what makes it unnecessary. The class table told
|
|
2675
|
-
* the MACHINE to pass `--subdir`, which narrows nothing: the scan walks the repository root
|
|
2676
|
-
* whatever `--subdir` says. A human and an agent reading the same refusal were sent to different
|
|
2677
|
-
* options, and only one of them could work.
|
|
2678
|
-
*
|
|
2679
|
-
* So a throw site that knows better than its class says so here, and `init-refusal-shape.ts`
|
|
2680
|
-
* prefers it. One refusal instance, one remedy, whoever is reading.
|
|
2681
|
-
*/
|
|
2682
|
-
remedy;
|
|
2683
|
-
constructor(failureClass, message, questionKey = null, remedy = null) {
|
|
2684
|
-
super(message);
|
|
2685
|
-
this.name = "InitRefusal";
|
|
2686
|
-
this.failureClass = failureClass;
|
|
2687
|
-
this.questionKey = questionKey;
|
|
2688
|
-
this.remedy = remedy;
|
|
2676
|
+
// src/deno-project.ts
|
|
2677
|
+
import { lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
|
|
2678
|
+
import { join as join4 } from "path";
|
|
2679
|
+
import { parseJsonc } from "@abloh/core";
|
|
2680
|
+
var DENO_CONFIG_FILES = ["deno.json", "deno.jsonc"];
|
|
2681
|
+
var DENO_LOCKFILE = "deno.lock";
|
|
2682
|
+
function stageInvokesDenoTest(stage) {
|
|
2683
|
+
const tokens = stage.trim().split(/\s+/u).filter((token) => token !== "");
|
|
2684
|
+
const first = (tokens[0] ?? "").replace(/^.*\//u, "");
|
|
2685
|
+
return first === "deno" && /^deno\s+test(?:\s|$)/u.test(stage.trim());
|
|
2686
|
+
}
|
|
2687
|
+
function denoTestInvocation(taskBody) {
|
|
2688
|
+
if (taskBody === null) return null;
|
|
2689
|
+
const script = taskBody.trim();
|
|
2690
|
+
if (script === "") return null;
|
|
2691
|
+
if (/[;|<>`$()\n]/u.test(script)) return null;
|
|
2692
|
+
if (/(^|[^&])&([^&]|$)/u.test(script)) return null;
|
|
2693
|
+
const stages = script.split("&&").map((stage) => stage.trim());
|
|
2694
|
+
if (stages.some((stage) => stage === "")) return null;
|
|
2695
|
+
const matches = stages.filter(stageInvokesDenoTest);
|
|
2696
|
+
return matches.length === 1 ? matches[0] : null;
|
|
2697
|
+
}
|
|
2698
|
+
function readDenoProject(dir, readText = diskReadText) {
|
|
2699
|
+
for (const name of DENO_CONFIG_FILES) {
|
|
2700
|
+
const path = join4(dir, name);
|
|
2701
|
+
const text = readText(path);
|
|
2702
|
+
if (text === null) continue;
|
|
2703
|
+
const parsed = parseJsonc(text);
|
|
2704
|
+
const tasks = parsed?.tasks;
|
|
2705
|
+
const declared = tasks !== null && typeof tasks === "object" ? tasks["test"] : void 0;
|
|
2706
|
+
const body = typeof declared === "string" ? declared : declared !== null && typeof declared === "object" && typeof declared.command === "string" ? declared.command : null;
|
|
2707
|
+
if (body === null || body.trim() === "") return null;
|
|
2708
|
+
return { configFile: name, testTask: body, testInvocation: denoTestInvocation(body) };
|
|
2689
2709
|
}
|
|
2690
|
-
|
|
2691
|
-
function initRefusal(failureClass, message, questionKey = null, remedy = null) {
|
|
2692
|
-
return new InitRefusal(failureClass, message, questionKey, remedy);
|
|
2710
|
+
return null;
|
|
2693
2711
|
}
|
|
2694
|
-
function
|
|
2695
|
-
return
|
|
2712
|
+
function denoLockfilePath(dir, readText = diskReadText) {
|
|
2713
|
+
return readText(join4(dir, DENO_LOCKFILE)) === null ? null : DENO_LOCKFILE;
|
|
2696
2714
|
}
|
|
2697
|
-
function
|
|
2698
|
-
|
|
2715
|
+
function scriptRunsDenoTest(testScript) {
|
|
2716
|
+
if (testScript === null) return false;
|
|
2717
|
+
return testScript.split(/&&|\|\||[;&|]/u).map((stage) => stage.trim()).some(stageInvokesDenoTest);
|
|
2718
|
+
}
|
|
2719
|
+
function diskReadText(absolutePath) {
|
|
2720
|
+
try {
|
|
2721
|
+
const info = lstatSync2(absolutePath);
|
|
2722
|
+
if (!info.isFile() || info.isSymbolicLink()) return null;
|
|
2723
|
+
return readFileSync2(absolutePath, "utf8");
|
|
2724
|
+
} catch {
|
|
2725
|
+
return null;
|
|
2726
|
+
}
|
|
2699
2727
|
}
|
|
2700
2728
|
|
|
2701
|
-
// src/
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2729
|
+
// src/rush-workspace.ts
|
|
2730
|
+
import { parseJsonc as parseJsonc2 } from "@abloh/core";
|
|
2731
|
+
var RUSH_CONFIG_FILE = "rush.json";
|
|
2732
|
+
var RUSH_TEST_SCRIPTS = ["test", "_phase:test"];
|
|
2733
|
+
var RUSH_CONFIG_DIRECTORY = "common/config/rush";
|
|
2734
|
+
var RUSH_SUBSPACE_DIRECTORY = "common/config/subspaces";
|
|
2735
|
+
var RUSH_LOCKFILE_NAME = "pnpm-lock.yaml";
|
|
2736
|
+
var RUSH_REPO_STATE_NAME = "repo-state.json";
|
|
2737
|
+
var RUSH_INSTALL_COMMAND = "node common/scripts/install-run-rush.js install";
|
|
2738
|
+
var RUSH_INSTALL_SCRIPT = "common/scripts/install-run-rush.js";
|
|
2739
|
+
function classifyRushTestScript(manifest, directory) {
|
|
2740
|
+
const table = manifestScripts2(manifest);
|
|
2741
|
+
for (const script of RUSH_TEST_SCRIPTS) {
|
|
2742
|
+
const body = table[script];
|
|
2743
|
+
if (typeof body !== "string" || body.trim() === "") continue;
|
|
2744
|
+
if (/[;&|<>`\n]|\$\(/u.test(body)) {
|
|
2745
|
+
return {
|
|
2746
|
+
state: "unmeasurable",
|
|
2747
|
+
reason: `the Rush project at ${directory} composes shell commands in its ${script} script rather than running one suite`,
|
|
2748
|
+
remedy: `reduce ${directory}'s ${script} script to one command, or declare a plain "test" script that runs one suite`
|
|
2749
|
+
};
|
|
2711
2750
|
}
|
|
2712
|
-
|
|
2751
|
+
return { state: "command", script, command: body.trim() };
|
|
2713
2752
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
2753
|
+
return {
|
|
2754
|
+
state: "unmeasurable",
|
|
2755
|
+
reason: `the Rush project at ${directory} declares neither a "test" nor a "_phase:test" script`,
|
|
2756
|
+
remedy: `give ${directory} a "test" or "_phase:test" script in its package.json that runs its tests`
|
|
2757
|
+
};
|
|
2716
2758
|
}
|
|
2717
|
-
function
|
|
2718
|
-
const
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
` browser: ${yamlScalar(input.browser.value, quote)}`
|
|
2749
|
-
],
|
|
2750
|
-
` installDirectory: ${yamlScalar(input.installDirectory, quote)}`,
|
|
2751
|
-
...receiptLinesQ([
|
|
2752
|
-
/* THE POINTER, WHERE FOUR DERIVED KEYS USED TO BE (Kenneth, 2026-08-26). `systemPackages`,
|
|
2753
|
-
`installCommand` and `setupCommands` are the commands that turn a checkout into a tree the
|
|
2754
|
-
suite can run in, and they are now steps in a file the customer owns. What is left in this
|
|
2755
|
-
block is everything that is NOT a command: the image, the directory, the test commands, the
|
|
2756
|
-
services, the variables, the files. The receipt names the script rather than a source file
|
|
2757
|
-
because the script carries a receipt of its own on every step. */
|
|
2758
|
-
{
|
|
2759
|
-
key: "setup",
|
|
2760
|
-
value: input.setupPath ?? SETUP_SCRIPT_PATH,
|
|
2761
|
-
receipt: "written by init and yours to edit. Every cold run executes it step by step"
|
|
2762
|
-
},
|
|
2763
|
-
/* THE ONE THING THE SCRIPT CANNOT SAY, so the file still does: WHY there is no install step
|
|
2764
|
-
in it. A repository with no frozen recipe gets a script with nothing to install, and a
|
|
2765
|
-
reader who only ever opens `abloh.yml` would otherwise see a pointer at a file that quietly
|
|
2766
|
-
does less than they think. The remedy is the same sentence the console prints. */
|
|
2767
|
-
...input.installCommand === null ? [{ key: "installCommand", value: null, receipt: input.evidence.installCommand }] : [],
|
|
2768
|
-
/* THE ONE RECEIPT THAT GAINS A CLAUSE IN THE FILE TOO, and only on the repositories it is
|
|
2769
|
-
true of. `genericAdapterCost` carries what changed and why the prompt alone was not it. */
|
|
2770
|
-
{
|
|
2771
|
-
key: "testCommand",
|
|
2772
|
-
value: input.testCommand,
|
|
2773
|
-
receipt: testCommandReceipt({
|
|
2774
|
-
evidence: input.evidence.testCommand,
|
|
2775
|
-
cost: input.testCommandCostNote
|
|
2776
|
-
})
|
|
2777
|
-
},
|
|
2778
|
-
/* WRITTEN ONLY WHERE IT SAYS SOMETHING, and its receipt is the whole reason it says anything.
|
|
2779
|
-
See `ConfigInput.runner`: on an uncontested repository the key would restate what
|
|
2780
|
-
`bindPreparedTestCommand` reads off the command one line above, so there is no line and the
|
|
2781
|
-
PROMPT is where the detected runner is shown. Where a candidate was discarded, the line
|
|
2782
|
-
records the decision AND names what was skipped, in the same words the prompt used - which
|
|
2783
|
-
is what makes an unattended init as informed as an interactive one. */
|
|
2784
|
-
...input.runner === null ? [] : [{ key: "runner", value: input.runner, receipt: input.evidence.runner }],
|
|
2785
|
-
/* THE ANSWER TO THE ONE QUESTION A VITEST REPOSITORY WITH NO COVERAGE PROVIDER IS ASKED.
|
|
2786
|
-
WRITTEN ONLY WHERE SOMEBODY ANSWERED IT, on the same restraint every derived key here
|
|
2787
|
-
follows: absent already means `install`, so a line saying `install` on a repository nobody
|
|
2788
|
-
asked would be a claim about a conversation that never happened. Both answers are written
|
|
2789
|
-
where there WAS one, because the file is where the run reads it. */
|
|
2790
|
-
...input.coverageProvider === void 0 ? [] : [{
|
|
2791
|
-
key: "coverageProvider",
|
|
2792
|
-
value: input.coverageProvider,
|
|
2793
|
-
receipt: input.coverageProvider === "install" ? "you agreed abloh may stage @vitest/coverage-v8 in the job that measures, into its own cache outside this repository" : "you asked abloh not to install @vitest/coverage-v8, so the changed-line check reports itself as declined"
|
|
2794
|
-
}],
|
|
2795
|
-
...input.sealedTestCommand !== null || input.sealedNote !== null ? [{
|
|
2796
|
-
key: "sealedTestCommand",
|
|
2797
|
-
value: input.sealedTestCommand,
|
|
2798
|
-
receipt: input.sealedTestCommand === null ? input.sealedNote ?? "" : input.evidence.sealedTestCommand
|
|
2799
|
-
}] : [],
|
|
2800
|
-
/* THE ONE KEY WHOSE ABSENCE PRODUCES A WRONG NUMBER RATHER THAN A REFUSAL, so the file states
|
|
2801
|
-
the finding even when it cannot state a value: a reviewer reading a commented-out
|
|
2802
|
-
`rebuildCommand` beside "these two files say your tests read your build output" has the
|
|
2803
|
-
whole question in front of them, and a file that stayed silent would leave a run reporting
|
|
2804
|
-
every mutant as a survivor with nothing anywhere to explain it. */
|
|
2805
|
-
...input.rebuild === null ? [] : [{
|
|
2806
|
-
key: "rebuildCommand",
|
|
2807
|
-
value: input.rebuild.value,
|
|
2808
|
-
/* THE KEY IS NAMED IN THE SENTENCE, because a null value renders as a bare comment with
|
|
2809
|
-
no key beside it - and a reader who agrees with the finding then has nothing to type.
|
|
2810
|
-
It is the same rule `installRemedy` follows one key up. */
|
|
2811
|
-
receipt: input.rebuild.value !== null ? input.rebuild.evidence : `${input.rebuild.evidence}. Set rebuildCommand to the build that writes it - this package declares ${input.rebuild.candidates.join(" and ")}. Without one, every planted bug is reported as missed`
|
|
2812
|
-
}]
|
|
2813
|
-
], " ")
|
|
2814
|
-
);
|
|
2815
|
-
if (input.services.length > 0) {
|
|
2816
|
-
lines.push(
|
|
2817
|
-
" services: # fresh instances per proof phase, reachable on localhost exactly as in your CI"
|
|
2818
|
-
);
|
|
2819
|
-
for (const service of input.services) {
|
|
2820
|
-
lines.push(
|
|
2821
|
-
...commentLines(service.evidence, " "),
|
|
2822
|
-
` - name: ${yamlScalar(service.name, quote)}`,
|
|
2823
|
-
` image: ${yamlScalar(service.image, quote)}`
|
|
2824
|
-
);
|
|
2825
|
-
if (service.env.length > 0) {
|
|
2826
|
-
lines.push(" env: # your workflow's own values; the suite connects with these");
|
|
2827
|
-
for (const entry of service.env) lines.push(` ${entry.name}: ${yamlScalar(entry.value, quote)}`);
|
|
2828
|
-
}
|
|
2829
|
-
if (service.healthCommand !== null) {
|
|
2830
|
-
lines.push(` healthCommand: ${yamlScalar(service.healthCommand, quote)}`);
|
|
2831
|
-
}
|
|
2832
|
-
if (service.healthPort !== null) lines.push(` healthPort: ${service.healthPort}`);
|
|
2833
|
-
if (service.localhostPort != null && service.localhostPort !== service.healthPort) {
|
|
2834
|
-
lines.push(
|
|
2835
|
-
` localhostPort: ${service.localhostPort} # another declared service listens on this one's port, so a run reaches it here`
|
|
2836
|
-
);
|
|
2837
|
-
}
|
|
2759
|
+
function manifestScripts2(manifest) {
|
|
2760
|
+
const scripts = manifest?.scripts;
|
|
2761
|
+
return scripts !== null && typeof scripts === "object" ? scripts : {};
|
|
2762
|
+
}
|
|
2763
|
+
function enumerateRushProjects(readFile, limit) {
|
|
2764
|
+
const parsed = parseJsonc2(readFile(RUSH_CONFIG_FILE));
|
|
2765
|
+
const declared = parsed?.projects;
|
|
2766
|
+
if (!Array.isArray(declared)) return [];
|
|
2767
|
+
const projects = [];
|
|
2768
|
+
for (const entry of declared) {
|
|
2769
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
2770
|
+
const name = entry.packageName;
|
|
2771
|
+
const folder = entry.projectFolder;
|
|
2772
|
+
if (typeof name !== "string" || typeof folder !== "string") continue;
|
|
2773
|
+
const directory = normalizeProjectFolder(folder);
|
|
2774
|
+
if (directory === null) continue;
|
|
2775
|
+
const manifestPath = `${directory}/package.json`;
|
|
2776
|
+
const manifestText = readFile(manifestPath);
|
|
2777
|
+
if (manifestText === null) {
|
|
2778
|
+
projects.push({
|
|
2779
|
+
directory,
|
|
2780
|
+
name,
|
|
2781
|
+
test: {
|
|
2782
|
+
state: "unmeasurable",
|
|
2783
|
+
// Enumerated even so: the directory must not be silently absorbed into a parent's file
|
|
2784
|
+
// list, which would attribute this project's changed files to another project.
|
|
2785
|
+
reason: `the Rush project ${name} declares the folder ${directory}, which has no readable package.json`,
|
|
2786
|
+
remedy: `add ${manifestPath}, or remove ${name} from rush.json's projects`
|
|
2787
|
+
}
|
|
2788
|
+
});
|
|
2789
|
+
continue;
|
|
2838
2790
|
}
|
|
2791
|
+
const manifest = parseJsonc2(manifestText);
|
|
2792
|
+
projects.push({
|
|
2793
|
+
directory,
|
|
2794
|
+
name,
|
|
2795
|
+
test: manifest === null ? {
|
|
2796
|
+
state: "unmeasurable",
|
|
2797
|
+
reason: `the Rush project ${name}'s manifest at ${manifestPath} is not readable JSON`,
|
|
2798
|
+
remedy: `fix ${manifestPath} so its manifest parses`
|
|
2799
|
+
} : classifyRushTestScript(manifest, directory)
|
|
2800
|
+
});
|
|
2801
|
+
if (projects.length >= limit) break;
|
|
2839
2802
|
}
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
if (
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2803
|
+
return projects.sort((a, b) => a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0);
|
|
2804
|
+
}
|
|
2805
|
+
function normalizeProjectFolder(raw) {
|
|
2806
|
+
const path = raw.trim().replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
|
|
2807
|
+
if (path === "" || path.startsWith("/") || /^[A-Za-z]:/u.test(path)) return null;
|
|
2808
|
+
if (/[;&|<>`\s'"$]|\$\(/u.test(path)) return null;
|
|
2809
|
+
if (path.split("/").some((segment) => segment === "..")) return null;
|
|
2810
|
+
return path;
|
|
2811
|
+
}
|
|
2812
|
+
function rushIdentityFiles(input) {
|
|
2813
|
+
const files = [RUSH_CONFIG_FILE];
|
|
2814
|
+
const collect = (directory) => {
|
|
2815
|
+
for (const name of [RUSH_LOCKFILE_NAME, RUSH_REPO_STATE_NAME]) {
|
|
2816
|
+
const path = `${directory}/${name}`;
|
|
2817
|
+
if (input.exists(path)) files.push(path);
|
|
2848
2818
|
}
|
|
2819
|
+
};
|
|
2820
|
+
collect(RUSH_CONFIG_DIRECTORY);
|
|
2821
|
+
for (const subspace of [...input.subdirectories(RUSH_SUBSPACE_DIRECTORY)].sort()) {
|
|
2822
|
+
collect(`${RUSH_SUBSPACE_DIRECTORY}/${subspace}`);
|
|
2849
2823
|
}
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2824
|
+
return files;
|
|
2825
|
+
}
|
|
2826
|
+
function rushLockfiles(input) {
|
|
2827
|
+
return rushIdentityFiles(input).filter((path) => path.endsWith(`/${RUSH_LOCKFILE_NAME}`));
|
|
2828
|
+
}
|
|
2829
|
+
function rushNodeVersionRange(rushJsonText) {
|
|
2830
|
+
const parsed = parseJsonc2(rushJsonText);
|
|
2831
|
+
const range = parsed?.nodeSupportedVersionRange;
|
|
2832
|
+
return typeof range === "string" && range.trim() !== "" ? range.trim() : null;
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// src/workspace-aggregator.ts
|
|
2836
|
+
import { SCRIPT_NAMED_RUNNERS as SCRIPT_NAMED_RUNNERS2, binaryName, planCommand as planCommand2 } from "@abloh/core";
|
|
2837
|
+
function invokedProgram(command) {
|
|
2838
|
+
const plan = planCommand2(command);
|
|
2839
|
+
if (plan.kind === "program") return { program: plan.program, args: plan.args };
|
|
2840
|
+
if (plan.kind === "manager-passthrough" && plan.token !== null) {
|
|
2841
|
+
return { program: binaryName(plan.token), args: plan.args };
|
|
2862
2842
|
}
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2843
|
+
return null;
|
|
2844
|
+
}
|
|
2845
|
+
var TOOLS = ["turbo", "lage", "nx", "lerna"];
|
|
2846
|
+
function nxTargets(args) {
|
|
2847
|
+
const targets = [];
|
|
2848
|
+
for (let at = 0; at < args.length; at += 1) {
|
|
2849
|
+
const arg = args[at];
|
|
2850
|
+
const inline = /^(?:-t|--targets?)=(.+)$/u.exec(arg);
|
|
2851
|
+
if (inline !== null) {
|
|
2852
|
+
targets.push(...inline[1].split(","));
|
|
2853
|
+
continue;
|
|
2867
2854
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2855
|
+
if (/^(?:-t|--targets?)$/u.test(arg)) {
|
|
2856
|
+
const value = args[at + 1];
|
|
2857
|
+
if (value !== void 0 && !value.startsWith("-")) {
|
|
2858
|
+
targets.push(...value.split(","));
|
|
2859
|
+
at += 1;
|
|
2860
|
+
}
|
|
2873
2861
|
}
|
|
2874
2862
|
}
|
|
2875
|
-
return
|
|
2863
|
+
return targets.filter((task) => task !== "");
|
|
2876
2864
|
}
|
|
2877
|
-
function
|
|
2878
|
-
|
|
2879
|
-
const
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
...renderEnvironmentBlock({ ...input, coverageNote: null })
|
|
2897
|
-
];
|
|
2898
|
-
return `${lines.join("\n")}
|
|
2899
|
-
`;
|
|
2900
|
-
}
|
|
2901
|
-
function renderWorkflow(input) {
|
|
2902
|
-
return readableWorkflow(workflowYaml({
|
|
2903
|
-
node: input.workflowNode,
|
|
2904
|
-
/* THE ONE RECIPE, NAMED. `environment.setup` when the repository declares its own path, so a
|
|
2905
|
-
re-run of `init` over a customer's chosen path writes a workflow that calls that path. */
|
|
2906
|
-
setupScriptPath: input.setupPath ?? SETUP_SCRIPT_PATH,
|
|
2907
|
-
/* The same directory the sealed image runs those steps in - `installWorkdir` in the runner -
|
|
2908
|
-
because a recipe that installs in one directory on the runner and another in the proof image
|
|
2909
|
-
is two recipes again. */
|
|
2910
|
-
setupWorkingDirectory: input.installDirectory,
|
|
2911
|
-
runtimes: input.runtimes.map((need) => ({
|
|
2912
|
-
name: need.runtime,
|
|
2913
|
-
version: need.runtime === "bun" ? SEALED_BUN_VERSION : SEALED_DENO_VERSION,
|
|
2914
|
-
evidence: need.evidence
|
|
2915
|
-
}))
|
|
2916
|
-
}));
|
|
2917
|
-
}
|
|
2918
|
-
function readableWorkflow(text) {
|
|
2919
|
-
try {
|
|
2920
|
-
parse2(text);
|
|
2921
|
-
} catch (error) {
|
|
2922
|
-
throw new DeclaredRefusalError(refusalFromFacts({
|
|
2923
|
-
kind: "setup-step-written",
|
|
2924
|
-
workflow: WORKFLOW_FILE_PATH,
|
|
2925
|
-
job: WORKFLOW_JOB_ID,
|
|
2926
|
-
why: `it does not parse as YAML: ${error instanceof Error ? error.message : "unknown error"}`
|
|
2927
|
-
}));
|
|
2928
|
-
}
|
|
2929
|
-
return text;
|
|
2930
|
-
}
|
|
2931
|
-
function testCommandReceipt(input) {
|
|
2932
|
-
return input.cost == null ? input.evidence : `${input.evidence}. ${input.cost}`;
|
|
2933
|
-
}
|
|
2934
|
-
function receiptLines(entries, indent, quote) {
|
|
2935
|
-
const bodyOf = (entry) => entry.value === null ? null : `${indent}${entry.key}: ${yamlScalar(entry.value, quote)}`;
|
|
2936
|
-
const itemBody = (item) => `${indent} - ${yamlScalar(item.value, quote)}`;
|
|
2937
|
-
const withReceipt = (body, receipt) => `${body} # ${receipt}`;
|
|
2938
|
-
const lines = [];
|
|
2939
|
-
for (const entry of entries) {
|
|
2940
|
-
if (entry.items !== void 0) {
|
|
2941
|
-
if (entry.items.length === 0) continue;
|
|
2942
|
-
lines.push(`${indent}${entry.key}:${entry.receipt === "" ? "" : ` # ${entry.receipt}`}`);
|
|
2943
|
-
for (const item of entry.items) lines.push(withReceipt(itemBody(item), item.receipt));
|
|
2944
|
-
continue;
|
|
2945
|
-
}
|
|
2946
|
-
const body = bodyOf(entry);
|
|
2947
|
-
if (body === null) {
|
|
2948
|
-
if (entry.receipt !== "") lines.push(`${indent}# ${entry.key}: ${entry.receipt}`);
|
|
2949
|
-
continue;
|
|
2865
|
+
function aggregatorInvocation(testScript) {
|
|
2866
|
+
if (testScript === null) return null;
|
|
2867
|
+
const script = testScript.trim();
|
|
2868
|
+
if (script === "" || /[;&|<>`\n]|\$\(/u.test(script)) return null;
|
|
2869
|
+
const invoked = invokedProgram(script);
|
|
2870
|
+
if (invoked === null) return null;
|
|
2871
|
+
const tool = TOOLS.find((candidate) => candidate === invoked.program);
|
|
2872
|
+
if (tool === void 0) return null;
|
|
2873
|
+
const args = invoked.args;
|
|
2874
|
+
if (tool === "nx") {
|
|
2875
|
+
const explicit = nxTargets(args);
|
|
2876
|
+
if (explicit.length > 0) return { tool, tasks: explicit };
|
|
2877
|
+
const positional = args.filter((arg) => !arg.startsWith("-"));
|
|
2878
|
+
const head = positional[0];
|
|
2879
|
+
if (head === void 0) return { tool, tasks: [] };
|
|
2880
|
+
if (head === "run-many" || head === "affected") return { tool, tasks: [] };
|
|
2881
|
+
if (head === "run") {
|
|
2882
|
+
const target = positional[1]?.split(":")[1];
|
|
2883
|
+
return { tool, tasks: target === void 0 ? [] : [target] };
|
|
2950
2884
|
}
|
|
2951
|
-
|
|
2885
|
+
return { tool, tasks: [head] };
|
|
2952
2886
|
}
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
*
|
|
2963
|
-
* A SECOND SPELLING RATHER THAN A REWORD, on the rule above: `install`'s exact text is what a
|
|
2964
|
-
* re-run recognizes as its own step, so changing it would orphan every install step already
|
|
2965
|
-
* committed. Both are install steps to {@link initStepRole} and to {@link isInstallStep}, which is
|
|
2966
|
-
* what every reader asks instead of comparing against one of them.
|
|
2967
|
-
*
|
|
2968
|
-
* IT EXISTS BECAUSE THE OTHER ONE WOULD BE FALSE. `fastify/fastify` and `pinojs/pino` both
|
|
2969
|
-
* `.gitignore` their lockfile, so the drafted step resolves rather than installing frozen, and a
|
|
2970
|
-
* comment saying "from your lockfile" above a command that has none is a claim the file cannot
|
|
2971
|
-
* back. The `source` beside it names the ignore rule.
|
|
2972
|
-
*/
|
|
2973
|
-
installUnlocked: "dependencies, resolved - your repository commits no lockfile",
|
|
2974
|
-
build: "your build"
|
|
2975
|
-
};
|
|
2976
|
-
function isInstallStep(what) {
|
|
2977
|
-
return what === STEP_WHAT.install || what === STEP_WHAT.installUnlocked;
|
|
2978
|
-
}
|
|
2979
|
-
function setupStepOrigin(step) {
|
|
2980
|
-
return initStepRole(step.what) === null ? CUSTOMER_TYPED : "abloh-derived";
|
|
2887
|
+
if (tool === "lerna") {
|
|
2888
|
+
const positional = args.filter((arg) => !arg.startsWith("-"));
|
|
2889
|
+
if (positional[0] !== "run") return null;
|
|
2890
|
+
const leading2 = positional.slice(1, 2);
|
|
2891
|
+
return { tool, tasks: leading2 };
|
|
2892
|
+
}
|
|
2893
|
+
const leading = args.slice(0, args.findIndex((arg) => arg.startsWith("-")) === -1 ? args.length : args.findIndex((arg) => arg.startsWith("-")));
|
|
2894
|
+
const tasks = leading[0] === "run" ? leading.slice(1) : leading;
|
|
2895
|
+
return { tool, tasks };
|
|
2981
2896
|
}
|
|
2982
|
-
function
|
|
2983
|
-
if (
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
return null;
|
|
2897
|
+
function aggregatorTaskPhrase(invocation) {
|
|
2898
|
+
if (invocation.tasks.length === 0) return "the tasks its own configuration selects";
|
|
2899
|
+
const named = invocation.tasks.length < 3 ? invocation.tasks.join(" and ") : `${invocation.tasks.slice(0, -1).join(", ")} and ${invocation.tasks.at(-1)}`;
|
|
2900
|
+
return `each package's own ${named} script${invocation.tasks.length === 1 ? "" : "s"}`;
|
|
2987
2901
|
}
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2902
|
+
var ROOT_CONFIG_EXECUTORS = /* @__PURE__ */ new Map([
|
|
2903
|
+
// Every spelling read out of a real repository on 2026-08-16: `@nx/jest:jest` is what ngxs/store
|
|
2904
|
+
// and nrwl/nx-labs declare, `@nx/vitest:test` is what analogjs/analog declares, and the `@nrwl/*`
|
|
2905
|
+
// names are the pre-16 package scope the same executors shipped under.
|
|
2906
|
+
["@nx/jest:jest", { runner: "jest", configOption: "jestConfig" }],
|
|
2907
|
+
["@nrwl/jest:jest", { runner: "jest", configOption: "jestConfig" }],
|
|
2908
|
+
["@nx/vite:test", { runner: "vitest", configOption: "configFile" }],
|
|
2909
|
+
["@nx/vitest:test", { runner: "vitest", configOption: "configFile" }],
|
|
2910
|
+
["@nrwl/vite:test", { runner: "vitest", configOption: "configFile" }]
|
|
2911
|
+
]);
|
|
2912
|
+
function workspaceRootCommand(runner, configFile, directory) {
|
|
2913
|
+
return runner === "jest" ? `npx --no-install jest --config ${configFile}` : `npx --no-install vitest run --root ${directory} --config ${configFile}`;
|
|
2998
2914
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
2915
|
+
var RUN_COMMANDS_EXECUTORS = /* @__PURE__ */ new Set(["nx:run-commands", "@nrwl/workspace:run-commands"]);
|
|
2916
|
+
function commandRunner(command) {
|
|
2917
|
+
const plan = planCommand2(command);
|
|
2918
|
+
if (plan.kind === "node-test") return "node-test";
|
|
2919
|
+
if (plan.kind === "runner") return plan.runner;
|
|
2920
|
+
const invoked = invokedProgram(command);
|
|
2921
|
+
if (invoked === null) return null;
|
|
2922
|
+
return SCRIPT_NAMED_RUNNERS2.includes(invoked.program) ? invoked.program : null;
|
|
3005
2923
|
}
|
|
3006
|
-
function
|
|
3007
|
-
|
|
2924
|
+
function normalizeWorkspacePath(raw) {
|
|
2925
|
+
const path = raw.trim().replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
|
|
2926
|
+
if (path === "" || path.startsWith("/") || /^[A-Za-z]:/u.test(path)) return null;
|
|
2927
|
+
if (/[;&|<>`\s'"$]|\$\(/u.test(path)) return null;
|
|
2928
|
+
if (path.split("/").some((segment) => segment === "..")) return null;
|
|
2929
|
+
return path;
|
|
3008
2930
|
}
|
|
3009
|
-
function
|
|
3010
|
-
const
|
|
3011
|
-
const
|
|
3012
|
-
if (
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
command: "corepack enable"
|
|
3019
|
-
});
|
|
3020
|
-
}
|
|
3021
|
-
steps.push({
|
|
3022
|
-
/* The step and its comment are one statement: a resolved install may not be introduced as
|
|
3023
|
-
coming from a lockfile the commit does not hold. See {@link ConfigInput.installUnlocked}. */
|
|
3024
|
-
what: input.installUnlocked === true ? STEP_WHAT.installUnlocked : STEP_WHAT.install,
|
|
3025
|
-
source: input.evidence.installCommand,
|
|
3026
|
-
/* THE COMMAND IS ABLOH'S AND THE ENVIRONMENT IS THE CUSTOMER'S. See
|
|
3027
|
-
{@link ConfigInput.installEnvironment}: the command comes from the lockfile and the values
|
|
3028
|
-
come from the step their own CI installs with. */
|
|
3029
|
-
command: stepBody(input.installEnvironment, input.installCommand)
|
|
3030
|
-
});
|
|
3031
|
-
}
|
|
3032
|
-
for (const entry of input.setupCommands) {
|
|
3033
|
-
steps.push({ what: STEP_WHAT.build, source: entry.source, command: stepBody(entry.env, entry.command) });
|
|
2931
|
+
function classifyNxTestTarget(projectJson, directory) {
|
|
2932
|
+
const targets = projectJson?.targets ?? projectJson?.architect;
|
|
2933
|
+
const target = targets?.["test"];
|
|
2934
|
+
if (target === void 0) {
|
|
2935
|
+
return {
|
|
2936
|
+
state: "unmeasurable",
|
|
2937
|
+
reason: `the Nx project at ${directory} declares no test target`,
|
|
2938
|
+
remedy: `give ${directory} a test target in its project.json, or a "test" script in its package.json`
|
|
2939
|
+
};
|
|
3034
2940
|
}
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
// src/setup-question-descriptor.ts
|
|
3039
|
-
import {
|
|
3040
|
-
judgeConfigKeyValue,
|
|
3041
|
-
SETUP_FAIL_FAST_ANSWERS,
|
|
3042
|
-
SETUP_FAIL_FAST_CONSEQUENCES
|
|
3043
|
-
} from "@abloh/core";
|
|
3044
|
-
var ATOM = /\s*,\s*/u;
|
|
3045
|
-
var COMMANDS = /\s*&&\s*/u;
|
|
3046
|
-
function splitAnswerList(shape, raw) {
|
|
3047
|
-
const trimmed = raw.trim();
|
|
3048
|
-
if (trimmed === "") return [];
|
|
3049
|
-
return trimmed.split(shape.kind === "commands" ? COMMANDS : ATOM).map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
3050
|
-
}
|
|
3051
|
-
function splitAtFirstEquals(entry) {
|
|
3052
|
-
const at = entry.indexOf("=");
|
|
3053
|
-
return at === -1 ? [entry, null] : [entry.slice(0, at), entry.slice(at + 1)];
|
|
3054
|
-
}
|
|
3055
|
-
var SERVICE_ENTRY = /^(?<image>.*@sha256:[a-f0-9]{64})(?::(?<port>\d{1,5}))?$/u;
|
|
3056
|
-
function readServiceEntries(raw) {
|
|
3057
|
-
return splitAnswerList({ kind: "list" }, raw).map((entry) => {
|
|
3058
|
-
const [name, rest] = splitAtFirstEquals(entry);
|
|
3059
|
-
const gate = rest === null ? null : SERVICE_ENTRY.exec(rest);
|
|
3060
|
-
if (gate === null) return { name, image: rest ?? "" };
|
|
3061
|
-
const port = gate.groups?.["port"];
|
|
2941
|
+
const executor = typeof target.executor === "string" ? target.executor : typeof target.builder === "string" ? target.builder : null;
|
|
2942
|
+
if (executor === null) {
|
|
3062
2943
|
return {
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
};
|
|
3067
|
-
});
|
|
3068
|
-
}
|
|
3069
|
-
function readAnswerValue(descriptor, raw) {
|
|
3070
|
-
switch (descriptor.shape.kind) {
|
|
3071
|
-
case "scalar":
|
|
3072
|
-
return raw.trim();
|
|
3073
|
-
case "list":
|
|
3074
|
-
return splitAnswerList(descriptor.shape, raw);
|
|
3075
|
-
case "commands":
|
|
3076
|
-
return splitAnswerList(descriptor.shape, raw);
|
|
3077
|
-
case "names":
|
|
3078
|
-
return splitAnswerList(descriptor.shape, raw);
|
|
3079
|
-
case "packages":
|
|
3080
|
-
return splitAnswerList(descriptor.shape, raw).map((entry) => {
|
|
3081
|
-
const [name, version] = splitAtFirstEquals(entry);
|
|
3082
|
-
return version === null ? { name } : { name, version };
|
|
3083
|
-
});
|
|
3084
|
-
case "services":
|
|
3085
|
-
return readServiceEntries(raw);
|
|
3086
|
-
case "pairs":
|
|
3087
|
-
return Object.fromEntries(
|
|
3088
|
-
splitAnswerList(descriptor.shape, raw).map((entry) => {
|
|
3089
|
-
const [name, value] = splitAtFirstEquals(entry);
|
|
3090
|
-
return [name, value ?? ""];
|
|
3091
|
-
})
|
|
3092
|
-
);
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3095
|
-
function setupAnswerProblem(key, raw) {
|
|
3096
|
-
const descriptor = SETUP_QUESTION_DESCRIPTORS[key];
|
|
3097
|
-
if (descriptor === void 0) return `abloh asks no question about ${key}`;
|
|
3098
|
-
const value = raw.trim();
|
|
3099
|
-
if (value === "") return descriptor.required ? "this question needs an answer" : null;
|
|
3100
|
-
if (descriptor.candidates.length > 0 && !descriptor.candidates.includes(value)) {
|
|
3101
|
-
return `${value} is not one of ${descriptor.candidates.join(", ")}`;
|
|
3102
|
-
}
|
|
3103
|
-
if (descriptor.key !== "target.directory" && descriptor.channel === "discovery") return null;
|
|
3104
|
-
const verdict = judgeConfigKeyValue(key, readAnswerValue(descriptor, raw));
|
|
3105
|
-
if (verdict.ok) return null;
|
|
3106
|
-
return verdict.at === null || verdict.at === void 0 ? verdict.reason : `${verdict.at} ${verdict.reason}`;
|
|
3107
|
-
}
|
|
3108
|
-
function applyAnswersToContract(draft, answers) {
|
|
3109
|
-
let out = draft;
|
|
3110
|
-
for (const descriptor of Object.values(SETUP_QUESTION_DESCRIPTORS)) {
|
|
3111
|
-
if (descriptor.apply === void 0) continue;
|
|
3112
|
-
const raw = answers[descriptor.key];
|
|
3113
|
-
if (raw === void 0 || raw.trim() === "") continue;
|
|
3114
|
-
out = descriptor.apply(out, raw);
|
|
3115
|
-
}
|
|
3116
|
-
return out;
|
|
3117
|
-
}
|
|
3118
|
-
var ANSWERED_EVIDENCE = "you answered this when you set abloh up";
|
|
3119
|
-
function evidenceFor(entries) {
|
|
3120
|
-
return entries.map((path) => ({ path, evidence: ANSWERED_EVIDENCE }));
|
|
3121
|
-
}
|
|
3122
|
-
var SETUP_QUESTION_DESCRIPTORS = {
|
|
3123
|
-
"environment.installCommand": {
|
|
3124
|
-
key: "environment.installCommand",
|
|
3125
|
-
ask: "How does this repository install its dependencies?",
|
|
3126
|
-
why: "Abloh read your CI without running it, so it saw the command your workflow declares and not what your install actually writes. On the borrow road your own job installs and Abloh measures inside it, so this is only needed when Abloh builds for itself.",
|
|
3127
|
-
candidates: [],
|
|
3128
|
-
required: false,
|
|
3129
|
-
unreadable: true,
|
|
3130
|
-
shape: { kind: "scalar" },
|
|
3131
|
-
channel: "contract",
|
|
3132
|
-
apply: (draft, answer) => ({
|
|
3133
|
-
...draft,
|
|
3134
|
-
installCommand: answer.trim(),
|
|
3135
|
-
evidence: { ...draft.evidence, installCommand: ANSWERED_EVIDENCE }
|
|
3136
|
-
})
|
|
3137
|
-
},
|
|
3138
|
-
"environment.setupCommands": {
|
|
3139
|
-
key: "environment.setupCommands",
|
|
3140
|
-
ask: "What does this repository build between installing and testing?",
|
|
3141
|
-
why: "Abloh read the build steps out of the job it rides. A step it could not read is one the proof container never runs, and a suite that needs it then fails inside abloh rather than in your CI.",
|
|
3142
|
-
candidates: [],
|
|
3143
|
-
required: true,
|
|
3144
|
-
/* ONE STEP PER ` && `, WHICH IS THE ONE KEY THAT IS NOT COMMA-SEPARATED and the CLI's prompt says
|
|
3145
|
-
so for the same reason: a comma is an ordinary character inside a command, and a build is read
|
|
3146
|
-
top to bottom. `splitAnswerList` is the one grammar both roads ask. */
|
|
3147
|
-
shape: { kind: "commands" },
|
|
3148
|
-
channel: "contract",
|
|
3149
|
-
apply: (draft, answer) => ({
|
|
3150
|
-
...draft,
|
|
3151
|
-
setupCommands: splitAnswerList({ kind: "commands" }, answer).map((command) => ({
|
|
3152
|
-
command,
|
|
3153
|
-
source: ANSWERED_EVIDENCE,
|
|
3154
|
-
/* A COMMAND SOMEBODY TYPED CARRIES THE ENVIRONMENT THEY TYPED INTO IT. `CiSetupCommand.env`
|
|
3155
|
-
is the environment abloh READ off a workflow step, so an answered command has none: what
|
|
3156
|
-
the answer says is the whole of what this command runs. */
|
|
3157
|
-
env: []
|
|
3158
|
-
}))
|
|
3159
|
-
})
|
|
3160
|
-
},
|
|
3161
|
-
"environment.runtimeImage": {
|
|
3162
|
-
key: "environment.runtimeImage",
|
|
3163
|
-
ask: "Which image should Abloh build its proof container from?",
|
|
3164
|
-
why: "Abloh pins one by pulling it and reading its digest, which needs a machine. Left unanswered, a run uses the reviewed default for the Node your repository declares.",
|
|
3165
|
-
candidates: [],
|
|
3166
|
-
required: false,
|
|
3167
|
-
unreadable: true,
|
|
3168
|
-
shape: { kind: "scalar" },
|
|
3169
|
-
channel: "contract",
|
|
3170
|
-
apply: (draft, answer) => ({
|
|
3171
|
-
...draft,
|
|
3172
|
-
runtimeImage: answer.trim(),
|
|
3173
|
-
evidence: { ...draft.evidence, runtimeImage: ANSWERED_EVIDENCE }
|
|
3174
|
-
})
|
|
3175
|
-
},
|
|
3176
|
-
"setup.job": {
|
|
3177
|
-
key: "setup.job",
|
|
3178
|
-
ask: "Which job should abloh measure in?",
|
|
3179
|
-
why: "Everything else in the contract is read out of that one job.",
|
|
3180
|
-
candidates: [],
|
|
3181
|
-
required: true,
|
|
3182
|
-
shape: { kind: "scalar" },
|
|
3183
|
-
channel: "discovery"
|
|
3184
|
-
},
|
|
3185
|
-
"setup.matrixLeg": {
|
|
3186
|
-
key: "setup.matrixLeg",
|
|
3187
|
-
ask: "Which leg of that job's matrix?",
|
|
3188
|
-
why: "The job abloh rides fans out, and a leg names one value per dimension of it.",
|
|
3189
|
-
candidates: [],
|
|
3190
|
-
required: true,
|
|
3191
|
-
shape: { kind: "scalar" },
|
|
3192
|
-
channel: "discovery"
|
|
3193
|
-
},
|
|
3194
|
-
"setup.failFast": {
|
|
3195
|
-
key: "setup.failFast",
|
|
3196
|
-
ask: "That job cancels every matrix leg when one fails. May abloh turn that off for it?",
|
|
3197
|
-
/* THE VALUES THE CONTRACT ACCEPTS, on `flaky`'s own rule: the card writes the chosen candidate
|
|
3198
|
-
into `abloh.yml`, so a candidate that read well and parsed badly would be a form that writes an
|
|
3199
|
-
invalid file. What each one MEANS is the ask above it. */
|
|
3200
|
-
why: SETUP_FAIL_FAST_ANSWERS.map((answer) => `${answer} ${SETUP_FAIL_FAST_CONSEQUENCES[answer]}.`).join(" "),
|
|
3201
|
-
candidates: SETUP_FAIL_FAST_ANSWERS,
|
|
3202
|
-
required: true,
|
|
3203
|
-
shape: { kind: "scalar" },
|
|
3204
|
-
/* A LINE IN THE FILE AND NOT A RE-DERIVATION. It does not move which job the recipe is read out
|
|
3205
|
-
of - it is the maintainer's standing answer about the job already chosen - and
|
|
3206
|
-
`configSetupJobLines` writes it on every road, with or without a job, because it is the one
|
|
3207
|
-
statement in that block that was never abloh's to discard. */
|
|
3208
|
-
channel: "contract",
|
|
3209
|
-
apply: (draft, answer) => ({ ...draft, setupFailFast: answer.trim() })
|
|
3210
|
-
},
|
|
3211
|
-
flaky: {
|
|
3212
|
-
key: "flaky",
|
|
3213
|
-
ask: "Your suite was red on untouched code. What should abloh do about those tests?",
|
|
3214
|
-
/* THE VALUES THE CONTRACT ACCEPTS, not a prose label: the card writes the chosen candidate into
|
|
3215
|
-
`abloh.yml`, so a candidate that read well and parsed badly would be a form that writes an
|
|
3216
|
-
invalid file. What each one MEANS is the ask above it. */
|
|
3217
|
-
why: "quarantine sets them aside and measures the rest. strict measures nothing until they pass.",
|
|
3218
|
-
candidates: ["quarantine", "strict"],
|
|
3219
|
-
required: true,
|
|
3220
|
-
shape: { kind: "scalar" },
|
|
3221
|
-
channel: "contract",
|
|
3222
|
-
/* `quarantine` WRITES NO LINE and that is not the answer being dropped: it is what a file with
|
|
3223
|
-
no `flaky:` key already means, which `configFlakyLines` states at the renderer. */
|
|
3224
|
-
apply: (draft, answer) => ({ ...draft, flaky: answer.trim() })
|
|
3225
|
-
},
|
|
3226
|
-
"environment.testCommand": {
|
|
3227
|
-
key: "environment.testCommand",
|
|
3228
|
-
ask: "Which command runs your tests?",
|
|
3229
|
-
why: "Abloh read your CI without running it, and the command it found did not run a suite it could observe.",
|
|
3230
|
-
candidates: [],
|
|
3231
|
-
required: true,
|
|
3232
|
-
shape: { kind: "scalar" },
|
|
3233
|
-
channel: "contract",
|
|
3234
|
-
apply: (draft, answer) => ({ ...draft, testCommand: answer.trim() })
|
|
3235
|
-
},
|
|
3236
|
-
"environment.sealedTestCommand": {
|
|
3237
|
-
key: "environment.sealedTestCommand",
|
|
3238
|
-
ask: "Which command runs your tests inside abloh's proof container?",
|
|
3239
|
-
why: "Your own test script did something abloh's container could not, so it needs the runner on its own.",
|
|
3240
|
-
candidates: [],
|
|
3241
|
-
required: true,
|
|
3242
|
-
shape: { kind: "scalar" },
|
|
3243
|
-
channel: "contract",
|
|
3244
|
-
apply: (draft, answer) => ({ ...draft, sealedTestCommand: answer.trim() })
|
|
3245
|
-
},
|
|
3246
|
-
"environment.rebuildCommand": {
|
|
3247
|
-
key: "environment.rebuildCommand",
|
|
3248
|
-
ask: "Which command builds the output your tests import?",
|
|
3249
|
-
why: "Your tests read compiled output rather than the source abloh plants in, so without the build every planted bug is reported as missed. Abloh cannot resolve which of your build scripts writes it from your files alone.",
|
|
3250
|
-
candidates: [],
|
|
3251
|
-
required: true,
|
|
3252
|
-
shape: { kind: "scalar" },
|
|
3253
|
-
channel: "contract",
|
|
3254
|
-
/* THE CANDIDATES AND THE MANAGER SURVIVE THE ANSWER, because they are the drafting's reading of
|
|
3255
|
-
this repository and the answer is a value: a run that later clears the value would otherwise
|
|
3256
|
-
lose the finding that made the key worth writing. Absent, they are the empty pair, which is
|
|
3257
|
-
what a draft that found no build script already carries. */
|
|
3258
|
-
apply: (draft, answer) => ({
|
|
3259
|
-
...draft,
|
|
3260
|
-
rebuild: {
|
|
3261
|
-
...draft.rebuild ?? { candidates: [], manager: "" },
|
|
3262
|
-
value: answer.trim(),
|
|
3263
|
-
evidence: ANSWERED_EVIDENCE
|
|
3264
|
-
}
|
|
3265
|
-
})
|
|
3266
|
-
},
|
|
3267
|
-
"target.directory": {
|
|
3268
|
-
key: "target.directory",
|
|
3269
|
-
ask: "Which package should abloh measure?",
|
|
3270
|
-
why: "This repository holds several, and a run measures one.",
|
|
3271
|
-
candidates: [],
|
|
3272
|
-
required: true,
|
|
3273
|
-
shape: { kind: "scalar" },
|
|
3274
|
-
/* A DISCOVERY ANSWER AND NOT A LINE. The runner, the test command and the whole measurement plan
|
|
3275
|
-
are read out of the package being measured, so writing this key over a draft derived from the
|
|
3276
|
-
root would leave every sibling value describing a different package. It goes back through
|
|
3277
|
-
`resolveMeasurementPlan` as its `subdir`, exactly as the job pin goes back through the job. */
|
|
3278
|
-
channel: "discovery"
|
|
3279
|
-
},
|
|
3280
|
-
"environment.coverageProvider": {
|
|
3281
|
-
key: "environment.coverageProvider",
|
|
3282
|
-
ask: "May abloh install the coverage provider your runner needs?",
|
|
3283
|
-
why: "Your tree does not carry one, and without it abloh cannot say which of your changed lines your tests execute.",
|
|
3284
|
-
candidates: ["install", "skip"],
|
|
3285
|
-
required: true,
|
|
3286
|
-
shape: { kind: "scalar" },
|
|
3287
|
-
channel: "contract",
|
|
3288
|
-
/* WRITTEN FOR BOTH ANSWERS, because a recorded answer is the difference between a maintainer
|
|
3289
|
-
who chose the default and one who was never asked - see `ConfigInput.coverageProvider`. */
|
|
3290
|
-
apply: (draft, answer) => ({ ...draft, coverageProvider: answer.trim() })
|
|
3291
|
-
},
|
|
3292
|
-
"environment.requiredVariables": {
|
|
3293
|
-
key: "environment.requiredVariables",
|
|
3294
|
-
ask: "Which environment variables does your suite need?",
|
|
3295
|
-
why: "Names only. The values come from your own environment and are masked in every log.",
|
|
3296
|
-
candidates: [],
|
|
3297
|
-
required: true,
|
|
3298
|
-
shape: { kind: "names" },
|
|
3299
|
-
channel: "contract",
|
|
3300
|
-
apply: (draft, answer) => ({
|
|
3301
|
-
...draft,
|
|
3302
|
-
requiredVariables: splitAnswerList({ kind: "names" }, answer).map((name) => ({
|
|
3303
|
-
name,
|
|
3304
|
-
evidence: ANSWERED_EVIDENCE
|
|
3305
|
-
}))
|
|
3306
|
-
})
|
|
3307
|
-
},
|
|
3308
|
-
"environment.environmentValues": {
|
|
3309
|
-
key: "environment.environmentValues",
|
|
3310
|
-
ask: "Which literal values does your suite need in its environment?",
|
|
3311
|
-
why: "A secret goes in requiredVariables by name instead, and never here.",
|
|
3312
|
-
candidates: [],
|
|
3313
|
-
required: true,
|
|
3314
|
-
shape: { kind: "pairs" },
|
|
3315
|
-
channel: "contract",
|
|
3316
|
-
apply: (draft, answer) => ({
|
|
3317
|
-
...draft,
|
|
3318
|
-
environmentValues: splitAnswerList({ kind: "pairs" }, answer).map((entry) => {
|
|
3319
|
-
const [name, value] = splitAtFirstEquals(entry);
|
|
3320
|
-
return { name, value: value ?? "", evidence: ANSWERED_EVIDENCE };
|
|
3321
|
-
})
|
|
3322
|
-
})
|
|
3323
|
-
},
|
|
3324
|
-
"environment.generatedFiles": {
|
|
3325
|
-
key: "environment.generatedFiles",
|
|
3326
|
-
ask: "Which directories does your own tooling write during a run?",
|
|
3327
|
-
why: "An undeclared change to the checkout aborts a run, and abloh cannot tell yours from a stray one.",
|
|
3328
|
-
candidates: [],
|
|
3329
|
-
required: true,
|
|
3330
|
-
shape: { kind: "list" },
|
|
3331
|
-
channel: "contract",
|
|
3332
|
-
apply: (draft, answer) => ({
|
|
3333
|
-
...draft,
|
|
3334
|
-
generatedFiles: evidenceFor(splitAnswerList({ kind: "list" }, answer))
|
|
3335
|
-
})
|
|
3336
|
-
},
|
|
3337
|
-
"environment.rewrittenFiles": {
|
|
3338
|
-
key: "environment.rewrittenFiles",
|
|
3339
|
-
ask: "Which committed files does your own install rewrite?",
|
|
3340
|
-
why: "A rate measured over bytes your install changed is a rate about something else.",
|
|
3341
|
-
candidates: [],
|
|
3342
|
-
required: true,
|
|
3343
|
-
shape: { kind: "list" },
|
|
3344
|
-
channel: "contract",
|
|
3345
|
-
apply: (draft, answer) => ({
|
|
3346
|
-
...draft,
|
|
3347
|
-
rewrittenFiles: evidenceFor(splitAnswerList({ kind: "list" }, answer))
|
|
3348
|
-
})
|
|
3349
|
-
},
|
|
3350
|
-
"environment.identityFiles": {
|
|
3351
|
-
key: "environment.identityFiles",
|
|
3352
|
-
ask: "Which files should a change to reinstall rather than reuse the container?",
|
|
3353
|
-
why: "Abloh reuses a prepared container between runs unless one of these moved.",
|
|
3354
|
-
candidates: [],
|
|
3355
|
-
required: true,
|
|
3356
|
-
shape: { kind: "list" },
|
|
3357
|
-
channel: "contract",
|
|
3358
|
-
apply: (draft, answer) => ({
|
|
3359
|
-
...draft,
|
|
3360
|
-
identityFiles: splitAnswerList({ kind: "list" }, answer)
|
|
3361
|
-
})
|
|
3362
|
-
},
|
|
3363
|
-
"environment.systemPackages": {
|
|
3364
|
-
key: "environment.systemPackages",
|
|
3365
|
-
ask: "Which OS packages does your build need?",
|
|
3366
|
-
why: "The reviewed proof image is slim, so a native dependency that compiles on install needs its toolchain named.",
|
|
3367
|
-
candidates: [],
|
|
3368
|
-
required: true,
|
|
3369
|
-
shape: { kind: "packages" },
|
|
3370
|
-
channel: "contract",
|
|
3371
|
-
apply: (draft, answer) => ({
|
|
3372
|
-
...draft,
|
|
3373
|
-
systemPackages: splitAnswerList({ kind: "packages" }, answer).map((entry) => {
|
|
3374
|
-
const [name, version] = splitAtFirstEquals(entry);
|
|
3375
|
-
return { name, version, source: ANSWERED_EVIDENCE };
|
|
3376
|
-
})
|
|
3377
|
-
})
|
|
3378
|
-
},
|
|
3379
|
-
"environment.runtimes": {
|
|
3380
|
-
key: "environment.runtimes",
|
|
3381
|
-
ask: "Which runtimes beyond Node does your suite need?",
|
|
3382
|
-
/* NO CLOSED CANDIDATE LIST, because this key takes SEVERAL: `setupAnswerProblem` compares a
|
|
3383
|
-
closed list against the WHOLE answer, so `bun, deno` would be refused by a two-word list that
|
|
3384
|
-
accepts each of them alone. `judgeConfigKeyValue` names the two the contract holds, entry by
|
|
3385
|
-
entry, which is the reading that fits a list. */
|
|
3386
|
-
why: "The proof image carries Node. A runtime your tests reach for and the image does not carry fails inside abloh rather than in your CI.",
|
|
3387
|
-
candidates: [],
|
|
3388
|
-
required: true,
|
|
3389
|
-
shape: { kind: "names" },
|
|
3390
|
-
channel: "contract",
|
|
3391
|
-
apply: (draft, answer) => ({
|
|
3392
|
-
...draft,
|
|
3393
|
-
runtimes: splitAnswerList({ kind: "names" }, answer).map((runtime) => ({
|
|
3394
|
-
runtime,
|
|
3395
|
-
evidence: ANSWERED_EVIDENCE
|
|
3396
|
-
}))
|
|
3397
|
-
})
|
|
3398
|
-
},
|
|
3399
|
-
"environment.services": {
|
|
3400
|
-
key: "environment.services",
|
|
3401
|
-
ask: "Which backing services does your suite need, and where is each one ready?",
|
|
3402
|
-
why: "A proof runs in the same network namespace as its services, so your suite reaches them on localhost exactly as in your CI. Abloh pins the image by digest because a mutable tag cannot be part of what a run binds, and each service needs something to wait on before the suite starts.",
|
|
3403
|
-
candidates: [],
|
|
3404
|
-
required: true,
|
|
3405
|
-
shape: { kind: "services" },
|
|
3406
|
-
channel: "contract",
|
|
3407
|
-
/* THE ENVIRONMENT IS NOT TYPED HERE and the entry says so at {@link SERVICE_ENTRY}: an image that
|
|
3408
|
-
needs credentials fails its readiness gate and reports that, which is the same narrow door the
|
|
3409
|
-
CLI's own typed entry goes through. */
|
|
3410
|
-
apply: (draft, answer) => ({
|
|
3411
|
-
...draft,
|
|
3412
|
-
services: readServiceEntries(answer).map((service) => ({
|
|
3413
|
-
name: service.name,
|
|
3414
|
-
spelling: `${service.name}=${service.image}`,
|
|
3415
|
-
image: service.image,
|
|
3416
|
-
env: [],
|
|
3417
|
-
healthCommand: null,
|
|
3418
|
-
healthPort: service.healthPort ?? null,
|
|
3419
|
-
source: ANSWERED_EVIDENCE,
|
|
3420
|
-
evidence: ANSWERED_EVIDENCE
|
|
3421
|
-
}))
|
|
3422
|
-
})
|
|
3423
|
-
},
|
|
3424
|
-
"environment.installDirectory": {
|
|
3425
|
-
key: "environment.installDirectory",
|
|
3426
|
-
ask: "Which directory owns the lockfile your install reads?",
|
|
3427
|
-
why: "A workspace package installs from the root that holds its lockfile, not from its own folder.",
|
|
3428
|
-
candidates: [],
|
|
3429
|
-
required: false,
|
|
3430
|
-
shape: { kind: "scalar" },
|
|
3431
|
-
channel: "contract",
|
|
3432
|
-
apply: (draft, answer) => ({ ...draft, installDirectory: answer.trim() })
|
|
3433
|
-
},
|
|
3434
|
-
"environment.runner": {
|
|
3435
|
-
key: "environment.runner",
|
|
3436
|
-
ask: "Which test runner does this repository use?",
|
|
3437
|
-
why: "Abloh reads it from your own command, and a declaration outranks that reading.",
|
|
3438
|
-
candidates: [],
|
|
3439
|
-
required: false,
|
|
3440
|
-
shape: { kind: "scalar" },
|
|
3441
|
-
channel: "contract",
|
|
3442
|
-
apply: (draft, answer) => ({
|
|
3443
|
-
...draft,
|
|
3444
|
-
runner: answer.trim(),
|
|
3445
|
-
boundRunner: answer.trim(),
|
|
3446
|
-
evidence: { ...draft.evidence, runner: "declared in abloh.yml" }
|
|
3447
|
-
})
|
|
3448
|
-
}
|
|
3449
|
-
};
|
|
3450
|
-
var ANSWERABLE_SETUP_KEYS = Object.keys(SETUP_QUESTION_DESCRIPTORS);
|
|
3451
|
-
function contractDescriptors() {
|
|
3452
|
-
return Object.values(SETUP_QUESTION_DESCRIPTORS).filter(
|
|
3453
|
-
(descriptor) => descriptor.channel === "contract"
|
|
3454
|
-
);
|
|
3455
|
-
}
|
|
3456
|
-
function answeredTargetDirectory(answers) {
|
|
3457
|
-
const value = answers["target.directory"];
|
|
3458
|
-
return value === void 0 || value.trim() === "" ? void 0 : value.trim();
|
|
3459
|
-
}
|
|
3460
|
-
|
|
3461
|
-
// src/deno-project.ts
|
|
3462
|
-
import { lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
|
|
3463
|
-
import { join as join4 } from "path";
|
|
3464
|
-
import { parseJsonc } from "@abloh/core";
|
|
3465
|
-
var DENO_CONFIG_FILES = ["deno.json", "deno.jsonc"];
|
|
3466
|
-
var DENO_LOCKFILE = "deno.lock";
|
|
3467
|
-
function stageInvokesDenoTest(stage) {
|
|
3468
|
-
const tokens = stage.trim().split(/\s+/u).filter((token) => token !== "");
|
|
3469
|
-
const first = (tokens[0] ?? "").replace(/^.*\//u, "");
|
|
3470
|
-
return first === "deno" && /^deno\s+test(?:\s|$)/u.test(stage.trim());
|
|
3471
|
-
}
|
|
3472
|
-
function denoTestInvocation(taskBody) {
|
|
3473
|
-
if (taskBody === null) return null;
|
|
3474
|
-
const script = taskBody.trim();
|
|
3475
|
-
if (script === "") return null;
|
|
3476
|
-
if (/[;|<>`$()\n]/u.test(script)) return null;
|
|
3477
|
-
if (/(^|[^&])&([^&]|$)/u.test(script)) return null;
|
|
3478
|
-
const stages = script.split("&&").map((stage) => stage.trim());
|
|
3479
|
-
if (stages.some((stage) => stage === "")) return null;
|
|
3480
|
-
const matches = stages.filter(stageInvokesDenoTest);
|
|
3481
|
-
return matches.length === 1 ? matches[0] : null;
|
|
3482
|
-
}
|
|
3483
|
-
function readDenoProject(dir, readText = diskReadText) {
|
|
3484
|
-
for (const name of DENO_CONFIG_FILES) {
|
|
3485
|
-
const path = join4(dir, name);
|
|
3486
|
-
const text = readText(path);
|
|
3487
|
-
if (text === null) continue;
|
|
3488
|
-
const parsed = parseJsonc(text);
|
|
3489
|
-
const tasks = parsed?.tasks;
|
|
3490
|
-
const declared = tasks !== null && typeof tasks === "object" ? tasks["test"] : void 0;
|
|
3491
|
-
const body = typeof declared === "string" ? declared : declared !== null && typeof declared === "object" && typeof declared.command === "string" ? declared.command : null;
|
|
3492
|
-
if (body === null || body.trim() === "") return null;
|
|
3493
|
-
return { configFile: name, testTask: body, testInvocation: denoTestInvocation(body) };
|
|
3494
|
-
}
|
|
3495
|
-
return null;
|
|
3496
|
-
}
|
|
3497
|
-
function denoLockfilePath(dir, readText = diskReadText) {
|
|
3498
|
-
return readText(join4(dir, DENO_LOCKFILE)) === null ? null : DENO_LOCKFILE;
|
|
3499
|
-
}
|
|
3500
|
-
function scriptRunsDenoTest(testScript) {
|
|
3501
|
-
if (testScript === null) return false;
|
|
3502
|
-
return testScript.split(/&&|\|\||[;&|]/u).map((stage) => stage.trim()).some(stageInvokesDenoTest);
|
|
3503
|
-
}
|
|
3504
|
-
function diskReadText(absolutePath) {
|
|
3505
|
-
try {
|
|
3506
|
-
const info = lstatSync2(absolutePath);
|
|
3507
|
-
if (!info.isFile() || info.isSymbolicLink()) return null;
|
|
3508
|
-
return readFileSync2(absolutePath, "utf8");
|
|
3509
|
-
} catch {
|
|
3510
|
-
return null;
|
|
3511
|
-
}
|
|
3512
|
-
}
|
|
3513
|
-
|
|
3514
|
-
// src/detect.ts
|
|
3515
|
-
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
3516
|
-
import { dirname as dirname4, isAbsolute as isAbsolute4, join as join8, relative as relative5, resolve as resolve6, sep as sep4 } from "path";
|
|
3517
|
-
import { SCRIPT_NAMED_RUNNERS as SCRIPT_NAMED_RUNNERS3, angularRunnerName, commandTokens, compiledOutputEvidence, debugPreparationOverflowStack, readsCompiledOutput, detectModuleLayout, directRunnerCommand, localBinaryPrefix, manifestScripts as manifestScripts3, parseLiteralArguments as parseLiteralArguments2, parsePreparedTestCommand, planCommand as planCommand3, nextActionSentence, readPackageManagerDeclaration, readPackageManagerPin, resolveCommandPlan, runnerNamesInScript, scriptRuns as scriptRuns2, selectAngularTestTarget, nameDrivenRunners, summaryThenAction, watchModeRefusal, withWatchModeDisabled, withoutWatchDisableFlags } from "@abloh/core";
|
|
3518
|
-
|
|
3519
|
-
// src/rush-workspace.ts
|
|
3520
|
-
import { parseJsonc as parseJsonc2 } from "@abloh/core";
|
|
3521
|
-
var RUSH_CONFIG_FILE = "rush.json";
|
|
3522
|
-
var RUSH_TEST_SCRIPTS = ["test", "_phase:test"];
|
|
3523
|
-
var RUSH_CONFIG_DIRECTORY = "common/config/rush";
|
|
3524
|
-
var RUSH_SUBSPACE_DIRECTORY = "common/config/subspaces";
|
|
3525
|
-
var RUSH_LOCKFILE_NAME = "pnpm-lock.yaml";
|
|
3526
|
-
var RUSH_REPO_STATE_NAME = "repo-state.json";
|
|
3527
|
-
var RUSH_INSTALL_COMMAND = "node common/scripts/install-run-rush.js install";
|
|
3528
|
-
var RUSH_INSTALL_SCRIPT = "common/scripts/install-run-rush.js";
|
|
3529
|
-
function classifyRushTestScript(manifest, directory) {
|
|
3530
|
-
const table = manifestScripts2(manifest);
|
|
3531
|
-
for (const script of RUSH_TEST_SCRIPTS) {
|
|
3532
|
-
const body = table[script];
|
|
3533
|
-
if (typeof body !== "string" || body.trim() === "") continue;
|
|
3534
|
-
if (/[;&|<>`\n]|\$\(/u.test(body)) {
|
|
3535
|
-
return {
|
|
3536
|
-
state: "unmeasurable",
|
|
3537
|
-
reason: `the Rush project at ${directory} composes shell commands in its ${script} script rather than running one suite`,
|
|
3538
|
-
remedy: `reduce ${directory}'s ${script} script to one command, or declare a plain "test" script that runs one suite`
|
|
3539
|
-
};
|
|
3540
|
-
}
|
|
3541
|
-
return { state: "command", script, command: body.trim() };
|
|
3542
|
-
}
|
|
3543
|
-
return {
|
|
3544
|
-
state: "unmeasurable",
|
|
3545
|
-
reason: `the Rush project at ${directory} declares neither a "test" nor a "_phase:test" script`,
|
|
3546
|
-
remedy: `give ${directory} a "test" or "_phase:test" script in its package.json that runs its tests`
|
|
3547
|
-
};
|
|
3548
|
-
}
|
|
3549
|
-
function manifestScripts2(manifest) {
|
|
3550
|
-
const scripts = manifest?.scripts;
|
|
3551
|
-
return scripts !== null && typeof scripts === "object" ? scripts : {};
|
|
3552
|
-
}
|
|
3553
|
-
function enumerateRushProjects(readFile, limit) {
|
|
3554
|
-
const parsed = parseJsonc2(readFile(RUSH_CONFIG_FILE));
|
|
3555
|
-
const declared = parsed?.projects;
|
|
3556
|
-
if (!Array.isArray(declared)) return [];
|
|
3557
|
-
const projects = [];
|
|
3558
|
-
for (const entry of declared) {
|
|
3559
|
-
if (entry === null || typeof entry !== "object") continue;
|
|
3560
|
-
const name = entry.packageName;
|
|
3561
|
-
const folder = entry.projectFolder;
|
|
3562
|
-
if (typeof name !== "string" || typeof folder !== "string") continue;
|
|
3563
|
-
const directory = normalizeProjectFolder(folder);
|
|
3564
|
-
if (directory === null) continue;
|
|
3565
|
-
const manifestPath = `${directory}/package.json`;
|
|
3566
|
-
const manifestText = readFile(manifestPath);
|
|
3567
|
-
if (manifestText === null) {
|
|
3568
|
-
projects.push({
|
|
3569
|
-
directory,
|
|
3570
|
-
name,
|
|
3571
|
-
test: {
|
|
3572
|
-
state: "unmeasurable",
|
|
3573
|
-
// Enumerated even so: the directory must not be silently absorbed into a parent's file
|
|
3574
|
-
// list, which would attribute this project's changed files to another project.
|
|
3575
|
-
reason: `the Rush project ${name} declares the folder ${directory}, which has no readable package.json`,
|
|
3576
|
-
remedy: `add ${manifestPath}, or remove ${name} from rush.json's projects`
|
|
3577
|
-
}
|
|
3578
|
-
});
|
|
3579
|
-
continue;
|
|
3580
|
-
}
|
|
3581
|
-
const manifest = parseJsonc2(manifestText);
|
|
3582
|
-
projects.push({
|
|
3583
|
-
directory,
|
|
3584
|
-
name,
|
|
3585
|
-
test: manifest === null ? {
|
|
3586
|
-
state: "unmeasurable",
|
|
3587
|
-
reason: `the Rush project ${name}'s manifest at ${manifestPath} is not readable JSON`,
|
|
3588
|
-
remedy: `fix ${manifestPath} so its manifest parses`
|
|
3589
|
-
} : classifyRushTestScript(manifest, directory)
|
|
3590
|
-
});
|
|
3591
|
-
if (projects.length >= limit) break;
|
|
3592
|
-
}
|
|
3593
|
-
return projects.sort((a, b) => a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0);
|
|
3594
|
-
}
|
|
3595
|
-
function normalizeProjectFolder(raw) {
|
|
3596
|
-
const path = raw.trim().replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
|
|
3597
|
-
if (path === "" || path.startsWith("/") || /^[A-Za-z]:/u.test(path)) return null;
|
|
3598
|
-
if (/[;&|<>`\s'"$]|\$\(/u.test(path)) return null;
|
|
3599
|
-
if (path.split("/").some((segment) => segment === "..")) return null;
|
|
3600
|
-
return path;
|
|
3601
|
-
}
|
|
3602
|
-
function rushIdentityFiles(input) {
|
|
3603
|
-
const files = [RUSH_CONFIG_FILE];
|
|
3604
|
-
const collect = (directory) => {
|
|
3605
|
-
for (const name of [RUSH_LOCKFILE_NAME, RUSH_REPO_STATE_NAME]) {
|
|
3606
|
-
const path = `${directory}/${name}`;
|
|
3607
|
-
if (input.exists(path)) files.push(path);
|
|
3608
|
-
}
|
|
3609
|
-
};
|
|
3610
|
-
collect(RUSH_CONFIG_DIRECTORY);
|
|
3611
|
-
for (const subspace of [...input.subdirectories(RUSH_SUBSPACE_DIRECTORY)].sort()) {
|
|
3612
|
-
collect(`${RUSH_SUBSPACE_DIRECTORY}/${subspace}`);
|
|
3613
|
-
}
|
|
3614
|
-
return files;
|
|
3615
|
-
}
|
|
3616
|
-
function rushLockfiles(input) {
|
|
3617
|
-
return rushIdentityFiles(input).filter((path) => path.endsWith(`/${RUSH_LOCKFILE_NAME}`));
|
|
3618
|
-
}
|
|
3619
|
-
function rushNodeVersionRange(rushJsonText) {
|
|
3620
|
-
const parsed = parseJsonc2(rushJsonText);
|
|
3621
|
-
const range = parsed?.nodeSupportedVersionRange;
|
|
3622
|
-
return typeof range === "string" && range.trim() !== "" ? range.trim() : null;
|
|
3623
|
-
}
|
|
3624
|
-
|
|
3625
|
-
// src/workspace-aggregator.ts
|
|
3626
|
-
import { SCRIPT_NAMED_RUNNERS as SCRIPT_NAMED_RUNNERS2, binaryName, planCommand as planCommand2 } from "@abloh/core";
|
|
3627
|
-
function invokedProgram(command) {
|
|
3628
|
-
const plan = planCommand2(command);
|
|
3629
|
-
if (plan.kind === "program") return { program: plan.program, args: plan.args };
|
|
3630
|
-
if (plan.kind === "manager-passthrough" && plan.token !== null) {
|
|
3631
|
-
return { program: binaryName(plan.token), args: plan.args };
|
|
3632
|
-
}
|
|
3633
|
-
return null;
|
|
3634
|
-
}
|
|
3635
|
-
var TOOLS = ["turbo", "lage", "nx"];
|
|
3636
|
-
function nxTargets(args) {
|
|
3637
|
-
const targets = [];
|
|
3638
|
-
for (let at = 0; at < args.length; at += 1) {
|
|
3639
|
-
const arg = args[at];
|
|
3640
|
-
const inline = /^(?:-t|--targets?)=(.+)$/u.exec(arg);
|
|
3641
|
-
if (inline !== null) {
|
|
3642
|
-
targets.push(...inline[1].split(","));
|
|
3643
|
-
continue;
|
|
3644
|
-
}
|
|
3645
|
-
if (/^(?:-t|--targets?)$/u.test(arg)) {
|
|
3646
|
-
const value = args[at + 1];
|
|
3647
|
-
if (value !== void 0 && !value.startsWith("-")) {
|
|
3648
|
-
targets.push(...value.split(","));
|
|
3649
|
-
at += 1;
|
|
3650
|
-
}
|
|
3651
|
-
}
|
|
3652
|
-
}
|
|
3653
|
-
return targets.filter((task) => task !== "");
|
|
3654
|
-
}
|
|
3655
|
-
function aggregatorInvocation(testScript) {
|
|
3656
|
-
if (testScript === null) return null;
|
|
3657
|
-
const script = testScript.trim();
|
|
3658
|
-
if (script === "" || /[;&|<>`\n]|\$\(/u.test(script)) return null;
|
|
3659
|
-
const invoked = invokedProgram(script);
|
|
3660
|
-
if (invoked === null) return null;
|
|
3661
|
-
const tool = TOOLS.find((candidate) => candidate === invoked.program);
|
|
3662
|
-
if (tool === void 0) return null;
|
|
3663
|
-
const args = invoked.args;
|
|
3664
|
-
if (tool === "nx") {
|
|
3665
|
-
const explicit = nxTargets(args);
|
|
3666
|
-
if (explicit.length > 0) return { tool, tasks: explicit };
|
|
3667
|
-
const positional = args.filter((arg) => !arg.startsWith("-"));
|
|
3668
|
-
const head = positional[0];
|
|
3669
|
-
if (head === void 0) return { tool, tasks: [] };
|
|
3670
|
-
if (head === "run-many" || head === "affected") return { tool, tasks: [] };
|
|
3671
|
-
if (head === "run") {
|
|
3672
|
-
const target = positional[1]?.split(":")[1];
|
|
3673
|
-
return { tool, tasks: target === void 0 ? [] : [target] };
|
|
3674
|
-
}
|
|
3675
|
-
return { tool, tasks: [head] };
|
|
3676
|
-
}
|
|
3677
|
-
const leading = args.slice(0, args.findIndex((arg) => arg.startsWith("-")) === -1 ? args.length : args.findIndex((arg) => arg.startsWith("-")));
|
|
3678
|
-
const tasks = leading[0] === "run" ? leading.slice(1) : leading;
|
|
3679
|
-
return { tool, tasks };
|
|
3680
|
-
}
|
|
3681
|
-
function aggregatorTaskPhrase(invocation) {
|
|
3682
|
-
if (invocation.tasks.length === 0) return "the tasks its own configuration selects";
|
|
3683
|
-
const named = invocation.tasks.length < 3 ? invocation.tasks.join(" and ") : `${invocation.tasks.slice(0, -1).join(", ")} and ${invocation.tasks.at(-1)}`;
|
|
3684
|
-
return `each package's own ${named} script${invocation.tasks.length === 1 ? "" : "s"}`;
|
|
3685
|
-
}
|
|
3686
|
-
var ROOT_CONFIG_EXECUTORS = /* @__PURE__ */ new Map([
|
|
3687
|
-
// Every spelling read out of a real repository on 2026-08-16: `@nx/jest:jest` is what ngxs/store
|
|
3688
|
-
// and nrwl/nx-labs declare, `@nx/vitest:test` is what analogjs/analog declares, and the `@nrwl/*`
|
|
3689
|
-
// names are the pre-16 package scope the same executors shipped under.
|
|
3690
|
-
["@nx/jest:jest", { runner: "jest", configOption: "jestConfig" }],
|
|
3691
|
-
["@nrwl/jest:jest", { runner: "jest", configOption: "jestConfig" }],
|
|
3692
|
-
["@nx/vite:test", { runner: "vitest", configOption: "configFile" }],
|
|
3693
|
-
["@nx/vitest:test", { runner: "vitest", configOption: "configFile" }],
|
|
3694
|
-
["@nrwl/vite:test", { runner: "vitest", configOption: "configFile" }]
|
|
3695
|
-
]);
|
|
3696
|
-
function workspaceRootCommand(runner, configFile, directory) {
|
|
3697
|
-
return runner === "jest" ? `npx --no-install jest --config ${configFile}` : `npx --no-install vitest run --root ${directory} --config ${configFile}`;
|
|
3698
|
-
}
|
|
3699
|
-
var RUN_COMMANDS_EXECUTORS = /* @__PURE__ */ new Set(["nx:run-commands", "@nrwl/workspace:run-commands"]);
|
|
3700
|
-
function commandRunner(command) {
|
|
3701
|
-
const plan = planCommand2(command);
|
|
3702
|
-
if (plan.kind === "node-test") return "node-test";
|
|
3703
|
-
if (plan.kind === "runner") return plan.runner;
|
|
3704
|
-
const invoked = invokedProgram(command);
|
|
3705
|
-
if (invoked === null) return null;
|
|
3706
|
-
return SCRIPT_NAMED_RUNNERS2.includes(invoked.program) ? invoked.program : null;
|
|
3707
|
-
}
|
|
3708
|
-
function normalizeWorkspacePath(raw) {
|
|
3709
|
-
const path = raw.trim().replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
|
|
3710
|
-
if (path === "" || path.startsWith("/") || /^[A-Za-z]:/u.test(path)) return null;
|
|
3711
|
-
if (/[;&|<>`\s'"$]|\$\(/u.test(path)) return null;
|
|
3712
|
-
if (path.split("/").some((segment) => segment === "..")) return null;
|
|
3713
|
-
return path;
|
|
3714
|
-
}
|
|
3715
|
-
function classifyNxTestTarget(projectJson, directory) {
|
|
3716
|
-
const targets = projectJson?.targets ?? projectJson?.architect;
|
|
3717
|
-
const target = targets?.["test"];
|
|
3718
|
-
if (target === void 0) {
|
|
3719
|
-
return {
|
|
3720
|
-
state: "unmeasurable",
|
|
3721
|
-
reason: `the Nx project at ${directory} declares no test target`,
|
|
3722
|
-
remedy: `give ${directory} a test target in its project.json, or a "test" script in its package.json`
|
|
3723
|
-
};
|
|
3724
|
-
}
|
|
3725
|
-
const executor = typeof target.executor === "string" ? target.executor : typeof target.builder === "string" ? target.builder : null;
|
|
3726
|
-
if (executor === null) {
|
|
3727
|
-
return {
|
|
3728
|
-
state: "unmeasurable",
|
|
3729
|
-
reason: `the Nx project at ${directory} declares a test target with no executor`,
|
|
3730
|
-
remedy: `name an executor on ${directory}'s test target, or give the package a "test" script`
|
|
2944
|
+
state: "unmeasurable",
|
|
2945
|
+
reason: `the Nx project at ${directory} declares a test target with no executor`,
|
|
2946
|
+
remedy: `name an executor on ${directory}'s test target, or give the package a "test" script`
|
|
3731
2947
|
};
|
|
3732
2948
|
}
|
|
3733
2949
|
const rootConfig = ROOT_CONFIG_EXECUTORS.get(executor);
|
|
@@ -3893,9 +3109,9 @@ import { InvariantError } from "@abloh/core";
|
|
|
3893
3109
|
import { execFileSync } from "child_process";
|
|
3894
3110
|
import { lstatSync as lstatSync3, mkdirSync, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync3, rmSync } from "fs";
|
|
3895
3111
|
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join6, normalize as normalize2, resolve as resolve5, sep as sep3 } from "path";
|
|
3896
|
-
import { DeclaredRefusalError
|
|
3112
|
+
import { DeclaredRefusalError, mergeBaseNotEstablished, refusalFromFacts } from "@abloh/core";
|
|
3897
3113
|
var refuseSubmodule = (path, observed) => {
|
|
3898
|
-
throw new
|
|
3114
|
+
throw new DeclaredRefusalError(refusalFromFacts({ kind: "submodule-admission", path, observed }));
|
|
3899
3115
|
};
|
|
3900
3116
|
var GITLINK_MODE = "160000";
|
|
3901
3117
|
function git(repoDir, args) {
|
|
@@ -3934,8 +3150,8 @@ function assertNoGitlinkChanges(repoDir, base, head, uncommitted) {
|
|
|
3934
3150
|
const newMode = fields[1];
|
|
3935
3151
|
if (oldMode === GITLINK_MODE || newMode === GITLINK_MODE) offenders.push(path);
|
|
3936
3152
|
}
|
|
3937
|
-
const refusal =
|
|
3938
|
-
if (refusal !== null) throw new
|
|
3153
|
+
const refusal = refusalFromFacts({ kind: "submodule-change", offenders });
|
|
3154
|
+
if (refusal !== null) throw new DeclaredRefusalError(refusal);
|
|
3939
3155
|
}
|
|
3940
3156
|
function gitAllowFail(repoDir, args) {
|
|
3941
3157
|
try {
|
|
@@ -5177,7 +4393,7 @@ function splitAtRunnerStageInWrapper(testScript, runner, scripts) {
|
|
|
5177
4393
|
if (/[;&|<>`]|\$\(/u.test(script)) return null;
|
|
5178
4394
|
let argv;
|
|
5179
4395
|
try {
|
|
5180
|
-
argv =
|
|
4396
|
+
argv = parseLiteralArguments(script, "npm-run-all test script");
|
|
5181
4397
|
} catch {
|
|
5182
4398
|
return null;
|
|
5183
4399
|
}
|
|
@@ -5208,7 +4424,7 @@ function splitAtRunnerStageInWrapper(testScript, runner, scripts) {
|
|
|
5208
4424
|
if (leaf === void 0 || leaf.trim() === "") return null;
|
|
5209
4425
|
let leafArgv;
|
|
5210
4426
|
try {
|
|
5211
|
-
leafArgv =
|
|
4427
|
+
leafArgv = parseLiteralArguments(leaf, `wrapper child ${name}`);
|
|
5212
4428
|
} catch {
|
|
5213
4429
|
leafArgv = [];
|
|
5214
4430
|
}
|
|
@@ -5301,7 +4517,7 @@ function jasmineSuiteFromScript(testScript) {
|
|
|
5301
4517
|
for (const stage of stages) {
|
|
5302
4518
|
let tokens;
|
|
5303
4519
|
try {
|
|
5304
|
-
tokens =
|
|
4520
|
+
tokens = parseLiteralArguments(stage, "test script");
|
|
5305
4521
|
} catch {
|
|
5306
4522
|
return void 0;
|
|
5307
4523
|
}
|
|
@@ -5406,6 +4622,7 @@ function classifyPackageManifest(pkg, aggregatorTasks = []) {
|
|
|
5406
4622
|
...adopted,
|
|
5407
4623
|
scripts,
|
|
5408
4624
|
aggregatorTasks: [],
|
|
4625
|
+
aggregatorInvocationScript: null,
|
|
5409
4626
|
aggregatorTask: taskSuite.task,
|
|
5410
4627
|
aggregatorTaskRefusal: null
|
|
5411
4628
|
};
|
|
@@ -5424,6 +4641,7 @@ function classifyPackageManifest(pkg, aggregatorTasks = []) {
|
|
|
5424
4641
|
scripts,
|
|
5425
4642
|
aggregator: aggregated?.tool ?? null,
|
|
5426
4643
|
aggregatorTasks: aggregated?.tasks ?? [],
|
|
4644
|
+
aggregatorInvocationScript: aggregated === null ? null : testScript,
|
|
5427
4645
|
aggregatorTask: null,
|
|
5428
4646
|
aggregatorTaskRefusal: taskSuite === null ? null : taskSuite.refusal
|
|
5429
4647
|
};
|
|
@@ -5446,7 +4664,12 @@ function classifyWithAliasHop(manifest, family, aggregatorTasks = []) {
|
|
|
5446
4664
|
if (typeof inner !== "string") return base;
|
|
5447
4665
|
const hopped = classifyPackageManifest({ ...manifest, scripts: { ...base.scripts, test: inner } });
|
|
5448
4666
|
if (hopped.runner !== null) return hopped;
|
|
5449
|
-
return hopped.aggregator === null ? base : {
|
|
4667
|
+
return hopped.aggregator === null ? base : {
|
|
4668
|
+
...base,
|
|
4669
|
+
aggregator: hopped.aggregator,
|
|
4670
|
+
aggregatorTasks: hopped.aggregatorTasks,
|
|
4671
|
+
aggregatorInvocationScript: hopped.aggregatorInvocationScript
|
|
4672
|
+
};
|
|
5450
4673
|
}
|
|
5451
4674
|
function probeNxProject(ctx, repoDir, dir) {
|
|
5452
4675
|
const text = accessText(ctx, join8(dir, "project.json"));
|
|
@@ -5542,658 +4765,1658 @@ function probePkg(ctx, dir, aliasFamily = null, repoDir = dir, aggregatorTasks =
|
|
|
5542
4765
|
refusal: null
|
|
5543
4766
|
};
|
|
5544
4767
|
}
|
|
5545
|
-
function probeManifest(ctx, dir, manifest, aliasFamily, repoDir, aggregatorTasks) {
|
|
5546
|
-
if (manifest === null) {
|
|
5547
|
-
const nx = probeNxProject(ctx, repoDir, dir);
|
|
5548
|
-
if (nx !== null) {
|
|
5549
|
-
return {
|
|
5550
|
-
dir,
|
|
5551
|
-
runner: nx.runner,
|
|
5552
|
-
testScript: null,
|
|
5553
|
-
note: null,
|
|
5554
|
-
scripts: {},
|
|
5555
|
-
aggregator: null,
|
|
5556
|
-
aggregatorTasks: [],
|
|
5557
|
-
aggregatorTask: null,
|
|
5558
|
-
angular: null,
|
|
5559
|
-
declaredCommand: nx.command,
|
|
5560
|
-
workspaceRootSuite: nx.workspaceRootSuite
|
|
5561
|
-
};
|
|
4768
|
+
function probeManifest(ctx, dir, manifest, aliasFamily, repoDir, aggregatorTasks) {
|
|
4769
|
+
if (manifest === null) {
|
|
4770
|
+
const nx = probeNxProject(ctx, repoDir, dir);
|
|
4771
|
+
if (nx !== null) {
|
|
4772
|
+
return {
|
|
4773
|
+
dir,
|
|
4774
|
+
runner: nx.runner,
|
|
4775
|
+
testScript: null,
|
|
4776
|
+
note: null,
|
|
4777
|
+
scripts: {},
|
|
4778
|
+
aggregator: null,
|
|
4779
|
+
aggregatorTasks: [],
|
|
4780
|
+
aggregatorTask: null,
|
|
4781
|
+
angular: null,
|
|
4782
|
+
declaredCommand: nx.command,
|
|
4783
|
+
workspaceRootSuite: nx.workspaceRootSuite
|
|
4784
|
+
};
|
|
4785
|
+
}
|
|
4786
|
+
return probeDenoProject(ctx, dir);
|
|
4787
|
+
}
|
|
4788
|
+
const pkg = manifest;
|
|
4789
|
+
const classified = classifyWithAliasHop(pkg, aliasFamily, aggregatorTasks);
|
|
4790
|
+
if (classified.aggregatorTask !== null && classified.testScript !== null) {
|
|
4791
|
+
return {
|
|
4792
|
+
dir,
|
|
4793
|
+
...classified,
|
|
4794
|
+
angular: null,
|
|
4795
|
+
declaredCommand: aggregatorTaskCommand(
|
|
4796
|
+
classified.testScript,
|
|
4797
|
+
classified.runner,
|
|
4798
|
+
detectModuleLayout(repoDir, dir)
|
|
4799
|
+
),
|
|
4800
|
+
workspaceRootSuite: null
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
if (classified.runner === null && scriptRunsDenoTest(classified.testScript)) {
|
|
4804
|
+
return {
|
|
4805
|
+
dir,
|
|
4806
|
+
...classified,
|
|
4807
|
+
runner: "deno",
|
|
4808
|
+
note: "deno",
|
|
4809
|
+
angular: null,
|
|
4810
|
+
declaredCommand: runnerInvocation(classified.testScript, "deno"),
|
|
4811
|
+
workspaceRootSuite: null
|
|
4812
|
+
};
|
|
4813
|
+
}
|
|
4814
|
+
if (classified.runner === null && classified.testScript === null) {
|
|
4815
|
+
const nx = probeNxProject(ctx, repoDir, dir);
|
|
4816
|
+
if (nx !== null) {
|
|
4817
|
+
return {
|
|
4818
|
+
dir,
|
|
4819
|
+
...classified,
|
|
4820
|
+
runner: nx.runner,
|
|
4821
|
+
angular: null,
|
|
4822
|
+
declaredCommand: nx.command,
|
|
4823
|
+
workspaceRootSuite: nx.workspaceRootSuite
|
|
4824
|
+
};
|
|
4825
|
+
}
|
|
4826
|
+
const rush = probeRushProject(ctx, repoDir, dir);
|
|
4827
|
+
if (rush !== null) return rush;
|
|
4828
|
+
const deno = probeDenoProject(ctx, dir);
|
|
4829
|
+
if (deno !== null) return deno;
|
|
4830
|
+
}
|
|
4831
|
+
const angular = probeAngular(ctx, dir, classified);
|
|
4832
|
+
if (angular !== null) {
|
|
4833
|
+
return {
|
|
4834
|
+
dir,
|
|
4835
|
+
...classified,
|
|
4836
|
+
runner: angularRunnerName(angular.runner),
|
|
4837
|
+
angular,
|
|
4838
|
+
note: `angular:${angular.builder}`,
|
|
4839
|
+
declaredCommand: null,
|
|
4840
|
+
workspaceRootSuite: null
|
|
4841
|
+
};
|
|
4842
|
+
}
|
|
4843
|
+
return {
|
|
4844
|
+
dir,
|
|
4845
|
+
...classified,
|
|
4846
|
+
angular: null,
|
|
4847
|
+
declaredCommand: null,
|
|
4848
|
+
workspaceRootSuite: null
|
|
4849
|
+
};
|
|
4850
|
+
}
|
|
4851
|
+
function classifyPackageDirectory(repoDir, dir, ctx = diskRepoAccess(repoDir)) {
|
|
4852
|
+
const reading = accessManifest(ctx, join8(repoDir, dir, "package.json"));
|
|
4853
|
+
if (reading.standing !== "read") return null;
|
|
4854
|
+
try {
|
|
4855
|
+
return classifyWithAliasHop(
|
|
4856
|
+
reading.manifest,
|
|
4857
|
+
rootAliasFamily(ctx, repoDir),
|
|
4858
|
+
rootAggregatorTasksInRepository(ctx, repoDir)
|
|
4859
|
+
);
|
|
4860
|
+
} catch {
|
|
4861
|
+
return null;
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4864
|
+
function declaredCommandContext(repoDir, subdir, ctx = diskRepoAccess(repoDir)) {
|
|
4865
|
+
const packageReading = accessManifest(ctx, join8(repoDir, subdir ?? ".", "package.json"));
|
|
4866
|
+
const scripts = packageReading.standing === "read" ? manifestScripts3(packageReading.manifest) : {};
|
|
4867
|
+
const rootReading = accessManifest(ctx, join8(repoDir, "package.json"));
|
|
4868
|
+
const manifest = rootReading.standing === "read" ? rootReading.manifest : void 0;
|
|
4869
|
+
return { scripts, ...manifest === void 0 ? {} : { manifest } };
|
|
4870
|
+
}
|
|
4871
|
+
function rootAliasFamily(ctx, repoDir) {
|
|
4872
|
+
return withRootManifest(ctx, repoDir, aliasHopFamily, null);
|
|
4873
|
+
}
|
|
4874
|
+
function rootAggregatorTasksInRepository(ctx, repoDir) {
|
|
4875
|
+
return withRootManifest(ctx, repoDir, rootAggregatorTasks, []);
|
|
4876
|
+
}
|
|
4877
|
+
function withRootManifest(ctx, repoDir, read, absent) {
|
|
4878
|
+
const reading = accessManifest(ctx, join8(repoDir, "package.json"));
|
|
4879
|
+
if (reading.standing !== "read") return absent;
|
|
4880
|
+
try {
|
|
4881
|
+
return read(reading.manifest);
|
|
4882
|
+
} catch {
|
|
4883
|
+
return absent;
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
var MAX_FANOUT_CHILDREN = 16;
|
|
4887
|
+
var NATIVE_COVERAGE_RUNNERS = /* @__PURE__ */ new Set([
|
|
4888
|
+
"jest",
|
|
4889
|
+
"vitest",
|
|
4890
|
+
"mocha",
|
|
4891
|
+
"node-test",
|
|
4892
|
+
"ava",
|
|
4893
|
+
"tap",
|
|
4894
|
+
"jasmine"
|
|
4895
|
+
]);
|
|
4896
|
+
function corepackManaged(manager) {
|
|
4897
|
+
return manager.version !== null && (manager.pm === "npm" || manager.pm === "pnpm" || manager.pm === "yarn");
|
|
4898
|
+
}
|
|
4899
|
+
function packageScriptCommand(manager, script) {
|
|
4900
|
+
return manager.pm === "bun" ? `bun run ${script}` : manager.pm === "deno" ? `deno task ${script}` : corepackManaged(manager) ? `corepack ${manager.pm} run ${script}` : `${manager.pm} run ${script}`;
|
|
4901
|
+
}
|
|
4902
|
+
function packageTestCommand(manager) {
|
|
4903
|
+
return manager.pm === "bun" ? "bun run test" : manager.pm === "deno" ? "deno task test" : corepackManaged(manager) ? `corepack ${manager.pm} test` : manager.pm === "npm" ? "npm test" : `${manager.pm} test`;
|
|
4904
|
+
}
|
|
4905
|
+
function scriptPattern(pattern) {
|
|
4906
|
+
const escaped = pattern.replace(/[\\^$+.[\]{}()|]/gu, "\\$&");
|
|
4907
|
+
return new RegExp(`^${escaped.replace(/\*/gu, "[^:]*").replace(/\?/gu, "[^:]")}$`, "u");
|
|
4908
|
+
}
|
|
4909
|
+
var WRAPPERS = ["npm-run-all", "run-p", "run-s"];
|
|
4910
|
+
var WRAPPER_SCHEDULING_OPTIONS = /^(?:-[aclps]+|--parallel|--sequential|--continue-on-error|--silent|--aggregate-output|--print-label)$/u;
|
|
4911
|
+
function leafRunner(leaf) {
|
|
4912
|
+
const named = runnerNamesInScript(leaf);
|
|
4913
|
+
const runner = named.length === 1 ? named[0] : invokesNodeTest(leaf) ? "node-test" : null;
|
|
4914
|
+
if (runner === null || !NATIVE_COVERAGE_RUNNERS.has(runner)) return null;
|
|
4915
|
+
return runnerSuiteInvocations(leaf, runner) > 1 ? null : runner;
|
|
4916
|
+
}
|
|
4917
|
+
function wrapperFanout(probe, manager) {
|
|
4918
|
+
const script = probe.testScript;
|
|
4919
|
+
if (script === null) return void 0;
|
|
4920
|
+
const executable = script.trim().split(/\s+/u)[0]?.replace(/^.*\//u, "");
|
|
4921
|
+
if (!WRAPPERS.includes(executable)) return void 0;
|
|
4922
|
+
if (/[;&|<>`]|\$\(/u.test(script)) return { refusal: "it composes shell commands around the wrapper" };
|
|
4923
|
+
let argv;
|
|
4924
|
+
try {
|
|
4925
|
+
argv = parseLiteralArguments(script, "npm-run-all test script");
|
|
4926
|
+
} catch {
|
|
4927
|
+
return { refusal: "its wrapper invocation could not be parsed as one literal command" };
|
|
4928
|
+
}
|
|
4929
|
+
const patterns = [];
|
|
4930
|
+
let parallel = executable === "run-p";
|
|
4931
|
+
let sequential = executable === "run-s";
|
|
4932
|
+
for (const arg of argv.slice(1)) {
|
|
4933
|
+
if (arg.startsWith("-")) {
|
|
4934
|
+
if (!WRAPPER_SCHEDULING_OPTIONS.test(arg)) {
|
|
4935
|
+
return { refusal: `the wrapper option ${arg} can change which suites run to completion` };
|
|
4936
|
+
}
|
|
4937
|
+
if (arg === "--parallel" || /^-[aclps]+$/u.test(arg) && arg.includes("p")) parallel = true;
|
|
4938
|
+
if (arg === "--sequential" || /^-[aclps]+$/u.test(arg) && arg.includes("s")) sequential = true;
|
|
4939
|
+
continue;
|
|
4940
|
+
}
|
|
4941
|
+
patterns.push(arg);
|
|
4942
|
+
}
|
|
4943
|
+
if (patterns.length === 0) return { refusal: "its wrapper selects no scripts" };
|
|
4944
|
+
if (parallel && sequential) return { refusal: "its wrapper asks for parallel and sequential at once" };
|
|
4945
|
+
const selected = /* @__PURE__ */ new Set();
|
|
4946
|
+
const names = Object.keys(probe.scripts).sort();
|
|
4947
|
+
for (const pattern of patterns) {
|
|
4948
|
+
const matches = names.filter((name) => name !== "test" && scriptPattern(pattern).test(name));
|
|
4949
|
+
if (matches.length === 0) return { refusal: `no script matches the pattern ${pattern}` };
|
|
4950
|
+
for (const match of matches) selected.add(match);
|
|
4951
|
+
}
|
|
4952
|
+
const children = scriptChildren([...selected].sort(), probe, manager);
|
|
4953
|
+
return "refusal" in children ? children : { children: children.children, execution: parallel ? "parallel" : "sequential" };
|
|
4954
|
+
}
|
|
4955
|
+
function concurrentlyFanout(probe, manager) {
|
|
4956
|
+
const script = probe.testScript;
|
|
4957
|
+
if (script === null) return void 0;
|
|
4958
|
+
if (script.trim().split(/\s+/u)[0]?.replace(/^.*\//u, "") !== "concurrently") return void 0;
|
|
4959
|
+
if (/[;&|<>`]|\$\(/u.test(script)) return { refusal: "it composes shell commands around concurrently" };
|
|
4960
|
+
let argv;
|
|
4961
|
+
try {
|
|
4962
|
+
argv = parseLiteralArguments(script, "concurrently test script");
|
|
4963
|
+
} catch {
|
|
4964
|
+
return { refusal: "its concurrently invocation could not be parsed as one literal command" };
|
|
4965
|
+
}
|
|
4966
|
+
const patterns = [];
|
|
4967
|
+
for (const arg of argv.slice(1)) {
|
|
4968
|
+
if (arg.startsWith("-")) {
|
|
4969
|
+
if (!/^(?:--(?:names|prefix|prefix-colors|group|raw)|-[np]|-c)$/u.test(arg)) {
|
|
4970
|
+
return { refusal: `the concurrently option ${arg} can change which suites run to completion` };
|
|
4971
|
+
}
|
|
4972
|
+
continue;
|
|
4973
|
+
}
|
|
4974
|
+
if (!arg.startsWith("npm:")) {
|
|
4975
|
+
return { refusal: "concurrently is given a raw command line rather than an npm: script reference" };
|
|
4976
|
+
}
|
|
4977
|
+
patterns.push(arg.slice("npm:".length));
|
|
4978
|
+
}
|
|
4979
|
+
if (patterns.length === 0) return { refusal: "its concurrently invocation selects no scripts" };
|
|
4980
|
+
const selected = /* @__PURE__ */ new Set();
|
|
4981
|
+
const names = Object.keys(probe.scripts).sort();
|
|
4982
|
+
for (const pattern of patterns) {
|
|
4983
|
+
const matches = names.filter((name) => name !== "test" && scriptPattern(pattern).test(name));
|
|
4984
|
+
if (matches.length === 0) return { refusal: `no script matches the pattern ${pattern}` };
|
|
4985
|
+
for (const match of matches) selected.add(match);
|
|
4986
|
+
}
|
|
4987
|
+
const children = scriptChildren([...selected].sort(), probe, manager);
|
|
4988
|
+
return "refusal" in children ? children : { children: children.children, execution: "parallel" };
|
|
4989
|
+
}
|
|
4990
|
+
function scriptChildren(scripts, probe, manager) {
|
|
4991
|
+
if (scripts.length === 0) return { refusal: "it selects no scripts" };
|
|
4992
|
+
if (scripts.length > MAX_FANOUT_CHILDREN) {
|
|
4993
|
+
return { refusal: `it launches ${scripts.length} suites, more than the ${MAX_FANOUT_CHILDREN} this run will measure` };
|
|
4994
|
+
}
|
|
4995
|
+
const children = [];
|
|
4996
|
+
for (const script of scripts) {
|
|
4997
|
+
if (!/^[A-Za-z0-9:_-]{1,128}$/u.test(script)) return { refusal: `the script name ${script} is not a plain name` };
|
|
4998
|
+
const leaf = probe.scripts[script];
|
|
4999
|
+
if (leaf === void 0 || /[;&|<>`]|\$\(/u.test(leaf)) {
|
|
5000
|
+
return { refusal: `the script ${script} composes shell commands rather than running one suite` };
|
|
5001
|
+
}
|
|
5002
|
+
let leafArgv;
|
|
5003
|
+
try {
|
|
5004
|
+
leafArgv = parseLiteralArguments(leaf, `fanout child ${script}`);
|
|
5005
|
+
} catch {
|
|
5006
|
+
return { refusal: `the script ${script} could not be parsed as one literal command` };
|
|
5007
|
+
}
|
|
5008
|
+
if (leafArgv.some((arg) => WRAPPERS.includes(arg))) {
|
|
5009
|
+
return { refusal: `the script ${script} is itself a wrapper, and this expands one level only` };
|
|
5010
|
+
}
|
|
5011
|
+
const runner = leafRunner(leaf);
|
|
5012
|
+
if (runner === null) return { refusal: `the script ${script} names no single runner with native coverage` };
|
|
5013
|
+
children.push({ script, runner, testCommand: packageScriptCommand(manager, script) });
|
|
5014
|
+
}
|
|
5015
|
+
return { children };
|
|
5016
|
+
}
|
|
5017
|
+
function chainFanout(probe, manager) {
|
|
5018
|
+
const script = probe.testScript;
|
|
5019
|
+
if (script === null || script.trim() === "") return void 0;
|
|
5020
|
+
if (suiteStages(script, probe.scripts) < 2) return void 0;
|
|
5021
|
+
if (/[|<>`]|\$\(/u.test(script) || /(^|[^&])&([^&]|$)/u.test(script)) {
|
|
5022
|
+
return { refusal: "its suites are composed with something other than `&&`, so which of them ran is not decidable" };
|
|
5023
|
+
}
|
|
5024
|
+
if (script.includes(";")) {
|
|
5025
|
+
return { refusal: "its suites are separated by `;`, which runs the next one whether or not the last passed" };
|
|
5026
|
+
}
|
|
5027
|
+
const chainStages = script.split("&&").map((stage) => stage.trim());
|
|
5028
|
+
if (chainStages.some((stage) => stage === "")) return { refusal: "its `&&` chain has an empty stage" };
|
|
5029
|
+
if (chainStages.length > MAX_FANOUT_CHILDREN) {
|
|
5030
|
+
return { refusal: `it chains ${chainStages.length} stages, more than the ${MAX_FANOUT_CHILDREN} this run will measure` };
|
|
5031
|
+
}
|
|
5032
|
+
const children = [];
|
|
5033
|
+
for (const stage of chainStages) {
|
|
5034
|
+
if (suiteStages(stage, probe.scripts) === 0) continue;
|
|
5035
|
+
const alias = /^(?:corepack\s+)?(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?([A-Za-z0-9:._-]+)$/u.exec(stage);
|
|
5036
|
+
if (alias !== null) {
|
|
5037
|
+
const family = stage.trim().replace(/^corepack\s+/u, "").split(/\s+/u)[0];
|
|
5038
|
+
if (family !== manager.pm) {
|
|
5039
|
+
return { refusal: `the stage \`${stage}\` runs under ${family} while this package resolves to ${manager.pm}` };
|
|
5040
|
+
}
|
|
5041
|
+
const named = scriptChildren([alias[1]], probe, manager);
|
|
5042
|
+
if ("refusal" in named) return named;
|
|
5043
|
+
children.push(...named.children);
|
|
5044
|
+
continue;
|
|
5562
5045
|
}
|
|
5563
|
-
|
|
5046
|
+
const runner = [...NATIVE_COVERAGE_RUNNERS].find((candidate) => stageInvokes(stage, candidate));
|
|
5047
|
+
if (runner === void 0) {
|
|
5048
|
+
return { refusal: `the stage \`${stage}\` is neither a suite nor a script this run can name` };
|
|
5049
|
+
}
|
|
5050
|
+
const testCommand = runner === "node-test" ? stage : `npx --no-install ${stage}`;
|
|
5051
|
+
try {
|
|
5052
|
+
parsePreparedTestCommand(testCommand, "fanout stage");
|
|
5053
|
+
} catch {
|
|
5054
|
+
return { refusal: `the stage \`${stage}\` is not a literal command this run can execute on its own` };
|
|
5055
|
+
}
|
|
5056
|
+
children.push({ runner, testCommand });
|
|
5564
5057
|
}
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5058
|
+
if (children.length < 2) return void 0;
|
|
5059
|
+
return { children, execution: "sequential" };
|
|
5060
|
+
}
|
|
5061
|
+
function suiteFanoutPlan(probe, manager) {
|
|
5062
|
+
const aggregated = probe.aggregator === null ? null : { tool: probe.aggregator, tasks: [...probe.aggregatorTasks] };
|
|
5063
|
+
if (aggregated !== null) {
|
|
5568
5064
|
return {
|
|
5569
|
-
|
|
5570
|
-
...classified,
|
|
5571
|
-
angular: null,
|
|
5572
|
-
declaredCommand: aggregatorTaskCommand(
|
|
5573
|
-
classified.testScript,
|
|
5574
|
-
classified.runner,
|
|
5575
|
-
detectModuleLayout(repoDir, dir)
|
|
5576
|
-
),
|
|
5577
|
-
workspaceRootSuite: null
|
|
5065
|
+
coverageExclusion: `this package's test script delegates to ${aggregated.tool}, which runs ${aggregatorTaskPhrase(aggregated)} rather than one suite, so no single coverage report describes it; abloh measures such a repository per package instead`
|
|
5578
5066
|
};
|
|
5579
5067
|
}
|
|
5580
|
-
|
|
5068
|
+
const resolved = wrapperFanout(probe, manager) ?? concurrentlyFanout(probe, manager) ?? chainFanout(probe, manager);
|
|
5069
|
+
if (resolved === void 0) return {};
|
|
5070
|
+
if ("refusal" in resolved) {
|
|
5071
|
+
return { coverageExclusion: `this package's test script launches more than one suite, but ${resolved.refusal}` };
|
|
5072
|
+
}
|
|
5073
|
+
const runners = [...new Set(resolved.children.map((child) => child.runner))].sort();
|
|
5074
|
+
if (runners.length > 1) {
|
|
5581
5075
|
return {
|
|
5582
|
-
|
|
5583
|
-
...classified,
|
|
5584
|
-
runner: "deno",
|
|
5585
|
-
note: "deno",
|
|
5586
|
-
angular: null,
|
|
5587
|
-
declaredCommand: runnerInvocation(classified.testScript, "deno"),
|
|
5588
|
-
workspaceRootSuite: null
|
|
5076
|
+
coverageExclusion: `this package's test script launches ${resolved.children.length} suites across ${runners.join(" and ")}, and one coverage report cannot carry two providers, so none of them is measured rather than some`
|
|
5589
5077
|
};
|
|
5590
5078
|
}
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5079
|
+
return { coverageChildren: resolved.children, coverageExecution: resolved.execution };
|
|
5080
|
+
}
|
|
5081
|
+
function compiledOutputFields(input) {
|
|
5082
|
+
if (input.angular || input.runner === "deno") return {};
|
|
5083
|
+
const reading = readsCompiledOutput({
|
|
5084
|
+
/*
|
|
5085
|
+
* THE ONE READING ON THIS PATH THAT DOES NOT GO THROUGH THE REPOSITORY READER, and it is a
|
|
5086
|
+
* decision rather than an oversight.
|
|
5087
|
+
*
|
|
5088
|
+
* `readsCompiledOutput` FOLLOWS `extends` INTO `node_modules` by design - rushstack's projects
|
|
5089
|
+
* extend `@rushstack/heft-node-rig/profiles/default/tsconfig-base.json`, whose `outDir` is five
|
|
5090
|
+
* hops out of the store and back into the project's own `lib`, and refusing to follow it means
|
|
5091
|
+
* this reading never sees the repository the compile-then-test ruling was measured on. An
|
|
5092
|
+
* installed store is exactly where symlinks live and exactly where "inside the checkout" is a
|
|
5093
|
+
* different question, so the reader's two refusals - a symlinked leaf, and a parent that
|
|
5094
|
+
* resolves outside the root - would silently narrow it. What that costs is not a refusal: a
|
|
5095
|
+
* config this reading cannot see produces a different `rebuildCommand`, and a wrong rebuild is
|
|
5096
|
+
* the one failure in this product that ends in a confident WRONG NUMBER.
|
|
5097
|
+
*
|
|
5098
|
+
* IT IS SAFE HERE FOR THE REASON THE READER EXISTS ELSEWHERE. The paths are composed by
|
|
5099
|
+
* `compiled-output.ts` from a config's own `extends` and from `node_modules` walked upwards,
|
|
5100
|
+
* never from a repository-author-controlled string this module passes through; and the only
|
|
5101
|
+
* road that reaches it is one that HAS a checkout, since a service reading over the contents
|
|
5102
|
+
* API has no installed store to follow into anyway.
|
|
5103
|
+
*/
|
|
5104
|
+
readFile: (path) => {
|
|
5105
|
+
const absolute = join8(input.repoDir, ...path.split("/"));
|
|
5106
|
+
if (!existsSync2(absolute)) return null;
|
|
5107
|
+
try {
|
|
5108
|
+
return readFileSync5(absolute, "utf8");
|
|
5109
|
+
} catch {
|
|
5110
|
+
return null;
|
|
5111
|
+
}
|
|
5112
|
+
},
|
|
5113
|
+
packageDir: input.packageDir ?? "",
|
|
5114
|
+
runner: input.runner,
|
|
5115
|
+
invocation: input.invocation,
|
|
5116
|
+
/* THE COMMAND THIS RUN EXECUTES, beside the invocation and never instead of it. `runnerInvocation`
|
|
5117
|
+
answers a narrower question - whether a stage can be RE-EMITTED - so it declines a stage with
|
|
5118
|
+
anything in front of the runner, and `devcontainers/cli`'s whole suite is
|
|
5119
|
+
`env TS_NODE_PROJECT=... mocha ... src/test/*.test.ts`. The reading in `@abloh/core` resolves the
|
|
5120
|
+
runner's stage out of this where no invocation arrived; its own header states both halves. */
|
|
5121
|
+
testCommand: input.testCommand,
|
|
5122
|
+
scripts: input.scripts,
|
|
5123
|
+
manager: input.manager,
|
|
5124
|
+
/*
|
|
5125
|
+
* THE SECOND READING'S SEAM IS THE REPOSITORY READER and never the raw filesystem one above.
|
|
5126
|
+
*
|
|
5127
|
+
* That one exists to follow a `tsconfig` `extends` into an installed store, which is the one
|
|
5128
|
+
* place this detection legitimately leaves the tree. This reading opens the SUITE'S OWN TEST
|
|
5129
|
+
* FILES, which are the repository's, so it takes the reader whose refusals are about staying
|
|
5130
|
+
* inside it - a symlinked leaf and a parent resolving out of the root.
|
|
5131
|
+
*/
|
|
5132
|
+
reader: input.ctx.reader
|
|
5133
|
+
});
|
|
5134
|
+
const found = reading.state === "reads-compiled-output" ? reading : null;
|
|
5135
|
+
return {
|
|
5136
|
+
rebuildCommand: found?.rebuildCommand ?? void 0,
|
|
5137
|
+
rebuildEvidence: found === null ? void 0 : compiledOutputEvidence(found),
|
|
5138
|
+
rebuildCandidates: found === null || found.candidates.length === 0 ? void 0 : found.candidates,
|
|
5139
|
+
rebuildOutputDirectory: found?.outputDirectory ?? void 0
|
|
5140
|
+
};
|
|
5141
|
+
}
|
|
5142
|
+
function repositoryRootOf(detected) {
|
|
5143
|
+
if (detected.subdir === null || detected.workspaceRootSuite !== void 0) return detected.workDir;
|
|
5144
|
+
return resolve6(detected.workDir, ...detected.subdir.split("/").map(() => ".."));
|
|
5145
|
+
}
|
|
5146
|
+
var VITEST_CONFIG_FILES = [
|
|
5147
|
+
"vitest.config.ts",
|
|
5148
|
+
"vitest.config.mts",
|
|
5149
|
+
"vitest.config.cts",
|
|
5150
|
+
"vitest.config.js",
|
|
5151
|
+
"vitest.config.mjs",
|
|
5152
|
+
"vitest.config.cjs",
|
|
5153
|
+
"vite.config.ts",
|
|
5154
|
+
"vite.config.mts",
|
|
5155
|
+
"vite.config.cts",
|
|
5156
|
+
"vite.config.js",
|
|
5157
|
+
"vite.config.mjs",
|
|
5158
|
+
"vite.config.cjs"
|
|
5159
|
+
];
|
|
5160
|
+
function detectVitestDir(dir, ctx = diskRepoAccess(dir)) {
|
|
5161
|
+
for (const name of VITEST_CONFIG_FILES) {
|
|
5162
|
+
const p = join8(dir, name);
|
|
5163
|
+
let src;
|
|
5164
|
+
try {
|
|
5165
|
+
src = accessText(ctx, p);
|
|
5166
|
+
} catch (e) {
|
|
5167
|
+
debugPreparationOverflowStack(e);
|
|
5168
|
+
throw new TargetDetectionError(
|
|
5169
|
+
`${name} at ${dir} could not be read: ${e instanceof Error ? e.message : "unreadable"}`
|
|
5170
|
+
);
|
|
5602
5171
|
}
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
const deno = probeDenoProject(ctx, dir);
|
|
5606
|
-
if (deno !== null) return deno;
|
|
5172
|
+
if (src === null) continue;
|
|
5173
|
+
return vitestDirIn(src);
|
|
5607
5174
|
}
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
}
|
|
5175
|
+
return void 0;
|
|
5176
|
+
}
|
|
5177
|
+
function vitestDirIn(source) {
|
|
5178
|
+
const masked = maskLiterals(source);
|
|
5179
|
+
const open = firstUnmasked(source, masked, /(?:^|[\s,;{])["']?test["']?\s*:\s*\{/gu);
|
|
5180
|
+
if (open === -1) return void 0;
|
|
5181
|
+
let depth = 0;
|
|
5182
|
+
let close = -1;
|
|
5183
|
+
for (let at = open; at < masked.length; at++) {
|
|
5184
|
+
if (masked[at] === "{") depth += 1;
|
|
5185
|
+
else if (masked[at] === "}") {
|
|
5186
|
+
depth -= 1;
|
|
5187
|
+
if (depth === 0) {
|
|
5188
|
+
close = at;
|
|
5189
|
+
break;
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
if (close === -1) return void 0;
|
|
5194
|
+
const quote = firstUnmasked(source.slice(0, close), masked.slice(0, close), /\bdir["']?\s*:\s*['"`]/gu, open);
|
|
5195
|
+
if (quote === -1) return void 0;
|
|
5196
|
+
const end = masked.indexOf(source[quote], quote + 1);
|
|
5197
|
+
if (end === -1 || end >= close) return void 0;
|
|
5198
|
+
const value = source.slice(quote + 1, end);
|
|
5199
|
+
return value === "" ? void 0 : value;
|
|
5200
|
+
}
|
|
5201
|
+
function firstUnmasked(source, masked, pattern, from = 0) {
|
|
5202
|
+
pattern.lastIndex = from;
|
|
5203
|
+
for (let match = pattern.exec(source); match !== null; match = pattern.exec(source)) {
|
|
5204
|
+
const at = match.index + match[0].length - 1;
|
|
5205
|
+
if (masked[at] === source[at]) return at;
|
|
5619
5206
|
}
|
|
5620
|
-
return
|
|
5621
|
-
dir,
|
|
5622
|
-
...classified,
|
|
5623
|
-
angular: null,
|
|
5624
|
-
declaredCommand: null,
|
|
5625
|
-
workspaceRootSuite: null
|
|
5626
|
-
};
|
|
5207
|
+
return -1;
|
|
5627
5208
|
}
|
|
5628
|
-
function
|
|
5629
|
-
const
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5209
|
+
function maskLiterals(source) {
|
|
5210
|
+
const out = source.split("");
|
|
5211
|
+
const blank = (from, to) => {
|
|
5212
|
+
for (let at2 = from; at2 < to && at2 < out.length; at2++) if (out[at2] !== "\n") out[at2] = " ";
|
|
5213
|
+
};
|
|
5214
|
+
let at = 0;
|
|
5215
|
+
let previous = "";
|
|
5216
|
+
while (at < source.length) {
|
|
5217
|
+
const char = source[at];
|
|
5218
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
5219
|
+
let end = at + 1;
|
|
5220
|
+
while (end < source.length && source[end] !== char) end += source[end] === "\\" ? 2 : 1;
|
|
5221
|
+
blank(at + 1, end);
|
|
5222
|
+
at = Math.min(end + 1, source.length);
|
|
5223
|
+
previous = char;
|
|
5224
|
+
continue;
|
|
5225
|
+
}
|
|
5226
|
+
if (char === "/" && source[at + 1] === "/") {
|
|
5227
|
+
const end = source.indexOf("\n", at);
|
|
5228
|
+
blank(at, end === -1 ? source.length : end);
|
|
5229
|
+
at = end === -1 ? source.length : end;
|
|
5230
|
+
continue;
|
|
5231
|
+
}
|
|
5232
|
+
if (char === "/" && source[at + 1] === "*") {
|
|
5233
|
+
const end = source.indexOf("*/", at + 2);
|
|
5234
|
+
blank(at, end === -1 ? source.length : end + 2);
|
|
5235
|
+
at = end === -1 ? source.length : end + 2;
|
|
5236
|
+
continue;
|
|
5237
|
+
}
|
|
5238
|
+
if (char === "/" && previous !== "" && /[(,=:[!&|?{};+\-*]/u.test(previous)) {
|
|
5239
|
+
let end = at + 1;
|
|
5240
|
+
while (end < source.length && source[end] !== "/" && source[end] !== "\n") end += source[end] === "\\" ? 2 : 1;
|
|
5241
|
+
if (source[end] === "/") {
|
|
5242
|
+
blank(at, end + 1);
|
|
5243
|
+
at = end + 1;
|
|
5244
|
+
continue;
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
if (!/\s/u.test(char)) previous = char;
|
|
5248
|
+
at += 1;
|
|
5639
5249
|
}
|
|
5250
|
+
return out.join("");
|
|
5640
5251
|
}
|
|
5641
|
-
function
|
|
5642
|
-
|
|
5643
|
-
const scripts = packageReading.standing === "read" ? manifestScripts3(packageReading.manifest) : {};
|
|
5644
|
-
const rootReading = accessManifest(ctx, join8(repoDir, "package.json"));
|
|
5645
|
-
const manifest = rootReading.standing === "read" ? rootReading.manifest : void 0;
|
|
5646
|
-
return { scripts, ...manifest === void 0 ? {} : { manifest } };
|
|
5252
|
+
function workDirPrefix(detection) {
|
|
5253
|
+
return detection.workspaceRootSuite === void 0 ? detection.subdir : null;
|
|
5647
5254
|
}
|
|
5648
|
-
function
|
|
5649
|
-
return
|
|
5255
|
+
function classicPerTestRunner(runner) {
|
|
5256
|
+
return runner !== "command" && classicPerTestRefusal(runner) === null;
|
|
5650
5257
|
}
|
|
5651
|
-
function
|
|
5652
|
-
return
|
|
5258
|
+
function classicPerTestRefusal(runner) {
|
|
5259
|
+
return runner === "deno" ? "deno's mutation runs the whole suite per mutant (Stryker ships no deno runner), so the classic per-test layer has no per-test report to read; the sealed v2 engine drives deno one test at a time and is where a proposed test for this repository comes from" : null;
|
|
5653
5260
|
}
|
|
5654
|
-
function
|
|
5655
|
-
const
|
|
5656
|
-
|
|
5261
|
+
function declaresWorkspace(ctx, repoDir) {
|
|
5262
|
+
for (const name of ["pnpm-workspace.yaml", "pnpm-workspace.yml", "nx.json", "lerna.json"]) {
|
|
5263
|
+
if (accessExists(ctx, join8(repoDir, name))) return true;
|
|
5264
|
+
}
|
|
5657
5265
|
try {
|
|
5658
|
-
|
|
5266
|
+
const reading = accessManifest(ctx, join8(repoDir, "package.json"));
|
|
5267
|
+
if (reading.standing !== "read") return false;
|
|
5268
|
+
const workspaces = reading.manifest.workspaces;
|
|
5269
|
+
if (Array.isArray(workspaces)) return workspaces.length > 0;
|
|
5270
|
+
if (workspaces !== null && typeof workspaces === "object") {
|
|
5271
|
+
return Array.isArray(workspaces.packages);
|
|
5272
|
+
}
|
|
5659
5273
|
} catch {
|
|
5660
|
-
return
|
|
5274
|
+
return false;
|
|
5661
5275
|
}
|
|
5276
|
+
return false;
|
|
5662
5277
|
}
|
|
5663
|
-
var
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5278
|
+
var NAMED_CANDIDATES = 8;
|
|
5279
|
+
function testableWorkspacePackages(repoDir, refusals, ctx = diskRepoAccess(repoDir)) {
|
|
5280
|
+
const aliasFamily = rootAliasFamily(ctx, repoDir);
|
|
5281
|
+
const aggregatorTasks = rootAggregatorTasksInRepository(ctx, repoDir);
|
|
5282
|
+
const found = [];
|
|
5283
|
+
const enumeration = enumerateWorkspacePackages(repoDir);
|
|
5284
|
+
if (refusals !== void 0) refusals.push(...enumeration.refusals);
|
|
5285
|
+
for (const dir of enumeration.packages) {
|
|
5286
|
+
const { probe, refusal } = probePkg(ctx, join8(repoDir, dir), aliasFamily, repoDir, aggregatorTasks);
|
|
5287
|
+
if (refusal !== null) {
|
|
5288
|
+
if (refusals !== void 0) refusals.push(refusal.summary);
|
|
5289
|
+
continue;
|
|
5290
|
+
}
|
|
5291
|
+
if (probe === null) continue;
|
|
5292
|
+
if (probe.runner !== null || declaresASuite(probe)) found.push(dir);
|
|
5293
|
+
}
|
|
5294
|
+
return found;
|
|
5675
5295
|
}
|
|
5676
|
-
function
|
|
5677
|
-
|
|
5296
|
+
function declaresASuite(probe) {
|
|
5297
|
+
if (probe.testScript === null) return false;
|
|
5298
|
+
const runs = scriptRuns2({ scripts: probe.scripts, text: probe.testScript });
|
|
5299
|
+
if (runs.length === 0) return true;
|
|
5300
|
+
return runs.some((run) => run.kind === "test" || run.kind === "unknown");
|
|
5678
5301
|
}
|
|
5679
|
-
function
|
|
5680
|
-
|
|
5302
|
+
function candidateRemedy(repoDir) {
|
|
5303
|
+
const refusals = [];
|
|
5304
|
+
const candidates = testableWorkspacePackages(repoDir, refusals);
|
|
5305
|
+
if (candidates.length === 0) {
|
|
5306
|
+
return refusals.length > 0 ? `${refusals[0]}. Declare environment.testCommand in abloh.yml with the command that runs your suite, or run abloh init from the package that holds it` : "declare environment.testCommand in abloh.yml with the command that runs your suite, or run abloh init from the package that holds it";
|
|
5307
|
+
}
|
|
5308
|
+
return `point --subdir (or abloh.yml target.directory) at one of the ${candidates.length} package(s) here that declare a suite, such as ${candidates[0]}`;
|
|
5681
5309
|
}
|
|
5682
|
-
function
|
|
5683
|
-
const
|
|
5684
|
-
|
|
5310
|
+
function candidateSentence(repoDir) {
|
|
5311
|
+
const candidates = testableWorkspacePackages(repoDir);
|
|
5312
|
+
if (candidates.length === 0) return "";
|
|
5313
|
+
const named = candidates.slice(0, NAMED_CANDIDATES);
|
|
5314
|
+
const more = candidates.length - named.length;
|
|
5315
|
+
return `. The packages here that declare a suite are ${named.join(", ")}` + (more > 0 ? ` and ${more} more` : "");
|
|
5316
|
+
}
|
|
5317
|
+
function detectTarget(repoDir, subdir, resolution = {}, ctx = diskRepoAccess(repoDir)) {
|
|
5318
|
+
const aliasFamily = rootAliasFamily(ctx, repoDir);
|
|
5319
|
+
const aggregatorTasks = rootAggregatorTasksInRepository(ctx, repoDir);
|
|
5320
|
+
const explicitResult = subdir ? probePkg(ctx, join8(repoDir, subdir), aliasFamily, repoDir, aggregatorTasks) : null;
|
|
5321
|
+
if (explicitResult?.refusal != null) throw manifestRefusal(explicitResult.refusal);
|
|
5322
|
+
const explicit = explicitResult?.probe ?? null;
|
|
5323
|
+
if (subdir && explicit === null) {
|
|
5324
|
+
throw new TargetNotFoundError(
|
|
5325
|
+
"configured-directory-has-no-package",
|
|
5326
|
+
`the configured target directory ${JSON.stringify(subdir)} has no package.json, so there is no package there to measure; point --subdir (or abloh.yml target.directory) at the directory holding the package's package.json, or remove it to measure the repository root`,
|
|
5327
|
+
"point --subdir (or abloh.yml target.directory) at the directory holding the package's package.json, or remove it to measure the repository root"
|
|
5328
|
+
);
|
|
5329
|
+
}
|
|
5330
|
+
const rootResult = probePkg(ctx, repoDir, aliasFamily, repoDir, aggregatorTasks);
|
|
5331
|
+
if (rootResult.refusal !== null) throw manifestRefusal(rootResult.refusal);
|
|
5332
|
+
const root = rootResult.probe;
|
|
5333
|
+
const probe = explicit !== null && (explicit.runner !== null || explicit.testScript !== null) ? explicit : root !== null && (root.runner !== null || root.testScript !== null) ? root : null;
|
|
5334
|
+
if (!probe) {
|
|
5335
|
+
throw new TargetNotFoundError(
|
|
5336
|
+
"no-testable-package",
|
|
5337
|
+
`no package.json with a test runner or test script found under ${repoDir}` + (subdir ? ` or in the configured target directory ${JSON.stringify(subdir)}` : "") + candidateSentence(repoDir),
|
|
5338
|
+
candidateRemedy(repoDir)
|
|
5339
|
+
);
|
|
5340
|
+
}
|
|
5341
|
+
const workspaceRootSuite = probe.workspaceRootSuite;
|
|
5342
|
+
const commandDir = workspaceRootSuite === null ? probe.dir : repoDir;
|
|
5343
|
+
const manager = detectPackageManagerContext(repoDir, commandDir, resolution, ctx);
|
|
5344
|
+
const pm = manager.pm;
|
|
5345
|
+
const runner = probe.runner ?? "command";
|
|
5346
|
+
const projectTestCommand = packageTestCommand(manager);
|
|
5347
|
+
const direct = directRunnerCommand(runner, detectModuleLayout(repoDir, probe.dir));
|
|
5348
|
+
if (probe.declaredCommand === null && probe.testScript === null && direct !== null && probe.dir === repoDir && declaresWorkspace(ctx, repoDir)) {
|
|
5349
|
+
throw new TargetNotFoundError(
|
|
5350
|
+
"workspace-root-declares-no-test-script",
|
|
5351
|
+
`${repoDir} is a workspace root that declares no test script, so there is no suite here to measure and Abloh will not guess one from an installed runner: point --subdir (or abloh.yml target.directory) at the package you want measured, or declare environment.testCommand if one command runs the suite from the root` + candidateSentence(repoDir),
|
|
5352
|
+
"point --subdir (or abloh.yml target.directory) at the package you want measured, or declare environment.testCommand if one command runs the suite from the root" + candidateSentence(repoDir)
|
|
5353
|
+
);
|
|
5354
|
+
}
|
|
5355
|
+
const testCommand = probe.declaredCommand ?? (probe.testScript !== null || direct === null ? projectTestCommand : direct);
|
|
5356
|
+
const rel = relative5(repoDir, probe.dir);
|
|
5357
|
+
const resolvedSubdir = rel === "" ? null : rel;
|
|
5358
|
+
const lifecycleHooks = [probe.scripts.pretest, probe.scripts.posttest].filter(
|
|
5359
|
+
(hook) => typeof hook === "string"
|
|
5360
|
+
);
|
|
5361
|
+
const runnerStage = probe.testScript !== null && testCommand === projectTestCommand ? runnerStageCommand(probe.testScript, runner, lifecycleHooks, probe.scripts) : null;
|
|
5362
|
+
const preRunnerStages = probe.testScript !== null && testCommand === projectTestCommand ? preRunnerStageCommands(probe.testScript, runner, probe.scripts) : [];
|
|
5363
|
+
const watchDeclaration = testCommand === projectTestCommand ? probe.testScript : testCommand;
|
|
5364
|
+
if (watchDeclaration !== null) {
|
|
5365
|
+
const refusal = watchModeRefusal(
|
|
5366
|
+
runner,
|
|
5367
|
+
watchDeclaration,
|
|
5368
|
+
testCommand === projectTestCommand ? `scripts.test in ${join8(relative5(repoDir, probe.dir), "package.json")}` : probe.declaredCommand === testCommand ? "your declared environment.testCommand" : "your test command"
|
|
5369
|
+
);
|
|
5370
|
+
if (refusal !== null) throw new TargetDetectionError(refusal);
|
|
5371
|
+
}
|
|
5372
|
+
const watchNormalized = watchDeclaration === null ? null : withWatchModeDisabled({
|
|
5373
|
+
runner,
|
|
5374
|
+
command: testCommand,
|
|
5375
|
+
watchDeclaration,
|
|
5376
|
+
packageManagerMajor: manager.version === null ? null : Number.parseInt(manager.version, 10)
|
|
5377
|
+
});
|
|
5378
|
+
const effectiveTestCommand = watchNormalized ?? testCommand;
|
|
5379
|
+
const watchNormalizedStage = runnerStage === null ? null : withWatchModeDisabled({ runner, command: runnerStage, watchDeclaration: runnerStage });
|
|
5380
|
+
const invocation = RUNNER_INVOCATION_CARRYING_RUNNERS.has(runner) ? runnerInvocation(probe.testScript, runner, probe.scripts) ?? probe.declaredCommand ?? (testCommand === direct ? direct : null) : null;
|
|
5381
|
+
return {
|
|
5382
|
+
pm,
|
|
5383
|
+
runner,
|
|
5384
|
+
testCommand: effectiveTestCommand,
|
|
5385
|
+
...watchNormalized === null ? {} : { watchModeNormalized: true },
|
|
5386
|
+
...probe.aggregator === null ? {} : { workspaceAggregator: probe.aggregator },
|
|
5387
|
+
...probe.aggregatorTask == null ? {} : { aggregatorTask: probe.aggregatorTask },
|
|
5388
|
+
...runnerStage === null ? {} : { runnerStageCommand: watchNormalizedStage ?? runnerStage },
|
|
5389
|
+
/* Only alongside a runner stage, because these are exactly what running that stage alone would
|
|
5390
|
+
otherwise skip. A one-stage script gated by a lifecycle hook has a runner stage and no
|
|
5391
|
+
earlier stage of its own, and gets an empty list rather than a key. */
|
|
5392
|
+
...runnerStage === null || preRunnerStages.length === 0 ? {} : { preRunnerStageCommands: preRunnerStages },
|
|
5393
|
+
...invocation === null ? {} : { runnerInvocation: invocation },
|
|
5394
|
+
vitestDir: runner === "vitest" ? detectVitestDir(probe.dir, ctx) : void 0,
|
|
5395
|
+
jasmineSuite: runner === "jasmine" ? detectJasmineSuite(probe.dir, probe.testScript, ctx) : void 0,
|
|
5396
|
+
...probe.angular === null ? {} : { angularTarget: probe.angular },
|
|
5397
|
+
subdir: resolvedSubdir,
|
|
5398
|
+
workDir: commandDir,
|
|
5399
|
+
...workspaceRootSuite === null ? {} : { workspaceRootSuite },
|
|
5400
|
+
...probe.note ? { identifiedRunner: probe.note } : {},
|
|
5401
|
+
...compiledOutputFields({
|
|
5402
|
+
ctx,
|
|
5403
|
+
repoDir,
|
|
5404
|
+
packageDir: resolvedSubdir,
|
|
5405
|
+
runner,
|
|
5406
|
+
invocation,
|
|
5407
|
+
testCommand: effectiveTestCommand,
|
|
5408
|
+
scripts: probe.scripts,
|
|
5409
|
+
manager: pm,
|
|
5410
|
+
angular: probe.angular !== null
|
|
5411
|
+
}),
|
|
5412
|
+
...runner === "command" ? suiteFanoutPlan(probe, manager) : {}
|
|
5413
|
+
};
|
|
5414
|
+
}
|
|
5415
|
+
|
|
5416
|
+
// src/aggregator-chain.ts
|
|
5417
|
+
import { runnerConfigFromInvocation } from "@abloh/core";
|
|
5418
|
+
var EXCLUDE_FLAGS = /* @__PURE__ */ new Set(["--exclude"]);
|
|
5419
|
+
var SELECTION_FLAGS = /* @__PURE__ */ new Set([
|
|
5420
|
+
"--filter",
|
|
5421
|
+
"-F",
|
|
5422
|
+
// turbo
|
|
5423
|
+
"--projects",
|
|
5424
|
+
"-p",
|
|
5425
|
+
// nx
|
|
5426
|
+
"--scope",
|
|
5427
|
+
"--ignore",
|
|
5428
|
+
"--since",
|
|
5429
|
+
// lerna
|
|
5430
|
+
"--to"
|
|
5431
|
+
// lage
|
|
5432
|
+
]);
|
|
5433
|
+
function flagValues(args, names) {
|
|
5434
|
+
const found = [];
|
|
5435
|
+
for (let at = 0; at < args.length; at += 1) {
|
|
5436
|
+
const arg = args[at];
|
|
5437
|
+
const eq = arg.indexOf("=");
|
|
5438
|
+
const name = eq === -1 ? arg : arg.slice(0, eq);
|
|
5439
|
+
if (!names.has(name)) continue;
|
|
5440
|
+
if (eq !== -1) {
|
|
5441
|
+
found.push(arg.slice(eq + 1));
|
|
5442
|
+
continue;
|
|
5443
|
+
}
|
|
5444
|
+
const next = args[at + 1];
|
|
5445
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
5446
|
+
found.push(next);
|
|
5447
|
+
at += 1;
|
|
5448
|
+
} else {
|
|
5449
|
+
found.push("");
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
return found;
|
|
5685
5453
|
}
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5454
|
+
function selectionFlagsPresent(args) {
|
|
5455
|
+
const present = [];
|
|
5456
|
+
for (const arg of args) {
|
|
5457
|
+
const eq = arg.indexOf("=");
|
|
5458
|
+
const name = eq === -1 ? arg : arg.slice(0, eq);
|
|
5459
|
+
if (SELECTION_FLAGS.has(name)) present.push(arg);
|
|
5460
|
+
}
|
|
5461
|
+
return present;
|
|
5693
5462
|
}
|
|
5694
|
-
function
|
|
5695
|
-
const
|
|
5696
|
-
if (
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
if (/[;&|<>`]|\$\(/u.test(script)) return { refusal: "it composes shell commands around the wrapper" };
|
|
5700
|
-
let argv;
|
|
5463
|
+
function plainGlob(pattern) {
|
|
5464
|
+
const path = pattern.trim().replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
|
|
5465
|
+
if (path === "" || path.startsWith("/") || /[{}()[\]!?+@|^$]/u.test(path)) return null;
|
|
5466
|
+
if (path.split("/").some((segment) => segment === "..")) return null;
|
|
5467
|
+
const source = path.split("/").map((segment) => segment === "**" ? "[^/]+(?:/[^/]+)*" : segment === "*" ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("/");
|
|
5701
5468
|
try {
|
|
5702
|
-
|
|
5469
|
+
return new RegExp(`^${source}$`, "u");
|
|
5703
5470
|
} catch {
|
|
5704
|
-
return
|
|
5471
|
+
return null;
|
|
5705
5472
|
}
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5473
|
+
}
|
|
5474
|
+
var CONFIG_STEMS = {
|
|
5475
|
+
vitest: ["vitest.config", "vite.config"],
|
|
5476
|
+
jest: ["jest.config"],
|
|
5477
|
+
mocha: [".mocharc"],
|
|
5478
|
+
ava: ["ava.config"],
|
|
5479
|
+
jasmine: ["jasmine.json"],
|
|
5480
|
+
tap: ["tap.yaml"]
|
|
5481
|
+
};
|
|
5482
|
+
var CONFIG_EXTENSIONS = ["", ".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json", ".yaml", ".yml"];
|
|
5483
|
+
function configFileFor(repoDir, ctx, directory, runner, script) {
|
|
5484
|
+
const named = runnerConfigFromInvocation({ runner, testCommand: script });
|
|
5485
|
+
if (named !== null) return joinInsideRepository(directory, named);
|
|
5486
|
+
for (const stem of CONFIG_STEMS[runner] ?? []) {
|
|
5487
|
+
for (const extension of CONFIG_EXTENSIONS) {
|
|
5488
|
+
const relative7 = directory === "" ? `${stem}${extension}` : `${directory}/${stem}${extension}`;
|
|
5489
|
+
if (accessKind(ctx, join9(repoDir, relative7)) === "file") return relative7;
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
return null;
|
|
5493
|
+
}
|
|
5494
|
+
function joinInsideRepository(directory, declared) {
|
|
5495
|
+
const raw = declared.replace(/^["']|["']$/gu, "").replace(/\\/gu, "/");
|
|
5496
|
+
if (raw === "" || raw.startsWith("/") || /^[A-Za-z]:/u.test(raw) || /[;&|<>`$]/u.test(raw)) return null;
|
|
5497
|
+
const resolved = [];
|
|
5498
|
+
for (const segment of [...directory.split("/"), ...raw.split("/")]) {
|
|
5499
|
+
if (segment === "" || segment === ".") continue;
|
|
5500
|
+
if (segment === "..") {
|
|
5501
|
+
if (resolved.length === 0) return null;
|
|
5502
|
+
resolved.pop();
|
|
5716
5503
|
continue;
|
|
5717
5504
|
}
|
|
5718
|
-
|
|
5505
|
+
resolved.push(segment);
|
|
5719
5506
|
}
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
const
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5507
|
+
return resolved.length === 0 ? null : resolved.join("/");
|
|
5508
|
+
}
|
|
5509
|
+
function manifestAt(ctx, repoDir, directory) {
|
|
5510
|
+
const reading = accessManifest(ctx, join9(repoDir, directory, "package.json"));
|
|
5511
|
+
return reading.standing === "read" ? reading.manifest : null;
|
|
5512
|
+
}
|
|
5513
|
+
function aggregatorChain(repoDir, ctx = diskRepoAccess(repoDir)) {
|
|
5514
|
+
const rootManifest = manifestAt(ctx, repoDir, "");
|
|
5515
|
+
if (rootManifest === null) return null;
|
|
5516
|
+
const family = aliasHopFamily(rootManifest);
|
|
5517
|
+
const classified = classifyWithAliasHop(rootManifest, family);
|
|
5518
|
+
if (classified.aggregator === null) return null;
|
|
5519
|
+
const invocationScript = classified.aggregatorInvocationScript;
|
|
5520
|
+
const invocation = invocationScript === null ? null : aggregatorInvocation(invocationScript);
|
|
5521
|
+
const args = invocationScript === null ? [] : invocationScript.trim().split(/\s+/u).slice(1);
|
|
5522
|
+
const unreadSelection = selectionFlagsPresent(args);
|
|
5523
|
+
const excludeGlobs = [];
|
|
5524
|
+
for (const value of flagValues(args, EXCLUDE_FLAGS)) {
|
|
5525
|
+
const glob = plainGlob(value);
|
|
5526
|
+
if (glob === null) unreadSelection.push(`--exclude=${value}`);
|
|
5527
|
+
else excludeGlobs.push(glob);
|
|
5528
|
+
}
|
|
5529
|
+
const enumeration = enumerateWorkspacePackages(repoDir, ctx);
|
|
5530
|
+
const packages = [];
|
|
5531
|
+
const gaps = [];
|
|
5532
|
+
const excluded = [];
|
|
5533
|
+
for (const directory of enumeration.packages) {
|
|
5534
|
+
if (excludeGlobs.some((glob) => glob.test(directory))) {
|
|
5535
|
+
excluded.push(directory);
|
|
5536
|
+
continue;
|
|
5537
|
+
}
|
|
5538
|
+
const manifest = manifestAt(ctx, repoDir, directory);
|
|
5539
|
+
if (manifest === null) {
|
|
5540
|
+
gaps.push({ directory, reason: "abloh could not read its package.json" });
|
|
5541
|
+
continue;
|
|
5542
|
+
}
|
|
5543
|
+
const packageFamily = aliasHopFamily(manifest) ?? family;
|
|
5544
|
+
const suite = classifyWithAliasHop(manifest, packageFamily, classified.aggregatorTasks);
|
|
5545
|
+
if (suite.runner === null || suite.testScript === null) {
|
|
5546
|
+
gaps.push({
|
|
5547
|
+
directory,
|
|
5548
|
+
reason: suite.aggregatorTaskRefusal ?? `nothing it declares under ${describeTasks(classified.aggregatorTasks)} runs a test runner abloh recognizes`
|
|
5549
|
+
});
|
|
5550
|
+
continue;
|
|
5551
|
+
}
|
|
5552
|
+
packages.push({
|
|
5553
|
+
directory,
|
|
5554
|
+
task: suite.aggregatorTask ?? "test",
|
|
5555
|
+
script: suite.testScript,
|
|
5556
|
+
runner: suite.runner,
|
|
5557
|
+
configFile: configFileFor(repoDir, ctx, directory, suite.runner, suite.testScript)
|
|
5558
|
+
});
|
|
5728
5559
|
}
|
|
5729
|
-
|
|
5730
|
-
|
|
5560
|
+
return {
|
|
5561
|
+
tool: classified.aggregator,
|
|
5562
|
+
tasks: invocation?.tasks ?? classified.aggregatorTasks,
|
|
5563
|
+
packages,
|
|
5564
|
+
gaps,
|
|
5565
|
+
unreadSelection,
|
|
5566
|
+
excluded,
|
|
5567
|
+
refusals: enumeration.refusals
|
|
5568
|
+
};
|
|
5731
5569
|
}
|
|
5732
|
-
function
|
|
5733
|
-
const
|
|
5734
|
-
if (
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5570
|
+
function describeTasks(tasks) {
|
|
5571
|
+
const named = tasks.length === 0 ? ["test"] : [...tasks];
|
|
5572
|
+
if (named.length === 1) return named[0];
|
|
5573
|
+
return `${named.slice(0, -1).join(", ")} or ${named.at(-1)}`;
|
|
5574
|
+
}
|
|
5575
|
+
|
|
5576
|
+
// src/config-draft.ts
|
|
5577
|
+
import { parse as parse2 } from "yaml";
|
|
5578
|
+
import {
|
|
5579
|
+
CUSTOMER_TYPED,
|
|
5580
|
+
DeclaredRefusalError as DeclaredRefusalError2,
|
|
5581
|
+
refusalFromFacts as refusalFromFacts2,
|
|
5582
|
+
WORKFLOW_FILE_PATH,
|
|
5583
|
+
WORKFLOW_JOB_ID,
|
|
5584
|
+
SEALED_BUN_VERSION,
|
|
5585
|
+
SEALED_DENO_VERSION,
|
|
5586
|
+
SETUP_SCRIPT_PATH,
|
|
5587
|
+
configFlakyLines,
|
|
5588
|
+
installCommandNeedsCorepackEnable,
|
|
5589
|
+
parseLiteralArguments as parseLiteralArguments2,
|
|
5590
|
+
systemPackageInstallCommand,
|
|
5591
|
+
configHeaderLines,
|
|
5592
|
+
configSettingLines,
|
|
5593
|
+
configSetupJobLines,
|
|
5594
|
+
credentialShapeRefusal,
|
|
5595
|
+
matchesCredentialShape as matchesCredentialShape2,
|
|
5596
|
+
workflowYaml,
|
|
5597
|
+
yamlScalar
|
|
5598
|
+
} from "@abloh/core";
|
|
5599
|
+
|
|
5600
|
+
// src/init-refusal.ts
|
|
5601
|
+
var InitRefusal = class extends Error {
|
|
5602
|
+
failureClass;
|
|
5603
|
+
/**
|
|
5604
|
+
* THE WALK KEY THIS REFUSAL IS ABOUT, where the throw site knows one the class cannot.
|
|
5605
|
+
*
|
|
5606
|
+
* Null on almost every throw, and that is right: the class already implies the key for a refusal
|
|
5607
|
+
* that can only ever be about one - `no-reviewed-image` is `runtimeImage` wherever it is raised,
|
|
5608
|
+
* and `init-refusal-shape.ts` holds that mapping in one table rather than repeating it at forty
|
|
5609
|
+
* throw sites. This field is for the refusals where the class covers SEVERAL keys, which today is
|
|
5610
|
+
* the answers document: `invalid-answers-file` is about whichever key the document got wrong, and
|
|
5611
|
+
* a fix depends on knowing which.
|
|
5612
|
+
*/
|
|
5613
|
+
questionKey;
|
|
5614
|
+
/**
|
|
5615
|
+
* THIS REFUSAL'S OWN REMEDY, where the class table's is not true of this instance.
|
|
5616
|
+
*
|
|
5617
|
+
* Null on almost every throw, and that is still right: a per-class remedy is what stops forty
|
|
5618
|
+
* throw sites carrying forty drifting copies of the same sentence. But a CLASS is not always one
|
|
5619
|
+
* situation, and `repository-too-large` is where that broke (external refusal review, rank 15).
|
|
5620
|
+
* Its four throw sites all tell the human to pass `--environment-image`, because the scan they
|
|
5621
|
+
* abandon is the Dockerfile scan and an image is what makes it unnecessary. The class table told
|
|
5622
|
+
* the MACHINE to pass `--subdir`, which narrows nothing: the scan walks the repository root
|
|
5623
|
+
* whatever `--subdir` says. A human and an agent reading the same refusal were sent to different
|
|
5624
|
+
* options, and only one of them could work.
|
|
5625
|
+
*
|
|
5626
|
+
* So a throw site that knows better than its class says so here, and `init-refusal-shape.ts`
|
|
5627
|
+
* prefers it. One refusal instance, one remedy, whoever is reading.
|
|
5628
|
+
*/
|
|
5629
|
+
remedy;
|
|
5630
|
+
constructor(failureClass, message, questionKey = null, remedy = null) {
|
|
5631
|
+
super(message);
|
|
5632
|
+
this.name = "InitRefusal";
|
|
5633
|
+
this.failureClass = failureClass;
|
|
5634
|
+
this.questionKey = questionKey;
|
|
5635
|
+
this.remedy = remedy;
|
|
5742
5636
|
}
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5637
|
+
};
|
|
5638
|
+
function initRefusal(failureClass, message, questionKey = null, remedy = null) {
|
|
5639
|
+
return new InitRefusal(failureClass, message, questionKey, remedy);
|
|
5640
|
+
}
|
|
5641
|
+
function refusalRemedyOf(error) {
|
|
5642
|
+
return error instanceof InitRefusal ? error.remedy : null;
|
|
5643
|
+
}
|
|
5644
|
+
function initFailureClassOf(error) {
|
|
5645
|
+
return error instanceof InitRefusal ? error.failureClass : "unclassified";
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
// src/config-draft.ts
|
|
5649
|
+
function commentLines(text, indent, width = 108) {
|
|
5650
|
+
const lines = [];
|
|
5651
|
+
let current = "";
|
|
5652
|
+
for (const word of text.split(/\s+/u).filter((value) => value !== "")) {
|
|
5653
|
+
const candidate = current === "" ? word : `${current} ${word}`;
|
|
5654
|
+
if (`${indent}# ${candidate}`.length > width && current !== "") {
|
|
5655
|
+
lines.push(`${indent}# ${current}`);
|
|
5656
|
+
current = word;
|
|
5657
|
+
continue;
|
|
5658
|
+
}
|
|
5659
|
+
current = candidate;
|
|
5660
|
+
}
|
|
5661
|
+
if (current !== "") lines.push(`${indent}# ${current}`);
|
|
5662
|
+
return lines;
|
|
5663
|
+
}
|
|
5664
|
+
function renderEnvironmentBlock(input) {
|
|
5665
|
+
const quote = input.yamlQuote ?? '"';
|
|
5666
|
+
const receiptLinesQ = (entries, indent) => receiptLines(entries, indent, quote);
|
|
5667
|
+
const lines = [];
|
|
5668
|
+
if (input.coverageNote != null) lines.push(...commentLines(input.coverageNote, "", 80), "");
|
|
5669
|
+
if (input.subdir !== null) {
|
|
5670
|
+
lines.push(
|
|
5671
|
+
"# The package measured.",
|
|
5672
|
+
"target:",
|
|
5673
|
+
` directory: ${yamlScalar(input.subdir, quote)}`,
|
|
5674
|
+
""
|
|
5675
|
+
);
|
|
5676
|
+
}
|
|
5677
|
+
lines.push(
|
|
5678
|
+
ENVIRONMENT_BLOCK_HEADER,
|
|
5679
|
+
"environment:",
|
|
5680
|
+
...receiptLinesQ([
|
|
5681
|
+
{ key: "runtimeImage", value: input.runtimeImage, receipt: input.evidence.runtimeImage }
|
|
5682
|
+
], " "),
|
|
5683
|
+
...input.runtimes.length === 0 ? [] : [
|
|
5684
|
+
" runtimes: # pinned runtimes added to the proof image, installed at build time",
|
|
5685
|
+
...input.runtimes.flatMap((need) => [
|
|
5686
|
+
...commentLines(need.evidence, " "),
|
|
5687
|
+
` - ${yamlScalar(need.runtime, quote)}`
|
|
5688
|
+
])
|
|
5689
|
+
],
|
|
5690
|
+
...input.browser === null ? [] : [
|
|
5691
|
+
...commentLines(
|
|
5692
|
+
"your suite drives a real browser, so the proof image carries one and the run uses the browser seal - still --network none, reaching 127.0.0.1 and nothing else. Read from: " + input.browser.evidence.join("; "),
|
|
5693
|
+
" "
|
|
5694
|
+
),
|
|
5695
|
+
` browser: ${yamlScalar(input.browser.value, quote)}`
|
|
5696
|
+
],
|
|
5697
|
+
` installDirectory: ${yamlScalar(input.installDirectory, quote)}`,
|
|
5698
|
+
...receiptLinesQ([
|
|
5699
|
+
/* THE POINTER, WHERE FOUR DERIVED KEYS USED TO BE (Kenneth, 2026-08-26). `systemPackages`,
|
|
5700
|
+
`installCommand` and `setupCommands` are the commands that turn a checkout into a tree the
|
|
5701
|
+
suite can run in, and they are now steps in a file the customer owns. What is left in this
|
|
5702
|
+
block is everything that is NOT a command: the image, the directory, the test commands, the
|
|
5703
|
+
services, the variables, the files. The receipt names the script rather than a source file
|
|
5704
|
+
because the script carries a receipt of its own on every step. */
|
|
5705
|
+
{
|
|
5706
|
+
key: "setup",
|
|
5707
|
+
value: input.setupPath ?? SETUP_SCRIPT_PATH,
|
|
5708
|
+
receipt: "written by init and yours to edit. Every cold run executes it step by step"
|
|
5709
|
+
},
|
|
5710
|
+
/* THE ONE THING THE SCRIPT CANNOT SAY, so the file still does: WHY there is no install step
|
|
5711
|
+
in it. A repository with no frozen recipe gets a script with nothing to install, and a
|
|
5712
|
+
reader who only ever opens `abloh.yml` would otherwise see a pointer at a file that quietly
|
|
5713
|
+
does less than they think. The remedy is the same sentence the console prints. */
|
|
5714
|
+
...input.installCommand === null ? [{ key: "installCommand", value: null, receipt: input.evidence.installCommand }] : [],
|
|
5715
|
+
/* THE ONE RECEIPT THAT GAINS A CLAUSE IN THE FILE TOO, and only on the repositories it is
|
|
5716
|
+
true of. `genericAdapterCost` carries what changed and why the prompt alone was not it. */
|
|
5717
|
+
{
|
|
5718
|
+
key: "testCommand",
|
|
5719
|
+
value: input.testCommand,
|
|
5720
|
+
receipt: testCommandReceipt({
|
|
5721
|
+
evidence: input.evidence.testCommand,
|
|
5722
|
+
cost: input.testCommandCostNote
|
|
5723
|
+
})
|
|
5724
|
+
},
|
|
5725
|
+
/* WRITTEN ONLY WHERE IT SAYS SOMETHING, and its receipt is the whole reason it says anything.
|
|
5726
|
+
See `ConfigInput.runner`: on an uncontested repository the key would restate what
|
|
5727
|
+
`bindPreparedTestCommand` reads off the command one line above, so there is no line and the
|
|
5728
|
+
PROMPT is where the detected runner is shown. Where a candidate was discarded, the line
|
|
5729
|
+
records the decision AND names what was skipped, in the same words the prompt used - which
|
|
5730
|
+
is what makes an unattended init as informed as an interactive one. */
|
|
5731
|
+
...input.runner === null ? [] : [{ key: "runner", value: input.runner, receipt: input.evidence.runner }],
|
|
5732
|
+
/* THE ANSWER TO THE ONE QUESTION A VITEST REPOSITORY WITH NO COVERAGE PROVIDER IS ASKED.
|
|
5733
|
+
WRITTEN ONLY WHERE SOMEBODY ANSWERED IT, on the same restraint every derived key here
|
|
5734
|
+
follows: absent already means `install`, so a line saying `install` on a repository nobody
|
|
5735
|
+
asked would be a claim about a conversation that never happened. Both answers are written
|
|
5736
|
+
where there WAS one, because the file is where the run reads it. */
|
|
5737
|
+
...input.coverageProvider === void 0 ? [] : [{
|
|
5738
|
+
key: "coverageProvider",
|
|
5739
|
+
value: input.coverageProvider,
|
|
5740
|
+
receipt: input.coverageProvider === "install" ? "you agreed abloh may stage @vitest/coverage-v8 where a run measures, into its own cache outside this repository" : "you asked abloh not to install @vitest/coverage-v8, so the changed-line check reports itself as declined"
|
|
5741
|
+
}],
|
|
5742
|
+
...input.sealedTestCommand !== null || input.sealedNote !== null ? [{
|
|
5743
|
+
key: "sealedTestCommand",
|
|
5744
|
+
value: input.sealedTestCommand,
|
|
5745
|
+
receipt: input.sealedTestCommand === null ? input.sealedNote ?? "" : input.evidence.sealedTestCommand
|
|
5746
|
+
}] : [],
|
|
5747
|
+
/* THE ONE KEY WHOSE ABSENCE PRODUCES A WRONG NUMBER RATHER THAN A REFUSAL, so the file states
|
|
5748
|
+
the finding even when it cannot state a value: a reviewer reading a commented-out
|
|
5749
|
+
`rebuildCommand` beside "these two files say your tests read your build output" has the
|
|
5750
|
+
whole question in front of them, and a file that stayed silent would leave a run reporting
|
|
5751
|
+
every mutant as a survivor with nothing anywhere to explain it. */
|
|
5752
|
+
...input.rebuild === null ? [] : [{
|
|
5753
|
+
key: "rebuildCommand",
|
|
5754
|
+
value: input.rebuild.value,
|
|
5755
|
+
/* THE KEY IS NAMED IN THE SENTENCE, because a null value renders as a bare comment with
|
|
5756
|
+
no key beside it - and a reader who agrees with the finding then has nothing to type.
|
|
5757
|
+
It is the same rule `installRemedy` follows one key up. */
|
|
5758
|
+
receipt: input.rebuild.value !== null ? input.rebuild.evidence : `${input.rebuild.evidence}. Set rebuildCommand to the build that writes it - this package declares ${input.rebuild.candidates.join(" and ")}. Without one, every planted bug is reported as missed`
|
|
5759
|
+
}]
|
|
5760
|
+
], " ")
|
|
5761
|
+
);
|
|
5762
|
+
if (input.services.length > 0) {
|
|
5763
|
+
lines.push(
|
|
5764
|
+
" services: # fresh instances per proof phase, reachable on localhost exactly as in your CI"
|
|
5765
|
+
);
|
|
5766
|
+
for (const service of input.services) {
|
|
5767
|
+
lines.push(
|
|
5768
|
+
...commentLines(service.evidence, " "),
|
|
5769
|
+
` - name: ${yamlScalar(service.name, quote)}`,
|
|
5770
|
+
` image: ${yamlScalar(service.image, quote)}`
|
|
5771
|
+
);
|
|
5772
|
+
if (service.env.length > 0) {
|
|
5773
|
+
lines.push(" env: # your workflow's own values; the suite connects with these");
|
|
5774
|
+
for (const entry of service.env) lines.push(` ${entry.name}: ${yamlScalar(entry.value, quote)}`);
|
|
5775
|
+
}
|
|
5776
|
+
if (service.healthCommand !== null) {
|
|
5777
|
+
lines.push(` healthCommand: ${yamlScalar(service.healthCommand, quote)}`);
|
|
5778
|
+
}
|
|
5779
|
+
if (service.healthPort !== null) lines.push(` healthPort: ${service.healthPort}`);
|
|
5780
|
+
if (service.localhostPort != null && service.localhostPort !== service.healthPort) {
|
|
5781
|
+
lines.push(
|
|
5782
|
+
` localhostPort: ${service.localhostPort} # another declared service listens on this one's port, so a run reaches it here`
|
|
5783
|
+
);
|
|
5748
5784
|
}
|
|
5749
|
-
continue;
|
|
5750
|
-
}
|
|
5751
|
-
if (!arg.startsWith("npm:")) {
|
|
5752
|
-
return { refusal: "concurrently is given a raw command line rather than an npm: script reference" };
|
|
5753
5785
|
}
|
|
5754
|
-
patterns.push(arg.slice("npm:".length));
|
|
5755
|
-
}
|
|
5756
|
-
if (patterns.length === 0) return { refusal: "its concurrently invocation selects no scripts" };
|
|
5757
|
-
const selected = /* @__PURE__ */ new Set();
|
|
5758
|
-
const names = Object.keys(probe.scripts).sort();
|
|
5759
|
-
for (const pattern of patterns) {
|
|
5760
|
-
const matches = names.filter((name) => name !== "test" && scriptPattern(pattern).test(name));
|
|
5761
|
-
if (matches.length === 0) return { refusal: `no script matches the pattern ${pattern}` };
|
|
5762
|
-
for (const match of matches) selected.add(match);
|
|
5763
5786
|
}
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
}
|
|
5767
|
-
function scriptChildren(scripts, probe, manager) {
|
|
5768
|
-
if (scripts.length === 0) return { refusal: "it selects no scripts" };
|
|
5769
|
-
if (scripts.length > MAX_FANOUT_CHILDREN) {
|
|
5770
|
-
return { refusal: `it launches ${scripts.length} suites, more than the ${MAX_FANOUT_CHILDREN} this run will measure` };
|
|
5787
|
+
if (input.identityFiles.length > 0) {
|
|
5788
|
+
lines.push(" identityFiles: # a change here makes CI reinstall instead of reusing its image");
|
|
5789
|
+
for (const path of input.identityFiles) lines.push(` - ${yamlScalar(path, quote)}`);
|
|
5771
5790
|
}
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
if (leaf === void 0 || /[;&|<>`]|\$\(/u.test(leaf)) {
|
|
5777
|
-
return { refusal: `the script ${script} composes shell commands rather than running one suite` };
|
|
5778
|
-
}
|
|
5779
|
-
let leafArgv;
|
|
5780
|
-
try {
|
|
5781
|
-
leafArgv = parseLiteralArguments2(leaf, `fanout child ${script}`);
|
|
5782
|
-
} catch {
|
|
5783
|
-
return { refusal: `the script ${script} could not be parsed as one literal command` };
|
|
5784
|
-
}
|
|
5785
|
-
if (leafArgv.some((arg) => WRAPPERS.includes(arg))) {
|
|
5786
|
-
return { refusal: `the script ${script} is itself a wrapper, and this expands one level only` };
|
|
5791
|
+
if (input.requiredVariables.length > 0) {
|
|
5792
|
+
lines.push(" requiredVariables: # names only; values come from your environment and are masked in every log");
|
|
5793
|
+
for (const variable of input.requiredVariables) {
|
|
5794
|
+
lines.push(...commentLines(variable.evidence, " "), ` - ${yamlScalar(variable.name, quote)}`);
|
|
5787
5795
|
}
|
|
5788
|
-
const runner = leafRunner(leaf);
|
|
5789
|
-
if (runner === null) return { refusal: `the script ${script} names no single runner with native coverage` };
|
|
5790
|
-
children.push({ script, runner, testCommand: packageScriptCommand(manager, script) });
|
|
5791
|
-
}
|
|
5792
|
-
return { children };
|
|
5793
|
-
}
|
|
5794
|
-
function chainFanout(probe, manager) {
|
|
5795
|
-
const script = probe.testScript;
|
|
5796
|
-
if (script === null || script.trim() === "") return void 0;
|
|
5797
|
-
if (suiteStages(script, probe.scripts) < 2) return void 0;
|
|
5798
|
-
if (/[|<>`]|\$\(/u.test(script) || /(^|[^&])&([^&]|$)/u.test(script)) {
|
|
5799
|
-
return { refusal: "its suites are composed with something other than `&&`, so which of them ran is not decidable" };
|
|
5800
|
-
}
|
|
5801
|
-
if (script.includes(";")) {
|
|
5802
|
-
return { refusal: "its suites are separated by `;`, which runs the next one whether or not the last passed" };
|
|
5803
|
-
}
|
|
5804
|
-
const chainStages = script.split("&&").map((stage) => stage.trim());
|
|
5805
|
-
if (chainStages.some((stage) => stage === "")) return { refusal: "its `&&` chain has an empty stage" };
|
|
5806
|
-
if (chainStages.length > MAX_FANOUT_CHILDREN) {
|
|
5807
|
-
return { refusal: `it chains ${chainStages.length} stages, more than the ${MAX_FANOUT_CHILDREN} this run will measure` };
|
|
5808
5796
|
}
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
if (alias !== null) {
|
|
5814
|
-
const family = stage.trim().replace(/^corepack\s+/u, "").split(/\s+/u)[0];
|
|
5815
|
-
if (family !== manager.pm) {
|
|
5816
|
-
return { refusal: `the stage \`${stage}\` runs under ${family} while this package resolves to ${manager.pm}` };
|
|
5817
|
-
}
|
|
5818
|
-
const named = scriptChildren([alias[1]], probe, manager);
|
|
5819
|
-
if ("refusal" in named) return named;
|
|
5820
|
-
children.push(...named.children);
|
|
5821
|
-
continue;
|
|
5822
|
-
}
|
|
5823
|
-
const runner = [...NATIVE_COVERAGE_RUNNERS].find((candidate) => stageInvokes(stage, candidate));
|
|
5824
|
-
if (runner === void 0) {
|
|
5825
|
-
return { refusal: `the stage \`${stage}\` is neither a suite nor a script this run can name` };
|
|
5797
|
+
if (input.environmentValues.length > 0) {
|
|
5798
|
+
for (const variable of input.environmentValues) {
|
|
5799
|
+
const shape = matchesCredentialShape2(variable.value);
|
|
5800
|
+
if (shape !== null) throw initRefusal("credential-in-environment-value", credentialShapeRefusal(variable.name, shape));
|
|
5826
5801
|
}
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5802
|
+
lines.push(" environmentValues: # literal values your CI sets; a secret goes in requiredVariables, by name");
|
|
5803
|
+
for (const variable of input.environmentValues) {
|
|
5804
|
+
lines.push(
|
|
5805
|
+
...commentLines(variable.evidence, " "),
|
|
5806
|
+
` ${variable.name}: ${yamlScalar(variable.value, quote)}`
|
|
5807
|
+
);
|
|
5832
5808
|
}
|
|
5833
|
-
children.push({ runner, testCommand });
|
|
5834
|
-
}
|
|
5835
|
-
if (children.length < 2) return void 0;
|
|
5836
|
-
return { children, execution: "sequential" };
|
|
5837
|
-
}
|
|
5838
|
-
function suiteFanoutPlan(probe, manager) {
|
|
5839
|
-
const aggregated = probe.aggregator === null ? null : { tool: probe.aggregator, tasks: [...probe.aggregatorTasks] };
|
|
5840
|
-
if (aggregated !== null) {
|
|
5841
|
-
return {
|
|
5842
|
-
coverageExclusion: `this package's test script delegates to ${aggregated.tool}, which runs ${aggregatorTaskPhrase(aggregated)} rather than one suite, so no single coverage report describes it; abloh measures such a repository per package instead`
|
|
5843
|
-
};
|
|
5844
|
-
}
|
|
5845
|
-
const resolved = wrapperFanout(probe, manager) ?? concurrentlyFanout(probe, manager) ?? chainFanout(probe, manager);
|
|
5846
|
-
if (resolved === void 0) return {};
|
|
5847
|
-
if ("refusal" in resolved) {
|
|
5848
|
-
return { coverageExclusion: `this package's test script launches more than one suite, but ${resolved.refusal}` };
|
|
5849
|
-
}
|
|
5850
|
-
const runners = [...new Set(resolved.children.map((child) => child.runner))].sort();
|
|
5851
|
-
if (runners.length > 1) {
|
|
5852
|
-
return {
|
|
5853
|
-
coverageExclusion: `this package's test script launches ${resolved.children.length} suites across ${runners.join(" and ")}, and one coverage report cannot carry two providers, so none of them is measured rather than some`
|
|
5854
|
-
};
|
|
5855
5809
|
}
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
const reading = readsCompiledOutput({
|
|
5861
|
-
/*
|
|
5862
|
-
* THE ONE READING ON THIS PATH THAT DOES NOT GO THROUGH THE REPOSITORY READER, and it is a
|
|
5863
|
-
* decision rather than an oversight.
|
|
5864
|
-
*
|
|
5865
|
-
* `readsCompiledOutput` FOLLOWS `extends` INTO `node_modules` by design - rushstack's projects
|
|
5866
|
-
* extend `@rushstack/heft-node-rig/profiles/default/tsconfig-base.json`, whose `outDir` is five
|
|
5867
|
-
* hops out of the store and back into the project's own `lib`, and refusing to follow it means
|
|
5868
|
-
* this reading never sees the repository the compile-then-test ruling was measured on. An
|
|
5869
|
-
* installed store is exactly where symlinks live and exactly where "inside the checkout" is a
|
|
5870
|
-
* different question, so the reader's two refusals - a symlinked leaf, and a parent that
|
|
5871
|
-
* resolves outside the root - would silently narrow it. What that costs is not a refusal: a
|
|
5872
|
-
* config this reading cannot see produces a different `rebuildCommand`, and a wrong rebuild is
|
|
5873
|
-
* the one failure in this product that ends in a confident WRONG NUMBER.
|
|
5874
|
-
*
|
|
5875
|
-
* IT IS SAFE HERE FOR THE REASON THE READER EXISTS ELSEWHERE. The paths are composed by
|
|
5876
|
-
* `compiled-output.ts` from a config's own `extends` and from `node_modules` walked upwards,
|
|
5877
|
-
* never from a repository-author-controlled string this module passes through; and the only
|
|
5878
|
-
* road that reaches it is one that HAS a checkout, since a service reading over the contents
|
|
5879
|
-
* API has no installed store to follow into anyway.
|
|
5880
|
-
*/
|
|
5881
|
-
readFile: (path) => {
|
|
5882
|
-
const absolute = join8(input.repoDir, ...path.split("/"));
|
|
5883
|
-
if (!existsSync2(absolute)) return null;
|
|
5884
|
-
try {
|
|
5885
|
-
return readFileSync5(absolute, "utf8");
|
|
5886
|
-
} catch {
|
|
5887
|
-
return null;
|
|
5888
|
-
}
|
|
5889
|
-
},
|
|
5890
|
-
packageDir: input.packageDir ?? "",
|
|
5891
|
-
runner: input.runner,
|
|
5892
|
-
invocation: input.invocation,
|
|
5893
|
-
/* THE COMMAND THIS RUN EXECUTES, beside the invocation and never instead of it. `runnerInvocation`
|
|
5894
|
-
answers a narrower question - whether a stage can be RE-EMITTED - so it declines a stage with
|
|
5895
|
-
anything in front of the runner, and `devcontainers/cli`'s whole suite is
|
|
5896
|
-
`env TS_NODE_PROJECT=... mocha ... src/test/*.test.ts`. The reading in `@abloh/core` resolves the
|
|
5897
|
-
runner's stage out of this where no invocation arrived; its own header states both halves. */
|
|
5898
|
-
testCommand: input.testCommand,
|
|
5899
|
-
scripts: input.scripts,
|
|
5900
|
-
manager: input.manager,
|
|
5901
|
-
/*
|
|
5902
|
-
* THE SECOND READING'S SEAM IS THE REPOSITORY READER and never the raw filesystem one above.
|
|
5903
|
-
*
|
|
5904
|
-
* That one exists to follow a `tsconfig` `extends` into an installed store, which is the one
|
|
5905
|
-
* place this detection legitimately leaves the tree. This reading opens the SUITE'S OWN TEST
|
|
5906
|
-
* FILES, which are the repository's, so it takes the reader whose refusals are about staying
|
|
5907
|
-
* inside it - a symlinked leaf and a parent resolving out of the root.
|
|
5908
|
-
*/
|
|
5909
|
-
reader: input.ctx.reader
|
|
5910
|
-
});
|
|
5911
|
-
const found = reading.state === "reads-compiled-output" ? reading : null;
|
|
5912
|
-
return {
|
|
5913
|
-
rebuildCommand: found?.rebuildCommand ?? void 0,
|
|
5914
|
-
rebuildEvidence: found === null ? void 0 : compiledOutputEvidence(found),
|
|
5915
|
-
rebuildCandidates: found === null || found.candidates.length === 0 ? void 0 : found.candidates,
|
|
5916
|
-
rebuildOutputDirectory: found?.outputDirectory ?? void 0
|
|
5917
|
-
};
|
|
5918
|
-
}
|
|
5919
|
-
function repositoryRootOf(detected) {
|
|
5920
|
-
if (detected.subdir === null || detected.workspaceRootSuite !== void 0) return detected.workDir;
|
|
5921
|
-
return resolve6(detected.workDir, ...detected.subdir.split("/").map(() => ".."));
|
|
5922
|
-
}
|
|
5923
|
-
var VITEST_CONFIG_FILES = [
|
|
5924
|
-
"vitest.config.ts",
|
|
5925
|
-
"vitest.config.mts",
|
|
5926
|
-
"vitest.config.cts",
|
|
5927
|
-
"vitest.config.js",
|
|
5928
|
-
"vitest.config.mjs",
|
|
5929
|
-
"vitest.config.cjs",
|
|
5930
|
-
"vite.config.ts",
|
|
5931
|
-
"vite.config.mts",
|
|
5932
|
-
"vite.config.cts",
|
|
5933
|
-
"vite.config.js",
|
|
5934
|
-
"vite.config.mjs",
|
|
5935
|
-
"vite.config.cjs"
|
|
5936
|
-
];
|
|
5937
|
-
function detectVitestDir(dir, ctx = diskRepoAccess(dir)) {
|
|
5938
|
-
for (const name of VITEST_CONFIG_FILES) {
|
|
5939
|
-
const p = join8(dir, name);
|
|
5940
|
-
let src;
|
|
5941
|
-
try {
|
|
5942
|
-
src = accessText(ctx, p);
|
|
5943
|
-
} catch (e) {
|
|
5944
|
-
debugPreparationOverflowStack(e);
|
|
5945
|
-
throw new TargetDetectionError(
|
|
5946
|
-
`${name} at ${dir} could not be read: ${e instanceof Error ? e.message : "unreadable"}`
|
|
5947
|
-
);
|
|
5810
|
+
if (input.generatedFiles.length > 0) {
|
|
5811
|
+
lines.push(" generatedFiles: # written by your tooling during a run; an undeclared change aborts it");
|
|
5812
|
+
for (const generated of input.generatedFiles) {
|
|
5813
|
+
lines.push(...commentLines(generated.evidence, " "), ` - ${yamlScalar(generated.path, quote)}`);
|
|
5948
5814
|
}
|
|
5949
|
-
if (src === null) continue;
|
|
5950
|
-
return vitestDirIn(src);
|
|
5951
5815
|
}
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
const open = firstUnmasked(source, masked, /(?:^|[\s,;{])["']?test["']?\s*:\s*\{/gu);
|
|
5957
|
-
if (open === -1) return void 0;
|
|
5958
|
-
let depth = 0;
|
|
5959
|
-
let close = -1;
|
|
5960
|
-
for (let at = open; at < masked.length; at++) {
|
|
5961
|
-
if (masked[at] === "{") depth += 1;
|
|
5962
|
-
else if (masked[at] === "}") {
|
|
5963
|
-
depth -= 1;
|
|
5964
|
-
if (depth === 0) {
|
|
5965
|
-
close = at;
|
|
5966
|
-
break;
|
|
5967
|
-
}
|
|
5816
|
+
if (input.rewrittenFiles.length > 0) {
|
|
5817
|
+
lines.push(" rewrittenFiles: # committed files your own install rewrites; measured as your install left them");
|
|
5818
|
+
for (const rewritten of input.rewrittenFiles) {
|
|
5819
|
+
lines.push(...commentLines(rewritten.evidence, " "), ` - ${yamlScalar(rewritten.path, quote)}`);
|
|
5968
5820
|
}
|
|
5969
5821
|
}
|
|
5970
|
-
|
|
5971
|
-
const quote = firstUnmasked(source.slice(0, close), masked.slice(0, close), /\bdir["']?\s*:\s*['"`]/gu, open);
|
|
5972
|
-
if (quote === -1) return void 0;
|
|
5973
|
-
const end = masked.indexOf(source[quote], quote + 1);
|
|
5974
|
-
if (end === -1 || end >= close) return void 0;
|
|
5975
|
-
const value = source.slice(quote + 1, end);
|
|
5976
|
-
return value === "" ? void 0 : value;
|
|
5822
|
+
return lines;
|
|
5977
5823
|
}
|
|
5978
|
-
function
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5824
|
+
function renderConfig(input) {
|
|
5825
|
+
const coverage = input.coverageNote == null ? [] : [...commentLines(input.coverageNote, "", 80), ""];
|
|
5826
|
+
const lines = [
|
|
5827
|
+
...coverage,
|
|
5828
|
+
...configHeaderLines("init"),
|
|
5829
|
+
...configSettingLines(),
|
|
5830
|
+
/* AFTER THE TWO SETTINGS EVERY FILE CARRIES AND BEFORE THE JOB, because it is a setting of the
|
|
5831
|
+
same kind - a rule about the whole run rather than a fact about this repository's CI. */
|
|
5832
|
+
...configFlakyLines(input.flaky),
|
|
5833
|
+
/* BEFORE THE ENVIRONMENT BLOCK, because it is the shortest answer to the first question a
|
|
5834
|
+
reviewer of this pull request asks - "which of our jobs is this running in" - and the
|
|
5835
|
+
environment block below is the long answer to a different one. */
|
|
5836
|
+
...configSetupJobLines(
|
|
5837
|
+
input.setupJob,
|
|
5838
|
+
input.setupMatrixLeg,
|
|
5839
|
+
input.setupFailFast,
|
|
5840
|
+
input.setupStepUnreadableConditions,
|
|
5841
|
+
input.yamlQuote ?? '"'
|
|
5842
|
+
),
|
|
5843
|
+
...renderEnvironmentBlock({ ...input, coverageNote: null })
|
|
5844
|
+
];
|
|
5845
|
+
return `${lines.join("\n")}
|
|
5846
|
+
`;
|
|
5847
|
+
}
|
|
5848
|
+
function renderWorkflow(input) {
|
|
5849
|
+
return readableWorkflow(workflowYaml({
|
|
5850
|
+
node: input.workflowNode,
|
|
5851
|
+
/* THE ONE RECIPE, NAMED. `environment.setup` when the repository declares its own path, so a
|
|
5852
|
+
re-run of `init` over a customer's chosen path writes a workflow that calls that path. */
|
|
5853
|
+
setupScriptPath: input.setupPath ?? SETUP_SCRIPT_PATH,
|
|
5854
|
+
/* The same directory the sealed image runs those steps in - `installWorkdir` in the runner -
|
|
5855
|
+
because a recipe that installs in one directory on the runner and another in the proof image
|
|
5856
|
+
is two recipes again. */
|
|
5857
|
+
setupWorkingDirectory: input.installDirectory,
|
|
5858
|
+
runtimes: input.runtimes.map((need) => ({
|
|
5859
|
+
name: need.runtime,
|
|
5860
|
+
version: need.runtime === "bun" ? SEALED_BUN_VERSION : SEALED_DENO_VERSION,
|
|
5861
|
+
evidence: need.evidence
|
|
5862
|
+
}))
|
|
5863
|
+
}));
|
|
5864
|
+
}
|
|
5865
|
+
function readableWorkflow(text) {
|
|
5866
|
+
try {
|
|
5867
|
+
parse2(text);
|
|
5868
|
+
} catch (error) {
|
|
5869
|
+
throw new DeclaredRefusalError2(refusalFromFacts2({
|
|
5870
|
+
kind: "setup-step-written",
|
|
5871
|
+
workflow: WORKFLOW_FILE_PATH,
|
|
5872
|
+
job: WORKFLOW_JOB_ID,
|
|
5873
|
+
why: `it does not parse as YAML: ${error instanceof Error ? error.message : "unknown error"}`
|
|
5874
|
+
}));
|
|
5983
5875
|
}
|
|
5984
|
-
return
|
|
5876
|
+
return text;
|
|
5985
5877
|
}
|
|
5986
|
-
function
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
}
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
if (
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
at = Math.min(end + 1, source.length);
|
|
6000
|
-
previous = char;
|
|
6001
|
-
continue;
|
|
6002
|
-
}
|
|
6003
|
-
if (char === "/" && source[at + 1] === "/") {
|
|
6004
|
-
const end = source.indexOf("\n", at);
|
|
6005
|
-
blank(at, end === -1 ? source.length : end);
|
|
6006
|
-
at = end === -1 ? source.length : end;
|
|
5878
|
+
function testCommandReceipt(input) {
|
|
5879
|
+
return input.cost == null ? input.evidence : `${input.evidence}. ${input.cost}`;
|
|
5880
|
+
}
|
|
5881
|
+
function receiptLines(entries, indent, quote) {
|
|
5882
|
+
const bodyOf = (entry) => entry.value === null ? null : `${indent}${entry.key}: ${yamlScalar(entry.value, quote)}`;
|
|
5883
|
+
const itemBody = (item) => `${indent} - ${yamlScalar(item.value, quote)}`;
|
|
5884
|
+
const withReceipt = (body, receipt) => `${body} # ${receipt}`;
|
|
5885
|
+
const lines = [];
|
|
5886
|
+
for (const entry of entries) {
|
|
5887
|
+
if (entry.items !== void 0) {
|
|
5888
|
+
if (entry.items.length === 0) continue;
|
|
5889
|
+
lines.push(`${indent}${entry.key}:${entry.receipt === "" ? "" : ` # ${entry.receipt}`}`);
|
|
5890
|
+
for (const item of entry.items) lines.push(withReceipt(itemBody(item), item.receipt));
|
|
6007
5891
|
continue;
|
|
6008
5892
|
}
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
at = end === -1 ? source.length : end + 2;
|
|
5893
|
+
const body = bodyOf(entry);
|
|
5894
|
+
if (body === null) {
|
|
5895
|
+
if (entry.receipt !== "") lines.push(`${indent}# ${entry.key}: ${entry.receipt}`);
|
|
6013
5896
|
continue;
|
|
6014
5897
|
}
|
|
6015
|
-
|
|
6016
|
-
let end = at + 1;
|
|
6017
|
-
while (end < source.length && source[end] !== "/" && source[end] !== "\n") end += source[end] === "\\" ? 2 : 1;
|
|
6018
|
-
if (source[end] === "/") {
|
|
6019
|
-
blank(at, end + 1);
|
|
6020
|
-
at = end + 1;
|
|
6021
|
-
continue;
|
|
6022
|
-
}
|
|
6023
|
-
}
|
|
6024
|
-
if (!/\s/u.test(char)) previous = char;
|
|
6025
|
-
at += 1;
|
|
5898
|
+
lines.push(withReceipt(body, entry.receipt));
|
|
6026
5899
|
}
|
|
6027
|
-
return
|
|
5900
|
+
return lines;
|
|
6028
5901
|
}
|
|
6029
|
-
|
|
6030
|
-
|
|
5902
|
+
var ENVIRONMENT_BLOCK_HEADER = "# How CI rebuilds this project for abloh's check. Comments name the source file.";
|
|
5903
|
+
var RETIRED_ENVIRONMENT_BLOCK_HEADERS = [
|
|
5904
|
+
"# Rebuild recipe for the proof container. Comments name the source file."
|
|
5905
|
+
];
|
|
5906
|
+
function isEnvironmentBlockHeader(line) {
|
|
5907
|
+
return line === ENVIRONMENT_BLOCK_HEADER || RETIRED_ENVIRONMENT_BLOCK_HEADERS.includes(line);
|
|
6031
5908
|
}
|
|
6032
|
-
|
|
6033
|
-
|
|
5909
|
+
var STEP_WHAT = {
|
|
5910
|
+
systemPackages: "OS packages your suite needs, pinned",
|
|
5911
|
+
corepack: "put the package manager on PATH, for your own lifecycle scripts",
|
|
5912
|
+
install: "dependencies, from your lockfile",
|
|
5913
|
+
/**
|
|
5914
|
+
* THE SAME STEP ON A REPOSITORY THAT CARRIES NO LOCKFILE (census run 7 F3, 2026-09-06).
|
|
5915
|
+
*
|
|
5916
|
+
* A SECOND SPELLING RATHER THAN A REWORD, on the rule above: `install`'s exact text is what a
|
|
5917
|
+
* re-run recognizes as its own step, so changing it would orphan every install step already
|
|
5918
|
+
* committed. Both are install steps to {@link initStepRole} and to {@link isInstallStep}, which is
|
|
5919
|
+
* what every reader asks instead of comparing against one of them.
|
|
5920
|
+
*
|
|
5921
|
+
* IT EXISTS BECAUSE THE OTHER ONE WOULD BE FALSE. `fastify/fastify` and `pinojs/pino` both
|
|
5922
|
+
* `.gitignore` their lockfile, so the drafted step resolves rather than installing frozen, and a
|
|
5923
|
+
* comment saying "from your lockfile" above a command that has none is a claim the file cannot
|
|
5924
|
+
* back. The `source` beside it names the ignore rule.
|
|
5925
|
+
*/
|
|
5926
|
+
installUnlocked: "dependencies, resolved - your repository commits no lockfile",
|
|
5927
|
+
build: "your build"
|
|
5928
|
+
};
|
|
5929
|
+
function isInstallStep(what) {
|
|
5930
|
+
return what === STEP_WHAT.install || what === STEP_WHAT.installUnlocked;
|
|
6034
5931
|
}
|
|
6035
|
-
function
|
|
6036
|
-
|
|
5932
|
+
function setupStepOrigin(step) {
|
|
5933
|
+
const role = initStepRole(step.what);
|
|
5934
|
+
if (role === null) return CUSTOMER_TYPED;
|
|
5935
|
+
return role === "setupCommands" ? "abloh-composed" : "abloh-derived";
|
|
6037
5936
|
}
|
|
6038
|
-
function
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
5937
|
+
function initStepRole(what) {
|
|
5938
|
+
if (what === STEP_WHAT.systemPackages) return "systemPackages";
|
|
5939
|
+
if (what === STEP_WHAT.corepack || isInstallStep(what)) return "installCommand";
|
|
5940
|
+
if (what === STEP_WHAT.build) return "setupCommands";
|
|
5941
|
+
return null;
|
|
5942
|
+
}
|
|
5943
|
+
function systemPackagesStep(pinned) {
|
|
5944
|
+
if (pinned.length === 0) return null;
|
|
5945
|
+
return {
|
|
5946
|
+
what: STEP_WHAT.systemPackages,
|
|
5947
|
+
source: [...new Set(pinned.map((entry) => entry.source))].join(", "),
|
|
5948
|
+
/* THE SAME COMMAND THE IMAGE ALREADY RAN, one line per operation rather than one semicolon
|
|
5949
|
+
chain, because this file is read by a person now. `set -e` is in the header, so the chain's
|
|
5950
|
+
own guard is redundant here and its semicolons are noise. */
|
|
5951
|
+
command: systemPackageInstallCommand(pinned, { pin: "optional" }).replace(/^set -e;\s*/u, "").split(";").map((part) => part.trim()).filter((part) => part !== "").join("\n")
|
|
5952
|
+
};
|
|
5953
|
+
}
|
|
5954
|
+
function stepBody(env, command) {
|
|
5955
|
+
if (env.length === 0) return command;
|
|
5956
|
+
return [
|
|
5957
|
+
...[...env].sort((left, right) => left.name.localeCompare(right.name)).map((value) => `export ${value.name}=${shellSingleQuoted(value.value)}`),
|
|
5958
|
+
command
|
|
5959
|
+
].join("\n");
|
|
5960
|
+
}
|
|
5961
|
+
function shellSingleQuoted(value) {
|
|
5962
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
5963
|
+
}
|
|
5964
|
+
function setupScriptSteps(input) {
|
|
5965
|
+
const steps = [];
|
|
5966
|
+
const pinned = systemPackagesStep(input.systemPackages);
|
|
5967
|
+
if (pinned !== null) steps.push(pinned);
|
|
5968
|
+
if (input.installCommand !== null) {
|
|
5969
|
+
if (installCommandNeedsCorepackEnable(parseLiteralArguments2(input.installCommand))) {
|
|
5970
|
+
steps.push({
|
|
5971
|
+
what: STEP_WHAT.corepack,
|
|
5972
|
+
source: input.evidence.installCommand,
|
|
5973
|
+
command: "corepack enable"
|
|
5974
|
+
});
|
|
6049
5975
|
}
|
|
6050
|
-
|
|
6051
|
-
|
|
5976
|
+
steps.push({
|
|
5977
|
+
/* The step and its comment are one statement: a resolved install may not be introduced as
|
|
5978
|
+
coming from a lockfile the commit does not hold. See {@link ConfigInput.installUnlocked}. */
|
|
5979
|
+
what: input.installUnlocked === true ? STEP_WHAT.installUnlocked : STEP_WHAT.install,
|
|
5980
|
+
source: input.evidence.installCommand,
|
|
5981
|
+
/* THE COMMAND IS ABLOH'S AND THE ENVIRONMENT IS THE CUSTOMER'S. See
|
|
5982
|
+
{@link ConfigInput.installEnvironment}: the command comes from the lockfile and the values
|
|
5983
|
+
come from the step their own CI installs with. */
|
|
5984
|
+
command: stepBody(input.installEnvironment, input.installCommand)
|
|
5985
|
+
});
|
|
5986
|
+
}
|
|
5987
|
+
for (const entry of input.setupCommands) {
|
|
5988
|
+
steps.push({ what: STEP_WHAT.build, source: entry.source, command: stepBody(entry.env, entry.command) });
|
|
5989
|
+
}
|
|
5990
|
+
return steps;
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5993
|
+
// src/setup-question-descriptor.ts
|
|
5994
|
+
import {
|
|
5995
|
+
judgeConfigKeyValue,
|
|
5996
|
+
SETUP_FAIL_FAST_ANSWERS,
|
|
5997
|
+
SETUP_FAIL_FAST_CONSEQUENCES
|
|
5998
|
+
} from "@abloh/core";
|
|
5999
|
+
var ATOM = /\s*,\s*/u;
|
|
6000
|
+
var COMMANDS = /\s*&&\s*/u;
|
|
6001
|
+
function splitAnswerList(shape, raw) {
|
|
6002
|
+
const trimmed = raw.trim();
|
|
6003
|
+
if (trimmed === "") return [];
|
|
6004
|
+
return trimmed.split(shape.kind === "commands" ? COMMANDS : ATOM).map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
6005
|
+
}
|
|
6006
|
+
function splitAtFirstEquals(entry) {
|
|
6007
|
+
const at = entry.indexOf("=");
|
|
6008
|
+
return at === -1 ? [entry, null] : [entry.slice(0, at), entry.slice(at + 1)];
|
|
6009
|
+
}
|
|
6010
|
+
var SERVICE_ENTRY = /^(?<image>.*@sha256:[a-f0-9]{64})(?::(?<port>\d{1,5}))?$/u;
|
|
6011
|
+
function readServiceEntries(raw) {
|
|
6012
|
+
return splitAnswerList({ kind: "list" }, raw).map((entry) => {
|
|
6013
|
+
const [name, rest] = splitAtFirstEquals(entry);
|
|
6014
|
+
const gate = rest === null ? null : SERVICE_ENTRY.exec(rest);
|
|
6015
|
+
if (gate === null) return { name, image: rest ?? "" };
|
|
6016
|
+
const port = gate.groups?.["port"];
|
|
6017
|
+
return {
|
|
6018
|
+
name,
|
|
6019
|
+
image: gate.groups?.["image"] ?? "",
|
|
6020
|
+
...port === void 0 ? {} : { healthPort: Number(port) }
|
|
6021
|
+
};
|
|
6022
|
+
});
|
|
6023
|
+
}
|
|
6024
|
+
function readAnswerValue(descriptor, raw) {
|
|
6025
|
+
switch (descriptor.shape.kind) {
|
|
6026
|
+
case "scalar":
|
|
6027
|
+
return raw.trim();
|
|
6028
|
+
case "list":
|
|
6029
|
+
return splitAnswerList(descriptor.shape, raw);
|
|
6030
|
+
case "commands":
|
|
6031
|
+
return splitAnswerList(descriptor.shape, raw);
|
|
6032
|
+
case "names":
|
|
6033
|
+
return splitAnswerList(descriptor.shape, raw);
|
|
6034
|
+
case "packages":
|
|
6035
|
+
return splitAnswerList(descriptor.shape, raw).map((entry) => {
|
|
6036
|
+
const [name, version] = splitAtFirstEquals(entry);
|
|
6037
|
+
return version === null ? { name } : { name, version };
|
|
6038
|
+
});
|
|
6039
|
+
case "services":
|
|
6040
|
+
return readServiceEntries(raw);
|
|
6041
|
+
case "pairs":
|
|
6042
|
+
return Object.fromEntries(
|
|
6043
|
+
splitAnswerList(descriptor.shape, raw).map((entry) => {
|
|
6044
|
+
const [name, value] = splitAtFirstEquals(entry);
|
|
6045
|
+
return [name, value ?? ""];
|
|
6046
|
+
})
|
|
6047
|
+
);
|
|
6052
6048
|
}
|
|
6053
|
-
return false;
|
|
6054
6049
|
}
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
const
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
for (const dir of enumeration.packages) {
|
|
6063
|
-
const { probe, refusal } = probePkg(ctx, join8(repoDir, dir), aliasFamily, repoDir, aggregatorTasks);
|
|
6064
|
-
if (refusal !== null) {
|
|
6065
|
-
if (refusals !== void 0) refusals.push(refusal.summary);
|
|
6066
|
-
continue;
|
|
6067
|
-
}
|
|
6068
|
-
if (probe === null) continue;
|
|
6069
|
-
if (probe.runner !== null || declaresASuite(probe)) found.push(dir);
|
|
6050
|
+
function setupAnswerProblem(key, raw) {
|
|
6051
|
+
const descriptor = SETUP_QUESTION_DESCRIPTORS[key];
|
|
6052
|
+
if (descriptor === void 0) return `abloh asks no question about ${key}`;
|
|
6053
|
+
const value = raw.trim();
|
|
6054
|
+
if (value === "") return descriptor.required ? "this question needs an answer" : null;
|
|
6055
|
+
if (descriptor.candidates.length > 0 && !descriptor.candidates.includes(value)) {
|
|
6056
|
+
return `${value} is not one of ${descriptor.candidates.join(", ")}`;
|
|
6070
6057
|
}
|
|
6071
|
-
return
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
const runs = scriptRuns2({ scripts: probe.scripts, text: probe.testScript });
|
|
6076
|
-
if (runs.length === 0) return true;
|
|
6077
|
-
return runs.some((run) => run.kind === "test" || run.kind === "unknown");
|
|
6058
|
+
if (descriptor.key !== "target.directory" && descriptor.channel === "discovery") return null;
|
|
6059
|
+
const verdict = judgeConfigKeyValue(key, readAnswerValue(descriptor, raw));
|
|
6060
|
+
if (verdict.ok) return null;
|
|
6061
|
+
return verdict.at === null || verdict.at === void 0 ? verdict.reason : `${verdict.at} ${verdict.reason}`;
|
|
6078
6062
|
}
|
|
6079
|
-
function
|
|
6080
|
-
|
|
6081
|
-
const
|
|
6082
|
-
|
|
6083
|
-
|
|
6063
|
+
function applyAnswersToContract(draft, answers) {
|
|
6064
|
+
let out = draft;
|
|
6065
|
+
for (const descriptor of Object.values(SETUP_QUESTION_DESCRIPTORS)) {
|
|
6066
|
+
if (descriptor.apply === void 0) continue;
|
|
6067
|
+
const raw = answers[descriptor.key];
|
|
6068
|
+
if (raw === void 0 || raw.trim() === "") continue;
|
|
6069
|
+
out = descriptor.apply(out, raw);
|
|
6084
6070
|
}
|
|
6085
|
-
return
|
|
6071
|
+
return out;
|
|
6086
6072
|
}
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
const named = candidates.slice(0, NAMED_CANDIDATES);
|
|
6091
|
-
const more = candidates.length - named.length;
|
|
6092
|
-
return `. The packages here that declare a suite are ${named.join(", ")}` + (more > 0 ? ` and ${more} more` : "");
|
|
6073
|
+
var ANSWERED_EVIDENCE = "you answered this when you set abloh up";
|
|
6074
|
+
function evidenceFor(entries) {
|
|
6075
|
+
return entries.map((path) => ({ path, evidence: ANSWERED_EVIDENCE }));
|
|
6093
6076
|
}
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6077
|
+
var SETUP_QUESTION_DESCRIPTORS = {
|
|
6078
|
+
"environment.installCommand": {
|
|
6079
|
+
key: "environment.installCommand",
|
|
6080
|
+
ask: "How does this repository install its dependencies?",
|
|
6081
|
+
why: "Abloh read your CI without running it, so it saw the command your workflow declares and not what your install actually writes. On the borrow road your own job installs and Abloh measures inside it, so this is only needed when Abloh builds for itself.",
|
|
6082
|
+
candidates: [],
|
|
6083
|
+
required: false,
|
|
6084
|
+
unreadable: true,
|
|
6085
|
+
shape: { kind: "scalar" },
|
|
6086
|
+
channel: "contract",
|
|
6087
|
+
apply: (draft, answer) => ({
|
|
6088
|
+
...draft,
|
|
6089
|
+
installCommand: answer.trim(),
|
|
6090
|
+
evidence: { ...draft.evidence, installCommand: ANSWERED_EVIDENCE }
|
|
6091
|
+
})
|
|
6092
|
+
},
|
|
6093
|
+
"environment.setupCommands": {
|
|
6094
|
+
key: "environment.setupCommands",
|
|
6095
|
+
ask: "What does this repository build between installing and testing?",
|
|
6096
|
+
why: "Abloh read the build steps out of the job it rides. A step it could not read is one the proof container never runs, and a suite that needs it then fails inside abloh rather than in your CI.",
|
|
6097
|
+
candidates: [],
|
|
6098
|
+
required: true,
|
|
6099
|
+
/* ONE STEP PER ` && `, WHICH IS THE ONE KEY THAT IS NOT COMMA-SEPARATED and the CLI's prompt says
|
|
6100
|
+
so for the same reason: a comma is an ordinary character inside a command, and a build is read
|
|
6101
|
+
top to bottom. `splitAnswerList` is the one grammar both roads ask. */
|
|
6102
|
+
shape: { kind: "commands" },
|
|
6103
|
+
channel: "contract",
|
|
6104
|
+
apply: (draft, answer) => ({
|
|
6105
|
+
...draft,
|
|
6106
|
+
setupCommands: splitAnswerList({ kind: "commands" }, answer).map((command) => ({
|
|
6107
|
+
command,
|
|
6108
|
+
source: ANSWERED_EVIDENCE,
|
|
6109
|
+
/* A COMMAND SOMEBODY TYPED CARRIES THE ENVIRONMENT THEY TYPED INTO IT. `CiSetupCommand.env`
|
|
6110
|
+
is the environment abloh READ off a workflow step, so an answered command has none: what
|
|
6111
|
+
the answer says is the whole of what this command runs. */
|
|
6112
|
+
env: []
|
|
6113
|
+
}))
|
|
6114
|
+
})
|
|
6115
|
+
},
|
|
6116
|
+
"environment.runtimeImage": {
|
|
6117
|
+
key: "environment.runtimeImage",
|
|
6118
|
+
ask: "Which image should Abloh build its proof container from?",
|
|
6119
|
+
why: "Abloh pins one by pulling it and reading its digest, which needs a machine. Left unanswered, a run uses the reviewed default for the Node your repository declares.",
|
|
6120
|
+
candidates: [],
|
|
6121
|
+
required: false,
|
|
6122
|
+
unreadable: true,
|
|
6123
|
+
shape: { kind: "scalar" },
|
|
6124
|
+
channel: "contract",
|
|
6125
|
+
apply: (draft, answer) => ({
|
|
6126
|
+
...draft,
|
|
6127
|
+
runtimeImage: answer.trim(),
|
|
6128
|
+
evidence: { ...draft.evidence, runtimeImage: ANSWERED_EVIDENCE }
|
|
6129
|
+
})
|
|
6130
|
+
},
|
|
6131
|
+
"setup.job": {
|
|
6132
|
+
key: "setup.job",
|
|
6133
|
+
ask: "Which job should abloh measure in?",
|
|
6134
|
+
why: "Everything else in the contract is read out of that one job.",
|
|
6135
|
+
candidates: [],
|
|
6136
|
+
required: true,
|
|
6137
|
+
shape: { kind: "scalar" },
|
|
6138
|
+
channel: "discovery"
|
|
6139
|
+
},
|
|
6140
|
+
"setup.matrixLeg": {
|
|
6141
|
+
key: "setup.matrixLeg",
|
|
6142
|
+
ask: "Which leg of that job's matrix?",
|
|
6143
|
+
why: "The job abloh rides fans out, and a leg names one value per dimension of it.",
|
|
6144
|
+
candidates: [],
|
|
6145
|
+
required: true,
|
|
6146
|
+
shape: { kind: "scalar" },
|
|
6147
|
+
channel: "discovery"
|
|
6148
|
+
},
|
|
6149
|
+
"setup.failFast": {
|
|
6150
|
+
key: "setup.failFast",
|
|
6151
|
+
ask: "That job cancels every matrix leg when one fails. May abloh turn that off for it?",
|
|
6152
|
+
/* THE VALUES THE CONTRACT ACCEPTS, on `flaky`'s own rule: the card writes the chosen candidate
|
|
6153
|
+
into `abloh.yml`, so a candidate that read well and parsed badly would be a form that writes an
|
|
6154
|
+
invalid file. What each one MEANS is the ask above it. */
|
|
6155
|
+
why: SETUP_FAIL_FAST_ANSWERS.map((answer) => `${answer} ${SETUP_FAIL_FAST_CONSEQUENCES[answer]}.`).join(" "),
|
|
6156
|
+
candidates: SETUP_FAIL_FAST_ANSWERS,
|
|
6157
|
+
required: true,
|
|
6158
|
+
shape: { kind: "scalar" },
|
|
6159
|
+
/* A LINE IN THE FILE AND NOT A RE-DERIVATION. It does not move which job the recipe is read out
|
|
6160
|
+
of - it is the maintainer's standing answer about the job already chosen - and
|
|
6161
|
+
`configSetupJobLines` writes it on every road, with or without a job, because it is the one
|
|
6162
|
+
statement in that block that was never abloh's to discard. */
|
|
6163
|
+
channel: "contract",
|
|
6164
|
+
apply: (draft, answer) => ({ ...draft, setupFailFast: answer.trim() })
|
|
6165
|
+
},
|
|
6166
|
+
flaky: {
|
|
6167
|
+
key: "flaky",
|
|
6168
|
+
ask: "Your suite was red on untouched code. What should abloh do about those tests?",
|
|
6169
|
+
/* THE VALUES THE CONTRACT ACCEPTS, not a prose label: the card writes the chosen candidate into
|
|
6170
|
+
`abloh.yml`, so a candidate that read well and parsed badly would be a form that writes an
|
|
6171
|
+
invalid file. What each one MEANS is the ask above it. */
|
|
6172
|
+
why: "quarantine sets them aside and measures the rest. strict measures nothing until they pass.",
|
|
6173
|
+
candidates: ["quarantine", "strict"],
|
|
6174
|
+
required: true,
|
|
6175
|
+
shape: { kind: "scalar" },
|
|
6176
|
+
channel: "contract",
|
|
6177
|
+
/* `quarantine` WRITES NO LINE and that is not the answer being dropped: it is what a file with
|
|
6178
|
+
no `flaky:` key already means, which `configFlakyLines` states at the renderer. */
|
|
6179
|
+
apply: (draft, answer) => ({ ...draft, flaky: answer.trim() })
|
|
6180
|
+
},
|
|
6181
|
+
"environment.testCommand": {
|
|
6182
|
+
key: "environment.testCommand",
|
|
6183
|
+
ask: "Which command runs your tests?",
|
|
6184
|
+
why: "Abloh read your CI without running it, and the command it found did not run a suite it could observe.",
|
|
6185
|
+
candidates: [],
|
|
6186
|
+
required: true,
|
|
6187
|
+
shape: { kind: "scalar" },
|
|
6188
|
+
channel: "contract",
|
|
6189
|
+
apply: (draft, answer) => ({ ...draft, testCommand: answer.trim() })
|
|
6190
|
+
},
|
|
6191
|
+
"environment.sealedTestCommand": {
|
|
6192
|
+
key: "environment.sealedTestCommand",
|
|
6193
|
+
ask: "Which command runs your tests inside abloh's proof container?",
|
|
6194
|
+
why: "Your own test script did something abloh's container could not, so it needs the runner on its own.",
|
|
6195
|
+
candidates: [],
|
|
6196
|
+
required: true,
|
|
6197
|
+
shape: { kind: "scalar" },
|
|
6198
|
+
channel: "contract",
|
|
6199
|
+
apply: (draft, answer) => ({ ...draft, sealedTestCommand: answer.trim() })
|
|
6200
|
+
},
|
|
6201
|
+
"environment.rebuildCommand": {
|
|
6202
|
+
key: "environment.rebuildCommand",
|
|
6203
|
+
ask: "Which command builds the output your tests import?",
|
|
6204
|
+
why: "Your tests read compiled output rather than the source abloh plants in, so without the build every planted bug is reported as missed. Abloh cannot resolve which of your build scripts writes it from your files alone.",
|
|
6205
|
+
candidates: [],
|
|
6206
|
+
required: true,
|
|
6207
|
+
shape: { kind: "scalar" },
|
|
6208
|
+
channel: "contract",
|
|
6209
|
+
/* THE CANDIDATES AND THE MANAGER SURVIVE THE ANSWER, because they are the drafting's reading of
|
|
6210
|
+
this repository and the answer is a value: a run that later clears the value would otherwise
|
|
6211
|
+
lose the finding that made the key worth writing. Absent, they are the empty pair, which is
|
|
6212
|
+
what a draft that found no build script already carries. */
|
|
6213
|
+
apply: (draft, answer) => ({
|
|
6214
|
+
...draft,
|
|
6215
|
+
rebuild: {
|
|
6216
|
+
...draft.rebuild ?? { candidates: [], manager: "" },
|
|
6217
|
+
value: answer.trim(),
|
|
6218
|
+
evidence: ANSWERED_EVIDENCE
|
|
6219
|
+
}
|
|
6220
|
+
})
|
|
6221
|
+
},
|
|
6222
|
+
"target.directory": {
|
|
6223
|
+
key: "target.directory",
|
|
6224
|
+
ask: "Which package should abloh measure?",
|
|
6225
|
+
why: "This repository holds several, and a run measures one.",
|
|
6226
|
+
candidates: [],
|
|
6227
|
+
required: true,
|
|
6228
|
+
shape: { kind: "scalar" },
|
|
6229
|
+
/* A DISCOVERY ANSWER AND NOT A LINE. The runner, the test command and the whole measurement plan
|
|
6230
|
+
are read out of the package being measured, so writing this key over a draft derived from the
|
|
6231
|
+
root would leave every sibling value describing a different package. It goes back through
|
|
6232
|
+
`resolveMeasurementPlan` as its `subdir`, exactly as the job pin goes back through the job. */
|
|
6233
|
+
channel: "discovery"
|
|
6234
|
+
},
|
|
6235
|
+
"environment.coverageProvider": {
|
|
6236
|
+
key: "environment.coverageProvider",
|
|
6237
|
+
ask: "May abloh install the coverage provider your runner needs?",
|
|
6238
|
+
why: "Your tree does not carry one, and without it abloh cannot say which of your changed lines your tests execute.",
|
|
6239
|
+
candidates: ["install", "skip"],
|
|
6240
|
+
required: true,
|
|
6241
|
+
shape: { kind: "scalar" },
|
|
6242
|
+
channel: "contract",
|
|
6243
|
+
/* WRITTEN FOR BOTH ANSWERS, because a recorded answer is the difference between a maintainer
|
|
6244
|
+
who chose the default and one who was never asked - see `ConfigInput.coverageProvider`. */
|
|
6245
|
+
apply: (draft, answer) => ({ ...draft, coverageProvider: answer.trim() })
|
|
6246
|
+
},
|
|
6247
|
+
"environment.requiredVariables": {
|
|
6248
|
+
key: "environment.requiredVariables",
|
|
6249
|
+
ask: "Which environment variables does your suite need?",
|
|
6250
|
+
why: "Names only. The values come from your own environment and are masked in every log.",
|
|
6251
|
+
candidates: [],
|
|
6252
|
+
required: true,
|
|
6253
|
+
shape: { kind: "names" },
|
|
6254
|
+
channel: "contract",
|
|
6255
|
+
apply: (draft, answer) => ({
|
|
6256
|
+
...draft,
|
|
6257
|
+
requiredVariables: splitAnswerList({ kind: "names" }, answer).map((name) => ({
|
|
6258
|
+
name,
|
|
6259
|
+
evidence: ANSWERED_EVIDENCE
|
|
6260
|
+
}))
|
|
6261
|
+
})
|
|
6262
|
+
},
|
|
6263
|
+
"environment.environmentValues": {
|
|
6264
|
+
key: "environment.environmentValues",
|
|
6265
|
+
ask: "Which literal values does your suite need in its environment?",
|
|
6266
|
+
why: "A secret goes in requiredVariables by name instead, and never here.",
|
|
6267
|
+
candidates: [],
|
|
6268
|
+
required: true,
|
|
6269
|
+
shape: { kind: "pairs" },
|
|
6270
|
+
channel: "contract",
|
|
6271
|
+
apply: (draft, answer) => ({
|
|
6272
|
+
...draft,
|
|
6273
|
+
environmentValues: splitAnswerList({ kind: "pairs" }, answer).map((entry) => {
|
|
6274
|
+
const [name, value] = splitAtFirstEquals(entry);
|
|
6275
|
+
return { name, value: value ?? "", evidence: ANSWERED_EVIDENCE };
|
|
6276
|
+
})
|
|
6277
|
+
})
|
|
6278
|
+
},
|
|
6279
|
+
"environment.generatedFiles": {
|
|
6280
|
+
key: "environment.generatedFiles",
|
|
6281
|
+
ask: "Which directories does your own tooling write during a run?",
|
|
6282
|
+
why: "An undeclared change to the checkout aborts a run, and abloh cannot tell yours from a stray one.",
|
|
6283
|
+
candidates: [],
|
|
6284
|
+
required: true,
|
|
6285
|
+
shape: { kind: "list" },
|
|
6286
|
+
channel: "contract",
|
|
6287
|
+
apply: (draft, answer) => ({
|
|
6288
|
+
...draft,
|
|
6289
|
+
generatedFiles: evidenceFor(splitAnswerList({ kind: "list" }, answer))
|
|
6290
|
+
})
|
|
6291
|
+
},
|
|
6292
|
+
"environment.rewrittenFiles": {
|
|
6293
|
+
key: "environment.rewrittenFiles",
|
|
6294
|
+
ask: "Which committed files does your own install rewrite?",
|
|
6295
|
+
why: "A rate measured over bytes your install changed is a rate about something else.",
|
|
6296
|
+
candidates: [],
|
|
6297
|
+
required: true,
|
|
6298
|
+
shape: { kind: "list" },
|
|
6299
|
+
channel: "contract",
|
|
6300
|
+
apply: (draft, answer) => ({
|
|
6301
|
+
...draft,
|
|
6302
|
+
rewrittenFiles: evidenceFor(splitAnswerList({ kind: "list" }, answer))
|
|
6303
|
+
})
|
|
6304
|
+
},
|
|
6305
|
+
"environment.identityFiles": {
|
|
6306
|
+
key: "environment.identityFiles",
|
|
6307
|
+
ask: "Which files should a change to reinstall rather than reuse the container?",
|
|
6308
|
+
why: "Abloh reuses a prepared container between runs unless one of these moved.",
|
|
6309
|
+
candidates: [],
|
|
6310
|
+
required: true,
|
|
6311
|
+
shape: { kind: "list" },
|
|
6312
|
+
channel: "contract",
|
|
6313
|
+
apply: (draft, answer) => ({
|
|
6314
|
+
...draft,
|
|
6315
|
+
identityFiles: splitAnswerList({ kind: "list" }, answer)
|
|
6316
|
+
})
|
|
6317
|
+
},
|
|
6318
|
+
"environment.systemPackages": {
|
|
6319
|
+
key: "environment.systemPackages",
|
|
6320
|
+
ask: "Which OS packages does your build need?",
|
|
6321
|
+
why: "The reviewed proof image is slim, so a native dependency that compiles on install needs its toolchain named.",
|
|
6322
|
+
candidates: [],
|
|
6323
|
+
required: true,
|
|
6324
|
+
shape: { kind: "packages" },
|
|
6325
|
+
channel: "contract",
|
|
6326
|
+
apply: (draft, answer) => ({
|
|
6327
|
+
...draft,
|
|
6328
|
+
systemPackages: splitAnswerList({ kind: "packages" }, answer).map((entry) => {
|
|
6329
|
+
const [name, version] = splitAtFirstEquals(entry);
|
|
6330
|
+
return { name, version, source: ANSWERED_EVIDENCE };
|
|
6331
|
+
})
|
|
6332
|
+
})
|
|
6333
|
+
},
|
|
6334
|
+
"environment.runtimes": {
|
|
6335
|
+
key: "environment.runtimes",
|
|
6336
|
+
ask: "Which runtimes beyond Node does your suite need?",
|
|
6337
|
+
/* NO CLOSED CANDIDATE LIST, because this key takes SEVERAL: `setupAnswerProblem` compares a
|
|
6338
|
+
closed list against the WHOLE answer, so `bun, deno` would be refused by a two-word list that
|
|
6339
|
+
accepts each of them alone. `judgeConfigKeyValue` names the two the contract holds, entry by
|
|
6340
|
+
entry, which is the reading that fits a list. */
|
|
6341
|
+
why: "The proof image carries Node. A runtime your tests reach for and the image does not carry fails inside abloh rather than in your CI.",
|
|
6342
|
+
candidates: [],
|
|
6343
|
+
required: true,
|
|
6344
|
+
shape: { kind: "names" },
|
|
6345
|
+
channel: "contract",
|
|
6346
|
+
apply: (draft, answer) => ({
|
|
6347
|
+
...draft,
|
|
6348
|
+
runtimes: splitAnswerList({ kind: "names" }, answer).map((runtime) => ({
|
|
6349
|
+
runtime,
|
|
6350
|
+
evidence: ANSWERED_EVIDENCE
|
|
6351
|
+
}))
|
|
6352
|
+
})
|
|
6353
|
+
},
|
|
6354
|
+
"environment.services": {
|
|
6355
|
+
key: "environment.services",
|
|
6356
|
+
ask: "Which backing services does your suite need, and where is each one ready?",
|
|
6357
|
+
why: "A proof runs in the same network namespace as its services, so your suite reaches them on localhost exactly as in your CI. Abloh pins the image by digest because a mutable tag cannot be part of what a run binds, and each service needs something to wait on before the suite starts.",
|
|
6358
|
+
candidates: [],
|
|
6359
|
+
required: true,
|
|
6360
|
+
shape: { kind: "services" },
|
|
6361
|
+
channel: "contract",
|
|
6362
|
+
/* THE ENVIRONMENT IS NOT TYPED HERE and the entry says so at {@link SERVICE_ENTRY}: an image that
|
|
6363
|
+
needs credentials fails its readiness gate and reports that, which is the same narrow door the
|
|
6364
|
+
CLI's own typed entry goes through. */
|
|
6365
|
+
apply: (draft, answer) => ({
|
|
6366
|
+
...draft,
|
|
6367
|
+
services: readServiceEntries(answer).map((service) => ({
|
|
6368
|
+
name: service.name,
|
|
6369
|
+
spelling: `${service.name}=${service.image}`,
|
|
6370
|
+
image: service.image,
|
|
6371
|
+
env: [],
|
|
6372
|
+
healthCommand: null,
|
|
6373
|
+
healthPort: service.healthPort ?? null,
|
|
6374
|
+
source: ANSWERED_EVIDENCE,
|
|
6375
|
+
evidence: ANSWERED_EVIDENCE
|
|
6376
|
+
}))
|
|
6377
|
+
})
|
|
6378
|
+
},
|
|
6379
|
+
"environment.installDirectory": {
|
|
6380
|
+
key: "environment.installDirectory",
|
|
6381
|
+
ask: "Which directory owns the lockfile your install reads?",
|
|
6382
|
+
why: "A workspace package installs from the root that holds its lockfile, not from its own folder.",
|
|
6383
|
+
candidates: [],
|
|
6384
|
+
required: false,
|
|
6385
|
+
shape: { kind: "scalar" },
|
|
6386
|
+
channel: "contract",
|
|
6387
|
+
apply: (draft, answer) => ({ ...draft, installDirectory: answer.trim() })
|
|
6388
|
+
},
|
|
6389
|
+
"environment.runner": {
|
|
6390
|
+
key: "environment.runner",
|
|
6391
|
+
ask: "Which test runner does this repository use?",
|
|
6392
|
+
why: "Abloh reads it from your own command, and a declaration outranks that reading.",
|
|
6393
|
+
candidates: [],
|
|
6394
|
+
required: false,
|
|
6395
|
+
shape: { kind: "scalar" },
|
|
6396
|
+
channel: "contract",
|
|
6397
|
+
apply: (draft, answer) => ({
|
|
6398
|
+
...draft,
|
|
6399
|
+
runner: answer.trim(),
|
|
6400
|
+
boundRunner: answer.trim(),
|
|
6401
|
+
evidence: { ...draft.evidence, runner: "declared in abloh.yml" }
|
|
6402
|
+
})
|
|
6131
6403
|
}
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
(
|
|
6404
|
+
};
|
|
6405
|
+
var ANSWERABLE_SETUP_KEYS = Object.keys(SETUP_QUESTION_DESCRIPTORS);
|
|
6406
|
+
function contractDescriptors() {
|
|
6407
|
+
return Object.values(SETUP_QUESTION_DESCRIPTORS).filter(
|
|
6408
|
+
(descriptor) => descriptor.channel === "contract"
|
|
6137
6409
|
);
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
const
|
|
6141
|
-
|
|
6142
|
-
const refusal = watchModeRefusal(
|
|
6143
|
-
runner,
|
|
6144
|
-
watchDeclaration,
|
|
6145
|
-
testCommand === projectTestCommand ? `scripts.test in ${join8(relative5(repoDir, probe.dir), "package.json")}` : probe.declaredCommand === testCommand ? "your declared environment.testCommand" : "your test command"
|
|
6146
|
-
);
|
|
6147
|
-
if (refusal !== null) throw new TargetDetectionError(refusal);
|
|
6148
|
-
}
|
|
6149
|
-
const watchNormalized = watchDeclaration === null ? null : withWatchModeDisabled({
|
|
6150
|
-
runner,
|
|
6151
|
-
command: testCommand,
|
|
6152
|
-
watchDeclaration,
|
|
6153
|
-
packageManagerMajor: manager.version === null ? null : Number.parseInt(manager.version, 10)
|
|
6154
|
-
});
|
|
6155
|
-
const effectiveTestCommand = watchNormalized ?? testCommand;
|
|
6156
|
-
const watchNormalizedStage = runnerStage === null ? null : withWatchModeDisabled({ runner, command: runnerStage, watchDeclaration: runnerStage });
|
|
6157
|
-
const invocation = RUNNER_INVOCATION_CARRYING_RUNNERS.has(runner) ? runnerInvocation(probe.testScript, runner, probe.scripts) ?? probe.declaredCommand ?? (testCommand === direct ? direct : null) : null;
|
|
6158
|
-
return {
|
|
6159
|
-
pm,
|
|
6160
|
-
runner,
|
|
6161
|
-
testCommand: effectiveTestCommand,
|
|
6162
|
-
...watchNormalized === null ? {} : { watchModeNormalized: true },
|
|
6163
|
-
...probe.aggregator === null ? {} : { workspaceAggregator: probe.aggregator },
|
|
6164
|
-
...probe.aggregatorTask == null ? {} : { aggregatorTask: probe.aggregatorTask },
|
|
6165
|
-
...runnerStage === null ? {} : { runnerStageCommand: watchNormalizedStage ?? runnerStage },
|
|
6166
|
-
/* Only alongside a runner stage, because these are exactly what running that stage alone would
|
|
6167
|
-
otherwise skip. A one-stage script gated by a lifecycle hook has a runner stage and no
|
|
6168
|
-
earlier stage of its own, and gets an empty list rather than a key. */
|
|
6169
|
-
...runnerStage === null || preRunnerStages.length === 0 ? {} : { preRunnerStageCommands: preRunnerStages },
|
|
6170
|
-
...invocation === null ? {} : { runnerInvocation: invocation },
|
|
6171
|
-
vitestDir: runner === "vitest" ? detectVitestDir(probe.dir, ctx) : void 0,
|
|
6172
|
-
jasmineSuite: runner === "jasmine" ? detectJasmineSuite(probe.dir, probe.testScript, ctx) : void 0,
|
|
6173
|
-
...probe.angular === null ? {} : { angularTarget: probe.angular },
|
|
6174
|
-
subdir: resolvedSubdir,
|
|
6175
|
-
workDir: commandDir,
|
|
6176
|
-
...workspaceRootSuite === null ? {} : { workspaceRootSuite },
|
|
6177
|
-
...probe.note ? { identifiedRunner: probe.note } : {},
|
|
6178
|
-
...compiledOutputFields({
|
|
6179
|
-
ctx,
|
|
6180
|
-
repoDir,
|
|
6181
|
-
packageDir: resolvedSubdir,
|
|
6182
|
-
runner,
|
|
6183
|
-
invocation,
|
|
6184
|
-
testCommand: effectiveTestCommand,
|
|
6185
|
-
scripts: probe.scripts,
|
|
6186
|
-
manager: pm,
|
|
6187
|
-
angular: probe.angular !== null
|
|
6188
|
-
}),
|
|
6189
|
-
...runner === "command" ? suiteFanoutPlan(probe, manager) : {}
|
|
6190
|
-
};
|
|
6410
|
+
}
|
|
6411
|
+
function answeredTargetDirectory(answers) {
|
|
6412
|
+
const value = answers["target.directory"];
|
|
6413
|
+
return value === void 0 || value.trim() === "" ? void 0 : value.trim();
|
|
6191
6414
|
}
|
|
6192
6415
|
|
|
6193
6416
|
// src/measurement-plan-resolver.ts
|
|
6194
6417
|
import { readFileSync as readFileSync6 } from "fs";
|
|
6195
6418
|
import { createHash } from "crypto";
|
|
6196
|
-
import { join as
|
|
6419
|
+
import { join as join10, relative as relative6 } from "path";
|
|
6197
6420
|
import {
|
|
6198
6421
|
MEASUREMENT_PLAN_SCHEMA,
|
|
6199
6422
|
EVIDENCE_OUTPUTS,
|
|
@@ -6203,7 +6426,7 @@ import {
|
|
|
6203
6426
|
resolvePreparedTestCommand
|
|
6204
6427
|
} from "@abloh/core";
|
|
6205
6428
|
function policyDeclaration(repoDir, ctx = diskRepoAccess(repoDir)) {
|
|
6206
|
-
const policyPath =
|
|
6429
|
+
const policyPath = join10(repoDir, "abloh.yml");
|
|
6207
6430
|
if (!repoFileExists(ctx, policyPath)) {
|
|
6208
6431
|
return { directory: null, testCommand: void 0, runner: void 0 };
|
|
6209
6432
|
}
|
|
@@ -6296,7 +6519,7 @@ function resolveMeasurementPlan(input) {
|
|
|
6296
6519
|
});
|
|
6297
6520
|
const declaration = declarationOf(repoDir, input.declaration, ctx);
|
|
6298
6521
|
const workDir = relative6(repoDir, detection.workDir) || ".";
|
|
6299
|
-
const policyPath =
|
|
6522
|
+
const policyPath = join10(repoDir, "abloh.yml");
|
|
6300
6523
|
const environmentPolicy = input.declaration !== void 0 && input.declaration.source === "supplied" ? input.declaration.environment ?? null : repoFileExists(ctx, policyPath) ? tryLoadPolicy(policyPath)?.environment ?? null : null;
|
|
6301
6524
|
const command = resolvePreparedTestCommand({
|
|
6302
6525
|
...input.cliTestCommand === void 0 ? {} : { cli: input.cliTestCommand },
|
|
@@ -6309,27 +6532,27 @@ function resolveMeasurementPlan(input) {
|
|
|
6309
6532
|
bindings.push({ path: "abloh.yml", digest: digestOf(ctx, policyPath), kind: "policy" });
|
|
6310
6533
|
}
|
|
6311
6534
|
for (const name of PLAN_MANIFEST_FILES) {
|
|
6312
|
-
const path =
|
|
6535
|
+
const path = join10(detection.workDir, name);
|
|
6313
6536
|
if (repoFileExists(ctx, path)) {
|
|
6314
6537
|
bindings.push({
|
|
6315
|
-
path:
|
|
6538
|
+
path: join10(workDir === "." ? "" : workDir, name),
|
|
6316
6539
|
digest: digestOf(ctx, path),
|
|
6317
6540
|
kind: "manifest"
|
|
6318
6541
|
});
|
|
6319
6542
|
}
|
|
6320
6543
|
}
|
|
6321
6544
|
for (const name of PLAN_LOCKFILES) {
|
|
6322
|
-
const path =
|
|
6545
|
+
const path = join10(repoDir, name);
|
|
6323
6546
|
if (repoFileExists(ctx, path)) {
|
|
6324
6547
|
bindings.push({ path: name, digest: digestOf(ctx, path), kind: "lockfile" });
|
|
6325
6548
|
}
|
|
6326
6549
|
}
|
|
6327
6550
|
const setupPath = environmentPolicy?.setup ?? null;
|
|
6328
6551
|
if (setupPath !== null) {
|
|
6329
|
-
bindings.push({ path: setupPath, digest: digestOf(ctx,
|
|
6552
|
+
bindings.push({ path: setupPath, digest: digestOf(ctx, join10(repoDir, setupPath)), kind: "setup" });
|
|
6330
6553
|
}
|
|
6331
6554
|
for (const name of environmentPolicy?.identityFiles ?? []) {
|
|
6332
|
-
bindings.push({ path: name, digest: digestOf(ctx,
|
|
6555
|
+
bindings.push({ path: name, digest: digestOf(ctx, join10(repoDir, name)), kind: "identity" });
|
|
6333
6556
|
}
|
|
6334
6557
|
bindings.sort(
|
|
6335
6558
|
(left, right) => left.path === right.path ? left.kind.localeCompare(right.kind) : left.path.localeCompare(right.path)
|
|
@@ -6368,7 +6591,7 @@ function resolveMeasurementPlan(input) {
|
|
|
6368
6591
|
systemPackages: environmentPolicy?.systemPackages ?? []
|
|
6369
6592
|
},
|
|
6370
6593
|
setup: {
|
|
6371
|
-
script: setupPath === null ? null : { path: setupPath, digest: digestOf(ctx,
|
|
6594
|
+
script: setupPath === null ? null : { path: setupPath, digest: digestOf(ctx, join10(repoDir, setupPath)) },
|
|
6372
6595
|
installCommand: environmentPolicy?.installCommand ?? null,
|
|
6373
6596
|
installDirectory: environmentPolicy?.installDirectory ?? ".",
|
|
6374
6597
|
setupCommands: environmentPolicy?.setupCommands ?? [],
|
|
@@ -6506,7 +6729,7 @@ function nodeDeclarationProblemReason(rejection) {
|
|
|
6506
6729
|
}
|
|
6507
6730
|
|
|
6508
6731
|
// src/setup-job.ts
|
|
6509
|
-
import { join as
|
|
6732
|
+
import { join as join11 } from "path";
|
|
6510
6733
|
import {
|
|
6511
6734
|
filterSetupCandidates,
|
|
6512
6735
|
jobOnMatrixLeg,
|
|
@@ -6562,7 +6785,7 @@ function declaredSetupJob(repoDir) {
|
|
|
6562
6785
|
return declaredSetupPin(repoDir).job;
|
|
6563
6786
|
}
|
|
6564
6787
|
function declaredSetupPin(repoDir, ctx = diskRepoAccess(repoDir)) {
|
|
6565
|
-
const text = accessText(ctx,
|
|
6788
|
+
const text = accessText(ctx, join11(repoDir, "abloh.yml"));
|
|
6566
6789
|
if (text === null) return {};
|
|
6567
6790
|
const reading = readSetupPinFromDocument(text);
|
|
6568
6791
|
if (reading.kind === "absent") return {};
|
|
@@ -7278,6 +7501,20 @@ function failFastInsertion(lines, job) {
|
|
|
7278
7501
|
}
|
|
7279
7502
|
return null;
|
|
7280
7503
|
}
|
|
7504
|
+
function workflowRunsAbloh(text) {
|
|
7505
|
+
let document;
|
|
7506
|
+
try {
|
|
7507
|
+
document = parse3(text);
|
|
7508
|
+
} catch {
|
|
7509
|
+
return false;
|
|
7510
|
+
}
|
|
7511
|
+
const jobs = document?.jobs;
|
|
7512
|
+
if (typeof jobs !== "object" || jobs === null) return false;
|
|
7513
|
+
return Object.values(jobs).some((job) => {
|
|
7514
|
+
const steps = job?.steps;
|
|
7515
|
+
return Array.isArray(steps) && alreadyEndsInAblohStep(steps);
|
|
7516
|
+
});
|
|
7517
|
+
}
|
|
7281
7518
|
function alreadyEndsInAblohStep(steps) {
|
|
7282
7519
|
const repository = SETUP_STEP_ACTION.split("@")[0] ?? SETUP_STEP_ACTION;
|
|
7283
7520
|
return steps.some(
|
|
@@ -7593,6 +7830,7 @@ export {
|
|
|
7593
7830
|
DENO_LOCKFILE,
|
|
7594
7831
|
ENVIRONMENT_BLOCK_HEADER,
|
|
7595
7832
|
InitRefusal,
|
|
7833
|
+
RETIRED_ENVIRONMENT_BLOCK_HEADERS,
|
|
7596
7834
|
RUSH_CONFIG_DIRECTORY,
|
|
7597
7835
|
RUSH_CONFIG_FILE,
|
|
7598
7836
|
RUSH_INSTALL_COMMAND,
|
|
@@ -7616,6 +7854,7 @@ export {
|
|
|
7616
7854
|
accessSubdirectories,
|
|
7617
7855
|
accessText,
|
|
7618
7856
|
admitSubmodules,
|
|
7857
|
+
aggregatorChain,
|
|
7619
7858
|
aggregatorInvocation,
|
|
7620
7859
|
aggregatorTaskPhrase,
|
|
7621
7860
|
aliasHopFamily,
|
|
@@ -7670,12 +7909,14 @@ export {
|
|
|
7670
7909
|
initStepRole,
|
|
7671
7910
|
invokesNodeTest,
|
|
7672
7911
|
isBrowserInstallCommand,
|
|
7912
|
+
isEnvironmentBlockHeader,
|
|
7673
7913
|
isInstallStep,
|
|
7674
7914
|
jasmineSuiteFromScript,
|
|
7675
7915
|
layerEnvironment,
|
|
7676
7916
|
materializeSubmodule,
|
|
7677
7917
|
nodeDeclarationProblemReason,
|
|
7678
7918
|
nodeSpecVersions,
|
|
7919
|
+
overlayRepoAccess,
|
|
7679
7920
|
placeSetupStep,
|
|
7680
7921
|
policyDeclaration,
|
|
7681
7922
|
preRunnerStageCommands,
|
|
@@ -7721,5 +7962,6 @@ export {
|
|
|
7721
7962
|
unreadableConditionLine,
|
|
7722
7963
|
vitestDirIn,
|
|
7723
7964
|
withoutPackageRunnerPrefix,
|
|
7724
|
-
workDirPrefix
|
|
7965
|
+
workDirPrefix,
|
|
7966
|
+
workflowRunsAbloh
|
|
7725
7967
|
};
|