@starci/hfs 5.0.3 → 5.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/cache/app-families.d.mts +8 -5
- package/cache/app-families.mjs +11 -9
- package/cache/hfs-family.d.mts +2 -0
- package/cache/hfs-family.mjs +4 -4
- package/cache/run.d.mts +2 -0
- package/cache/run.mjs +2 -5
- package/cache/test-family.d.mts +5 -3
- package/cache/test-family.mjs +7 -7
- package/cache/types-family.d.mts +5 -3
- package/cache/types-family.mjs +9 -8
- package/cache/wiring-family.d.mts +4 -2
- package/cache/wiring-family.mjs +5 -5
- package/emit/contracts.d.mts +12 -9
- package/emit/contracts.mjs +29 -11
- package/lint/chunk.mjs +11 -2
- package/lint/run.d.mts +2 -1
- package/lint/run.mjs +26 -29
- package/package.json +1 -1
- package/runtime/knowledge/grammars/index.yaml +12 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +6 -6
- package/runtime/scripts/api/node/run-node.d.mts +29 -0
- package/runtime/scripts/api/node/run-node.mjs +19 -0
- package/runtime/scripts/hfs/app-wiring.d.mts +8 -2
- package/runtime/scripts/hfs/app-wiring.mjs +27 -7
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.d.mts +2 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract.d.mts +11 -3
- package/runtime/scripts/hfs/rules/contract.mjs +50 -11
- package/src/main.d.mts +3 -1
- package/src/main.mjs +13 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.0.5
|
|
4
|
+
|
|
5
|
+
- Changed: Govern each app-check linter, compiler, test discovery and coverage, contract emitter and boot child through the CLI execution port with actual worker limits and complete output capture. Preserve cache identities, closure, flush behavior and coverage thresholds. Record source-proven native Page body layouts with their contextual constraints.
|
|
6
|
+
|
|
7
|
+
## 5.0.4
|
|
8
|
+
|
|
9
|
+
- Changed: Record the existing shared Page semantic role in the selected canonical Grammar component contract, so universal composition proof does not infer concrete API names or accept an app-controlled switch.
|
|
10
|
+
- Fixed: Run the package export proof's normal pack lifecycle in a private source copy, so prepack never rewrites emit modules that concurrent worker proofs import.
|
|
11
|
+
|
|
3
12
|
## 5.0.3
|
|
4
13
|
|
|
5
14
|
- Changed: Bundled canon pins name the builder-provenance BE canon release and its coherent dependency cohort. This package changes no runtime behavior or rule severity.
|
package/cache/app-families.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
|
|
1
2
|
import { type CheckFinding } from '../runtime/scripts/hfs/check.mjs';
|
|
2
3
|
import { type TestFamilyResult } from './test-family.mjs';
|
|
3
4
|
import { type TypesFamilyResult, type TypesProgram } from './types-family.mjs';
|
|
@@ -6,24 +7,26 @@ import type { CheckSession } from './types.mjs';
|
|
|
6
7
|
/** The tsc programs of the app: the back end and each front-end workspace, from the tracked tsconfigs. */
|
|
7
8
|
export declare function programsOf(tracked: readonly string[]): TypesProgram[];
|
|
8
9
|
/** `--types`: `tsc --noEmit` per workspace program through the app's TypeScript, reused by program key. */
|
|
9
|
-
export declare function runTypesForApp(session: CheckSession, { repoRoot, tracked, env }: {
|
|
10
|
+
export declare function runTypesForApp(session: CheckSession, { repoRoot, tracked, env, execute }: {
|
|
10
11
|
repoRoot: string;
|
|
11
12
|
tracked: readonly string[];
|
|
12
13
|
env: NodeJS.ProcessEnv;
|
|
13
|
-
|
|
14
|
+
execute?: HeavyNodeRun | undefined;
|
|
15
|
+
}): Promise<{
|
|
14
16
|
result: TypesFamilyResult;
|
|
15
17
|
findings: CheckFinding[];
|
|
16
|
-
}
|
|
18
|
+
}>;
|
|
17
19
|
/** `--test`: the unit specs of the back end through the app's own Jest, reused by closure key; the merged total is judged by `@starci/hfs` (the coverage proof). */
|
|
18
|
-
export declare function runTestForApp(session: CheckSession, { repoRoot, tracked }: {
|
|
20
|
+
export declare function runTestForApp(session: CheckSession, { repoRoot, tracked, execute }: {
|
|
19
21
|
repoRoot: string;
|
|
20
22
|
tracked: readonly string[];
|
|
23
|
+
execute?: HeavyNodeRun | undefined;
|
|
21
24
|
}): Promise<{
|
|
22
25
|
result: TestFamilyResult | null;
|
|
23
26
|
findings: CheckFinding[];
|
|
24
27
|
}>;
|
|
25
28
|
/** `--test --full --shard i/n`: one shard of the full unit run in the shape the shard merge reads; nothing is recorded. */
|
|
26
|
-
export declare function runTestShard(repoRoot: string, index: number, total: number): number
|
|
29
|
+
export declare function runTestShard(repoRoot: string, index: number, total: number, execute?: HeavyNodeRun): Promise<number>;
|
|
27
30
|
/** `--sonar`: the Sonar and Codecov baselines and the predictions from the merged coverage records; they never pass or fail a run. */
|
|
28
31
|
export declare function runSonarForApp(session: CheckSession, { repoRoot }: {
|
|
29
32
|
repoRoot: string;
|
package/cache/app-families.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
|
-
import {
|
|
8
|
+
import { completedNode, runHeavyNode } from '../runtime/scripts/api/node/run-node.mjs';
|
|
9
9
|
import { skillRoot } from '../runtime/engine/runtime-root.mjs';
|
|
10
10
|
import { coverageFrom } from '../runtime/scripts/hfs/coverage-from.mjs';
|
|
11
11
|
import { readWhy } from '../runtime/scripts/hfs/check.mjs';
|
|
@@ -30,11 +30,11 @@ export function programsOf(tracked) {
|
|
|
30
30
|
return programs.sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
31
31
|
}
|
|
32
32
|
/** `--types`: `tsc --noEmit` per workspace program through the app's TypeScript, reused by program key. */
|
|
33
|
-
export function runTypesForApp(session, { repoRoot, tracked, env }) {
|
|
33
|
+
export async function runTypesForApp(session, { repoRoot, tracked, env, execute }) {
|
|
34
34
|
const programs = programsOf(tracked);
|
|
35
35
|
const trackedSet = new Set(tracked);
|
|
36
|
-
const result = runTypesFamily({
|
|
37
|
-
session, programs, run: createTscRunner({ appRoot: repoRoot }), toolCacheDir: toolCacheDir(repoRoot, env),
|
|
36
|
+
const result = await runTypesFamily({
|
|
37
|
+
session, programs, run: createTscRunner({ appRoot: repoRoot, execute }), toolCacheDir: toolCacheDir(repoRoot, env),
|
|
38
38
|
inputsOf: (program) => {
|
|
39
39
|
const side = program.name.startsWith('be') ? 'be' : 'fe';
|
|
40
40
|
const prefix = `${program.reads?.[0] ?? side}/`;
|
|
@@ -45,12 +45,12 @@ export function runTypesForApp(session, { repoRoot, tracked, env }) {
|
|
|
45
45
|
return { result, findings: result.findings.map((found) => asFinding(found, 'TS_DIAGNOSTIC', 'error', 'tsc')) };
|
|
46
46
|
}
|
|
47
47
|
/** `--test`: the unit specs of the back end through the app's own Jest, reused by closure key; the merged total is judged by `@starci/hfs` (the coverage proof). */
|
|
48
|
-
export async function runTestForApp(session, { repoRoot, tracked }) {
|
|
48
|
+
export async function runTestForApp(session, { repoRoot, tracked, execute }) {
|
|
49
49
|
if (!tracked.includes('be/package.json') && !tracked.some((file) => file.startsWith('be/')))
|
|
50
50
|
return { result: null, findings: [] };
|
|
51
|
-
const jest = await createJestEnvironment({ appRoot: repoRoot, side: 'be' });
|
|
51
|
+
const jest = await createJestEnvironment({ appRoot: repoRoot, side: 'be', execute });
|
|
52
52
|
let judged = [];
|
|
53
|
-
const result = runTestFamily({
|
|
53
|
+
const result = await runTestFamily({
|
|
54
54
|
session, jest, side: 'be', merge: installedSpecMerge(repoRoot),
|
|
55
55
|
judge: (lcovPath) => {
|
|
56
56
|
const report = coverageFrom({ repoRoot, lcov: fs.readFileSync(lcovPath, 'utf8'), files: [...tracked] });
|
|
@@ -64,10 +64,12 @@ export async function runTestForApp(session, { repoRoot, tracked }) {
|
|
|
64
64
|
return { result, findings };
|
|
65
65
|
}
|
|
66
66
|
/** `--test --full --shard i/n`: one shard of the full unit run in the shape the shard merge reads; nothing is recorded. */
|
|
67
|
-
export function runTestShard(repoRoot, index, total) {
|
|
67
|
+
export async function runTestShard(repoRoot, index, total, execute) {
|
|
68
68
|
const shard = shardArgs(index, total);
|
|
69
69
|
const jestBin = createRequire(path.join(repoRoot, 'be', 'package.json')).resolve('jest/bin/jest');
|
|
70
|
-
const run =
|
|
70
|
+
const run = completedNode(await runHeavyNode(execute, { kind: 'test', label: `app check jest shard ${index}/${total}`, args: [jestBin, ...shard.args], cwd: path.join(repoRoot, 'be'), env: { ...process.env, ...shard.env }, maxBuffer: 256 * 1024 * 1024 }), 'jest shard');
|
|
71
|
+
process.stdout.write(run.stdout);
|
|
72
|
+
process.stderr.write(run.stderr);
|
|
71
73
|
return run.status ?? 1;
|
|
72
74
|
}
|
|
73
75
|
/** `--sonar`: the Sonar and Codecov baselines and the predictions from the merged coverage records; they never pass or fail a run. */
|
package/cache/hfs-family.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
|
|
1
2
|
import { type MachineFn, type RepositoryResult } from '../runtime/scripts/hfs/check.mjs';
|
|
2
3
|
import type { Presets } from '../sync/index.mjs';
|
|
3
4
|
import type { CheckSession, FamilyReport } from './types.mjs';
|
|
@@ -20,6 +21,7 @@ export interface PassResult {
|
|
|
20
21
|
}
|
|
21
22
|
export interface PassInput {
|
|
22
23
|
readonly repoRoot: string;
|
|
24
|
+
readonly execute?: HeavyNodeRun | undefined;
|
|
23
25
|
readonly harness?: {
|
|
24
26
|
version?: string;
|
|
25
27
|
} | undefined;
|
package/cache/hfs-family.mjs
CHANGED
|
@@ -7,9 +7,9 @@ import { checkRepo, checkRepository, trackedFiles } from '../runtime/scripts/hfs
|
|
|
7
7
|
import { checkArchitecture } from '../runtime/scripts/hfs/architecture/index.mjs';
|
|
8
8
|
import { withoutBootProof } from '../runtime/scripts/hfs/rules/boot-proof.mjs';
|
|
9
9
|
import { withUnusedSymbolMemo } from '../runtime/scripts/hfs/rules/unused-symbol.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import { contractEmitFindingsAsync } from '../runtime/scripts/hfs/rules/contract.mjs';
|
|
11
11
|
import { loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import { emitContractsAsync } from '../emit/contracts.mjs';
|
|
13
13
|
import { summarize } from '../runtime/scripts/hfs/check-results.mjs';
|
|
14
14
|
import { managedFindings } from '../sync/managed.mjs';
|
|
15
15
|
import { RecordSet, scopedStep } from './records.mjs';
|
|
@@ -61,9 +61,9 @@ export async function runStructurePass(session, input) {
|
|
|
61
61
|
const key = keys.get('be');
|
|
62
62
|
const emit = await scopedStep(session, records, {
|
|
63
63
|
path: 'be', step: 'contract-emit', key: key?.key ?? 'none', reuse, inputs: key?.inputs ?? [],
|
|
64
|
-
compute: () => {
|
|
64
|
+
compute: async () => {
|
|
65
65
|
const files = tracked.filter((file) => file.startsWith(bePrefix)).map((file) => file.slice(bePrefix.length));
|
|
66
|
-
const emitted =
|
|
66
|
+
const emitted = await contractEmitFindingsAsync({ repoRoot: path.join(repoRoot, 'be'), files, repo: be, emit: (request) => emitContractsAsync({ ...request, execute: input.execute }) });
|
|
67
67
|
return JSON.parse(JSON.stringify({ findings: emitted.findings, apps: emitted.apps }));
|
|
68
68
|
},
|
|
69
69
|
compact: (value) => (value.findings).map(compact),
|
package/cache/run.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
|
|
1
2
|
import { type CheckFinding, type RepositoryResult } from '../runtime/scripts/hfs/check.mjs';
|
|
2
3
|
import type { Presets } from '../sync/index.mjs';
|
|
3
4
|
import type { Prettier } from '../sync/format.mjs';
|
|
@@ -6,6 +7,7 @@ import { type CheckSession, type FamilyId, type FamilyReport } from './types.mjs
|
|
|
6
7
|
export declare const DEFAULT_FAMILIES: readonly FamilyId[];
|
|
7
8
|
export interface AppRunInput {
|
|
8
9
|
readonly repoRoot: string;
|
|
10
|
+
readonly execute?: HeavyNodeRun | undefined;
|
|
9
11
|
readonly tracked: readonly string[];
|
|
10
12
|
readonly session: CheckSession;
|
|
11
13
|
readonly selected: ReadonlySet<FamilyId>;
|
package/cache/run.mjs
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// run.mts - one `starci app check` / `starci app lint` pass over the selected families, against one session. It owns the order of the families,
|
|
2
|
-
// the judging of their findings (edition and the Vietnamese why of every code) and the merge into the one result the verb prints and the gate
|
|
3
|
-
// judges, so the two verbs never hold two answers for one tree.
|
|
4
1
|
import { skillRoot } from '../runtime/engine/runtime-root.mjs';
|
|
5
2
|
import { readWhy } from '../runtime/scripts/hfs/check.mjs';
|
|
6
3
|
import { judgedFindings, summarize, withWhy } from '../runtime/scripts/hfs/check-results.mjs';
|
|
@@ -23,7 +20,7 @@ export async function runAppFamilies(input) {
|
|
|
23
20
|
const track = (report) => void (report && reports.push(report));
|
|
24
21
|
const structure = selected.has('hfs') || selected.has('secrets');
|
|
25
22
|
const pass = structure
|
|
26
|
-
? await runStructurePass(session, { repoRoot, harness: input.harness, presets: input.presets, tracked: input.tracked, hfs: selected.has('hfs'), secrets: selected.has('secrets') })
|
|
23
|
+
? await runStructurePass(session, { repoRoot, harness: input.harness, presets: input.presets, tracked: input.tracked, hfs: selected.has('hfs'), secrets: selected.has('secrets'), execute: input.execute })
|
|
27
24
|
: null;
|
|
28
25
|
if (pass) {
|
|
29
26
|
track(pass.report);
|
|
@@ -43,7 +40,7 @@ export async function runAppFamilies(input) {
|
|
|
43
40
|
}
|
|
44
41
|
let wiring;
|
|
45
42
|
if (selected.has('wiring')) {
|
|
46
|
-
const proved = await runWiringFamily(session, { repoRoot, edition: input.edition, files: input.tracked });
|
|
43
|
+
const proved = await runWiringFamily(session, { repoRoot, edition: input.edition, files: input.tracked, execute: input.execute });
|
|
47
44
|
track(proved.report);
|
|
48
45
|
wiring = { status: proved.wiring.status, apps: proved.wiring.apps, findings: proved.wiring.findings };
|
|
49
46
|
extra.push(...proved.wiring.findings);
|
package/cache/test-family.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
|
|
1
2
|
import type { CacheRecord, CheckSession, FamilyReport, JsonValue } from './types.mjs';
|
|
2
3
|
/** The folder of the session that holds the records of this family. */
|
|
3
4
|
export declare const TEST_FOLDER = "test-coverage";
|
|
@@ -33,7 +34,7 @@ export interface JestEnvironment {
|
|
|
33
34
|
/** The digest of the unit project's configuration, free of host paths. */
|
|
34
35
|
readonly configHash: string;
|
|
35
36
|
/** Runs `specs` (all of them when `specs` is every spec) writing the per-spec coverage into `coverageDir`. */
|
|
36
|
-
run(specs: readonly string[], coverageDir: string): RunOutcome
|
|
37
|
+
run(specs: readonly string[], coverageDir: string): RunOutcome | Promise<RunOutcome>;
|
|
37
38
|
/** Reads what the reporter wrote into `coverageDir`. */
|
|
38
39
|
readCoverage(coverageDir: string): readonly SpecCoverageOutput[];
|
|
39
40
|
}
|
|
@@ -94,7 +95,7 @@ export declare function sourcesOf(record: CacheRecord): Record<string, SourceIte
|
|
|
94
95
|
* Runs the unit specs against the cache. Diff mode runs the specs with no current record, full mode all of them; a passing spec's coverage is stored under its key,
|
|
95
96
|
* a failing one's is not, and the folder is replaced with exactly the current specs' records. The merged total is written to `<side>/coverage/lcov.info` and judged.
|
|
96
97
|
*/
|
|
97
|
-
export declare function runTestFamily(options: TestFamilyOptions): TestFamilyResult
|
|
98
|
+
export declare function runTestFamily(options: TestFamilyOptions): Promise<TestFamilyResult>;
|
|
98
99
|
/** The part of a Jest project config the closure needs. */
|
|
99
100
|
interface ProjectConfigLike {
|
|
100
101
|
readonly rootDir: string;
|
|
@@ -111,11 +112,12 @@ export declare function projectConfigHash(config: ProjectConfigLike): string;
|
|
|
111
112
|
* The app's own Jest, resolved from `jestFrom` (a package.json or folder inside the app's install): its unit project's configuration, its haste map and resolver
|
|
112
113
|
* (so `moduleNameMapper`, `jest.mock` targets and setup files count as Jest sees them), and its binary to run specs. `side` is the Jest root, app-relative.
|
|
113
114
|
*/
|
|
114
|
-
export declare function createJestEnvironment({ appRoot, side, jestFrom, project }: {
|
|
115
|
+
export declare function createJestEnvironment({ appRoot, side, jestFrom, project, execute }: {
|
|
115
116
|
appRoot: string;
|
|
116
117
|
side: string;
|
|
117
118
|
jestFrom?: string;
|
|
118
119
|
project?: string;
|
|
120
|
+
execute?: HeavyNodeRun | undefined;
|
|
119
121
|
}): Promise<JestEnvironment>;
|
|
120
122
|
/** The installed preset's union merge (resolved like the shard merge of `app coverage`): the records in, the lcov and coverage-final.json out. */
|
|
121
123
|
export declare function installedSpecMerge(appRoot: string, from?: string): MergeSpecRecords;
|
package/cache/test-family.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import fs from 'node:fs';
|
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
|
-
import {
|
|
11
|
+
import { completedNode, runHeavyNode } from '../runtime/scripts/api/node/run-node.mjs';
|
|
12
12
|
import { combineHashes, hashFile } from './hash.mjs';
|
|
13
13
|
import { canonicalJson } from './jsonl.mjs';
|
|
14
14
|
/** The folder of the session that holds the records of this family. */
|
|
@@ -57,7 +57,7 @@ function recordOf(spec, key, sources, side) {
|
|
|
57
57
|
* Runs the unit specs against the cache. Diff mode runs the specs with no current record, full mode all of them; a passing spec's coverage is stored under its key,
|
|
58
58
|
* a failing one's is not, and the folder is replaced with exactly the current specs' records. The merged total is written to `<side>/coverage/lcov.info` and judged.
|
|
59
59
|
*/
|
|
60
|
-
export function runTestFamily(options) {
|
|
60
|
+
export async function runTestFamily(options) {
|
|
61
61
|
const { session, jest, side, merge, judge } = options;
|
|
62
62
|
const decision = session.decide('test');
|
|
63
63
|
const hashOf = (file) => session.index.hashOf(file) ?? hashFile(path.join(session.root, file));
|
|
@@ -79,7 +79,7 @@ export function runTestFamily(options) {
|
|
|
79
79
|
try {
|
|
80
80
|
fs.rmSync(scratch, { recursive: true, force: true });
|
|
81
81
|
fs.mkdirSync(scratch, { recursive: true });
|
|
82
|
-
output = jest.run(toRun, scratch).output;
|
|
82
|
+
output = (await jest.run(toRun, scratch)).output;
|
|
83
83
|
const reported = new Map(jest.readCoverage(scratch).map((file) => [file.spec, file]));
|
|
84
84
|
for (const spec of toRun) {
|
|
85
85
|
const file = reported.get(posix(path.relative(path.join(session.root, side), path.join(session.root, spec))));
|
|
@@ -144,7 +144,7 @@ export function projectConfigHash(config) {
|
|
|
144
144
|
* The app's own Jest, resolved from `jestFrom` (a package.json or folder inside the app's install): its unit project's configuration, its haste map and resolver
|
|
145
145
|
* (so `moduleNameMapper`, `jest.mock` targets and setup files count as Jest sees them), and its binary to run specs. `side` is the Jest root, app-relative.
|
|
146
146
|
*/
|
|
147
|
-
export async function createJestEnvironment({ appRoot, side, jestFrom, project = 'unit' }) {
|
|
147
|
+
export async function createJestEnvironment({ appRoot, side, jestFrom, project = 'unit', execute }) {
|
|
148
148
|
const root = path.resolve(appRoot, side);
|
|
149
149
|
const fromApp = createRequire(jestFrom ?? path.join(root, 'package.json'));
|
|
150
150
|
const jestPackage = fromApp.resolve('jest/package.json');
|
|
@@ -189,7 +189,7 @@ export async function createJestEnvironment({ appRoot, side, jestFrom, project =
|
|
|
189
189
|
const relative = (file) => posix(path.relative(appRoot, file));
|
|
190
190
|
const setup = (name) => [...(config[name] ?? [])];
|
|
191
191
|
const setupClosure = closureFrom([...setup('setupFiles'), ...setup('setupFilesAfterEnv')]).map(relative);
|
|
192
|
-
const listed =
|
|
192
|
+
const listed = completedNode(await runHeavyNode(execute, { kind: 'test', label: `app check jest ${project} discovery`, args: [jestBin, '--selectProjects', project, '--listTests'], cwd: root, maxBuffer: 64 * 1024 * 1024 }), 'jest discovery');
|
|
193
193
|
if (listed.status !== 0)
|
|
194
194
|
throw new Error(`jest --listTests failed under ${side}: ${(listed.stderr || listed.stdout).split('\n')[0]}`);
|
|
195
195
|
const specs = listed.stdout.split(/\r?\n/u).map((line) => line.trim()).filter((line) => path.isAbsolute(line)).map((line) => relative(path.resolve(line))).sort();
|
|
@@ -206,7 +206,7 @@ export async function createJestEnvironment({ appRoot, side, jestFrom, project =
|
|
|
206
206
|
closures.set(spec, closure);
|
|
207
207
|
return closure;
|
|
208
208
|
},
|
|
209
|
-
run(toRun, coverageDir) {
|
|
209
|
+
async run(toRun, coverageDir) {
|
|
210
210
|
const base = [jestBin, '--selectProjects', project, '--coverage', '--ci'];
|
|
211
211
|
const everything = toRun.length === specs.length;
|
|
212
212
|
const sideRelative = toRun.map((spec) => posix(path.relative(root, path.resolve(appRoot, spec))));
|
|
@@ -223,7 +223,7 @@ export async function createJestEnvironment({ appRoot, side, jestFrom, project =
|
|
|
223
223
|
let output = '';
|
|
224
224
|
for (const chunk of chunks) {
|
|
225
225
|
const args = chunk.length > 0 ? [...base, '--runTestsByPath', ...chunk] : base;
|
|
226
|
-
const result =
|
|
226
|
+
const result = completedNode(await runHeavyNode(execute, { kind: 'test', label: `app check jest ${project} coverage`, args, cwd: root, env: { ...process.env, STARCI_SPEC_COVERAGE_DIR: coverageDir }, maxBuffer: 256 * 1024 * 1024 }), 'jest coverage');
|
|
227
227
|
status = status || (result.status ?? 1);
|
|
228
228
|
output += `${result.stdout}${result.stderr}`;
|
|
229
229
|
}
|
package/cache/types-family.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
|
|
1
2
|
import type { CheckSession, FamilyReport, Finding } from './types.mjs';
|
|
2
3
|
/** The folder of the session that holds the records of this family. */
|
|
3
4
|
export declare const TYPES_FOLDER = "check/types";
|
|
@@ -29,7 +30,7 @@ export interface TypesFamilyOptions {
|
|
|
29
30
|
readonly session: CheckSession;
|
|
30
31
|
readonly programs: readonly TypesProgram[];
|
|
31
32
|
/** Runs one program's `tsc`; `buildInfo` is where its `.tsbuildinfo` goes (the untracked tool cache). */
|
|
32
|
-
readonly run: (program: TypesProgram, buildInfo: string) => TscOutcome
|
|
33
|
+
readonly run: (program: TypesProgram, buildInfo: string) => TscOutcome | Promise<TscOutcome>;
|
|
33
34
|
/** The untracked tool cache of slot `app.tool-cache` (`<STARCI_CACHE_HOME>/<repo>/app-check`). */
|
|
34
35
|
readonly toolCacheDir: string;
|
|
35
36
|
readonly inputsOf?: (program: TypesProgram) => ProgramInputs;
|
|
@@ -63,12 +64,13 @@ export declare function parseTscOutput(output: string, appRoot: string, fallback
|
|
|
63
64
|
* Runs the type check of every program against the cache. A program whose key is unchanged takes its recorded diagnostics; a changed one runs in full (all of them
|
|
64
65
|
* under `--full`). A run that produced no diagnostics and failed (tsc itself could not complete) stores nothing, and so does a program whose files changed while it ran.
|
|
65
66
|
*/
|
|
66
|
-
export declare function runTypesFamily(options: TypesFamilyOptions): TypesFamilyResult
|
|
67
|
+
export declare function runTypesFamily(options: TypesFamilyOptions): Promise<TypesFamilyResult>;
|
|
67
68
|
/**
|
|
68
69
|
* The real `tsc` of an app: the program's own TypeScript (resolved from its folder, so the version the lockfile pins), `--noEmit` with an incremental build info in the
|
|
69
70
|
* tool cache, run from the app root so paths in the output are app-relative.
|
|
70
71
|
*/
|
|
71
|
-
export declare function createTscRunner({ appRoot, typescriptFrom }: {
|
|
72
|
+
export declare function createTscRunner({ appRoot, typescriptFrom, execute }: {
|
|
72
73
|
appRoot: string;
|
|
73
74
|
typescriptFrom?: string;
|
|
75
|
+
execute?: HeavyNodeRun | undefined;
|
|
74
76
|
}): TypesFamilyOptions['run'];
|
package/cache/types-family.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
|
-
import {
|
|
9
|
+
import { completedNode, runHeavyNode } from '../runtime/scripts/api/node/run-node.mjs';
|
|
10
10
|
import { CACHE_DIR } from './types.mjs';
|
|
11
11
|
import { combineHashes, hashFile } from './hash.mjs';
|
|
12
12
|
import { sortFindings } from './jsonl.mjs';
|
|
@@ -84,7 +84,7 @@ const identity = (finding) => `${finding.path}\0${finding.rule}\0${finding.messa
|
|
|
84
84
|
* Runs the type check of every program against the cache. A program whose key is unchanged takes its recorded diagnostics; a changed one runs in full (all of them
|
|
85
85
|
* under `--full`). A run that produced no diagnostics and failed (tsc itself could not complete) stores nothing, and so does a program whose files changed while it ran.
|
|
86
86
|
*/
|
|
87
|
-
export function runTypesFamily(options) {
|
|
87
|
+
export async function runTypesFamily(options) {
|
|
88
88
|
const { session, programs, run, toolCacheDir } = options;
|
|
89
89
|
const decision = session.decide('types');
|
|
90
90
|
const previous = new Map(session.records(TYPES_FOLDER).filter((record) => record.step === TSC_STEP).map((record) => [record.path, record]));
|
|
@@ -108,7 +108,7 @@ export function runTypesFamily(options) {
|
|
|
108
108
|
reused.push(program.name);
|
|
109
109
|
}
|
|
110
110
|
else {
|
|
111
|
-
const outcome = run(program, tsBuildInfoPath(toolCacheDir, session.root, program));
|
|
111
|
+
const outcome = await run(program, tsBuildInfoPath(toolCacheDir, session.root, program));
|
|
112
112
|
ran.push(program.name);
|
|
113
113
|
current = parseTscOutput(outcome.output, session.root, program.tsconfig);
|
|
114
114
|
if (current.length === 0 && outcome.status !== 0)
|
|
@@ -141,14 +141,15 @@ export function runTypesFamily(options) {
|
|
|
141
141
|
* The real `tsc` of an app: the program's own TypeScript (resolved from its folder, so the version the lockfile pins), `--noEmit` with an incremental build info in the
|
|
142
142
|
* tool cache, run from the app root so paths in the output are app-relative.
|
|
143
143
|
*/
|
|
144
|
-
export function createTscRunner({ appRoot, typescriptFrom }) {
|
|
145
|
-
return (program, buildInfo) => {
|
|
144
|
+
export function createTscRunner({ appRoot, typescriptFrom, execute }) {
|
|
145
|
+
return async (program, buildInfo) => {
|
|
146
146
|
const folder = path.resolve(appRoot, path.posix.dirname(program.tsconfig));
|
|
147
147
|
const tsc = path.join(path.dirname(createRequire(typescriptFrom ?? path.join(folder, 'package.json')).resolve('typescript/package.json')), 'bin', 'tsc');
|
|
148
148
|
fs.mkdirSync(path.dirname(buildInfo), { recursive: true });
|
|
149
|
-
const result =
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
const result = completedNode(await runHeavyNode(execute, {
|
|
150
|
+
kind: 'tsc', label: `app check tsc ${program.name}`, args: [tsc, '-p', posix(path.relative(appRoot, path.resolve(appRoot, program.tsconfig))), '--noEmit', '--incremental', '--tsBuildInfoFile', buildInfo, '--pretty', 'false'],
|
|
151
|
+
cwd: appRoot, maxBuffer: 256 * 1024 * 1024,
|
|
152
|
+
}), 'tsc');
|
|
152
153
|
return { status: result.status ?? 1, output: `${result.stdout}${result.stderr}` };
|
|
153
154
|
};
|
|
154
155
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
|
|
1
2
|
import { type WiringResult } from '../runtime/scripts/hfs/app-wiring.mjs';
|
|
2
3
|
import type { CheckSession, FamilyReport } from './types.mjs';
|
|
3
4
|
export declare const WIRING_FOLDER = "check/wiring";
|
|
@@ -6,9 +7,10 @@ export interface WiringFamilyResult {
|
|
|
6
7
|
readonly report: FamilyReport;
|
|
7
8
|
}
|
|
8
9
|
/** The boot proof family. `explicit` is the whole-tree proof of `--wiring`; without it a missing install leaves the compile unjudged, as the content finder does. */
|
|
9
|
-
export declare function runWiringFamily(session: CheckSession, { repoRoot, edition, files, run }: {
|
|
10
|
+
export declare function runWiringFamily(session: CheckSession, { repoRoot, edition, files, run, execute }: {
|
|
10
11
|
repoRoot: string;
|
|
11
12
|
edition?: string | undefined;
|
|
12
13
|
files?: readonly string[] | undefined;
|
|
13
|
-
run?: ((repoRoot: string) => WiringResult) | undefined;
|
|
14
|
+
run?: ((repoRoot: string) => WiringResult | Promise<WiringResult>) | undefined;
|
|
15
|
+
execute?: HeavyNodeRun | undefined;
|
|
14
16
|
}): Promise<WiringFamilyResult>;
|
package/cache/wiring-family.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// back-end side key is unchanged. A timeout reached no verdict and is never stored.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { appWiringAsync, BOOT_PROOF_TIMEOUT } from '../runtime/scripts/hfs/app-wiring.mjs';
|
|
6
6
|
import { loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
7
7
|
import { trackedFiles } from '../runtime/scripts/hfs/check.mjs';
|
|
8
8
|
import { RecordSet, scopedStep } from './records.mjs';
|
|
@@ -11,16 +11,16 @@ import { sideKey } from './side-key.mjs';
|
|
|
11
11
|
export const WIRING_FOLDER = 'check/wiring';
|
|
12
12
|
const compact = (finding) => ({ path: finding.path, line: 0, column: 0, rule: finding.code, message: finding.message });
|
|
13
13
|
/** The boot proof family. `explicit` is the whole-tree proof of `--wiring`; without it a missing install leaves the compile unjudged, as the content finder does. */
|
|
14
|
-
export async function runWiringFamily(session, { repoRoot, edition, files, run }) {
|
|
14
|
+
export async function runWiringFamily(session, { repoRoot, edition, files, run, execute }) {
|
|
15
15
|
const decision = session.decide('wiring');
|
|
16
16
|
const records = new RecordSet(session.records(WIRING_FOLDER));
|
|
17
17
|
const key = sideKey(repoRoot, session.index, 'be');
|
|
18
|
-
const prove = () => {
|
|
18
|
+
const prove = async () => {
|
|
19
19
|
if (run)
|
|
20
20
|
return run(repoRoot);
|
|
21
21
|
const declared = readRepoDeclaration(loadSlotManifest(), repoRoot);
|
|
22
22
|
const repo = { ...declared, edition: edition ?? declared.edition };
|
|
23
|
-
const proof =
|
|
23
|
+
const proof = await appWiringAsync({ repoRoot, repo: repo, files: [...(files ?? trackedFiles(repoRoot))], execute });
|
|
24
24
|
const installed = fs.existsSync(path.join(repoRoot, 'node_modules')) || fs.existsSync(path.join(repoRoot, 'be', 'node_modules'));
|
|
25
25
|
const findings = proof.findings.filter((finding) => installed || finding.reason !== 'module or proof could not be compiled');
|
|
26
26
|
return { ...proof, findings, ok: findings.length === 0 };
|
|
@@ -28,7 +28,7 @@ export async function runWiringFamily(session, { repoRoot, edition, files, run }
|
|
|
28
28
|
const step = session.progress.step('wiring', 'boot proof', Math.max(1, key.inputs.length));
|
|
29
29
|
const { value, reused } = await scopedStep(session, records, {
|
|
30
30
|
path: 'be', step: 'boot-proof', key: key.key, reuse: decision.mode === 'diff', inputs: key.inputs,
|
|
31
|
-
compute: () => JSON.parse(JSON.stringify(prove())),
|
|
31
|
+
compute: async () => JSON.parse(JSON.stringify(await prove())),
|
|
32
32
|
compact: (found) => found.findings.map(compact),
|
|
33
33
|
store: (found) => !found.findings.some((finding) => finding.code === BOOT_PROOF_TIMEOUT),
|
|
34
34
|
});
|
package/emit/contracts.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
|
|
1
2
|
/** The parts of the parsed hfs.json this module reads. */
|
|
2
3
|
export interface HfsDeclaration {
|
|
3
4
|
readonly apps?: ReadonlyArray<{
|
|
@@ -18,18 +19,20 @@ export declare function aliasesOf(paths: Readonly<Record<string, readonly string
|
|
|
18
19
|
export declare function aliasTarget(aliases: readonly PathAlias[], specifier: string): string | null;
|
|
19
20
|
/** The text written to a snapshot file: the printed schema and one final newline. Pure. */
|
|
20
21
|
export declare const snapshotText: (printed: string) => string;
|
|
21
|
-
|
|
22
|
-
* Writes the snapshots of every api app: `contracts/<app>/schema.graphql` when it serves GraphQL and `contracts/<app>/openapi.json`
|
|
23
|
-
* when it has an operation table. Answers `{ written: [paths], skipped: [apps], standIns: { app: [lines] } }`, `skipped`
|
|
24
|
-
* being the apps with neither. `declaration` is the parsed hfs.json. `outDir` (absolute) redirects the snapshots to another
|
|
25
|
-
* root, keeping their relative paths.
|
|
26
|
-
*/
|
|
27
|
-
export declare function emitContracts({ repoRoot, declaration, outDir }: {
|
|
22
|
+
interface EmitInput {
|
|
28
23
|
repoRoot: string;
|
|
29
24
|
declaration: HfsDeclaration;
|
|
30
25
|
outDir?: string;
|
|
31
|
-
}
|
|
26
|
+
}
|
|
27
|
+
interface EmitResult {
|
|
32
28
|
written: string[];
|
|
33
29
|
skipped: string[];
|
|
34
30
|
standIns: Record<string, string[]>;
|
|
35
|
-
}
|
|
31
|
+
}
|
|
32
|
+
/** Existing synchronous emitter for source finders and their worker proofs. */
|
|
33
|
+
export declare function emitContracts(input: EmitInput): EmitResult;
|
|
34
|
+
/** App-check/emit execution: each actual compiler worker is admitted separately, with the original isolated environment. */
|
|
35
|
+
export declare function emitContractsAsync(input: EmitInput & {
|
|
36
|
+
execute?: HeavyNodeRun | undefined;
|
|
37
|
+
}): Promise<EmitResult>;
|
|
38
|
+
export {};
|
package/emit/contracts.mjs
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* * artifact, so a redirected `outDir` (the R23 check mode, which emits into a scratch root) does not get them.
|
|
22
22
|
*/
|
|
23
23
|
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { completedNode, runHeavyNode } from '../runtime/scripts/api/node/run-node.mjs';
|
|
24
25
|
import fs from 'node:fs';
|
|
25
26
|
import path from 'node:path';
|
|
26
27
|
import { fileURLToPath } from 'node:url';
|
|
@@ -50,9 +51,8 @@ export function aliasTarget(aliases, specifier) {
|
|
|
50
51
|
const apiApps = (declaration) => (declaration.apps ?? []).filter((app) => app.kind === 'api').map((app) => app.name);
|
|
51
52
|
/** The text written to a snapshot file: the printed schema and one final newline. Pure. */
|
|
52
53
|
export const snapshotText = (printed) => `${printed.replace(/\n+$/, '')}\n`;
|
|
53
|
-
/**
|
|
54
|
-
function
|
|
55
|
-
const result = spawnSync(process.execPath, [worker, repoRoot, app], { encoding: 'utf8', cwd: repoRoot, env: { PATH: readEnv('PATH') ?? '' }, maxBuffer: 256 * 1024 * 1024 });
|
|
54
|
+
/** Both transports judge the same native worker status and exact text. Exit 3 means the app serves no such contract. */
|
|
55
|
+
function workerOutput(result, app) {
|
|
56
56
|
if (result.status === 3)
|
|
57
57
|
return { text: null, lines: [] };
|
|
58
58
|
const lines = (result.stderr ?? '').split(/\r?\n/).filter(Boolean);
|
|
@@ -60,24 +60,20 @@ function runWorker(worker, repoRoot, app) {
|
|
|
60
60
|
throw new Error(`starci app emit: ${app} failed (exit ${result.status}): ${(lines.join('\n') || result.stdout).trim()}`);
|
|
61
61
|
return { text: result.stdout, lines: lines.filter((line) => line.startsWith(STAND_IN)) };
|
|
62
62
|
}
|
|
63
|
-
/** Emits the GraphQL schema and the operations of one app: `{ graphql, openapi, standIns }`, each text or null. */
|
|
64
|
-
function emitApp(repoRoot, app) {
|
|
65
|
-
const graphql = runWorker(WORKER, repoRoot, app);
|
|
66
|
-
const operations = runWorker(OPERATIONS_WORKER, repoRoot, app);
|
|
67
|
-
return { graphql: graphql.text, openapi: operations.text, standIns: [...graphql.lines, ...operations.lines] };
|
|
68
|
-
}
|
|
69
63
|
/**
|
|
70
64
|
* Writes the snapshots of every api app: `contracts/<app>/schema.graphql` when it serves GraphQL and `contracts/<app>/openapi.json`
|
|
71
65
|
* when it has an operation table. Answers `{ written: [paths], skipped: [apps], standIns: { app: [lines] } }`, `skipped`
|
|
72
66
|
* being the apps with neither. `declaration` is the parsed hfs.json. `outDir` (absolute) redirects the snapshots to another
|
|
73
67
|
* root, keeping their relative paths.
|
|
74
68
|
*/
|
|
75
|
-
|
|
69
|
+
function* emissionProgram({ repoRoot, declaration, outDir = repoRoot }) {
|
|
76
70
|
const written = [];
|
|
77
71
|
const skipped = [];
|
|
78
72
|
const standIns = {};
|
|
79
73
|
for (const app of apiApps(declaration)) {
|
|
80
|
-
const
|
|
74
|
+
const graphql = yield { worker: WORKER, repoRoot, app };
|
|
75
|
+
const operations = yield { worker: OPERATIONS_WORKER, repoRoot, app };
|
|
76
|
+
const emitted = { graphql: graphql.text, openapi: operations.text, standIns: [...graphql.lines, ...operations.lines] };
|
|
81
77
|
if (emitted.graphql === null && emitted.openapi === null) {
|
|
82
78
|
skipped.push(app);
|
|
83
79
|
continue;
|
|
@@ -106,3 +102,25 @@ export function emitContracts({ repoRoot, declaration, outDir = repoRoot }) {
|
|
|
106
102
|
}
|
|
107
103
|
return { written, skipped, standIns };
|
|
108
104
|
}
|
|
105
|
+
/** Existing synchronous emitter for source finders and their worker proofs. */
|
|
106
|
+
export function emitContracts(input) {
|
|
107
|
+
const program = emissionProgram(input);
|
|
108
|
+
let step = program.next();
|
|
109
|
+
while (!step.done) {
|
|
110
|
+
const { worker, repoRoot, app } = step.value;
|
|
111
|
+
const result = spawnSync(process.execPath, [worker, repoRoot, app], { encoding: 'utf8', cwd: repoRoot, env: { PATH: readEnv('PATH') ?? '' }, maxBuffer: 256 * 1024 * 1024 });
|
|
112
|
+
step = program.next(workerOutput(result, app));
|
|
113
|
+
}
|
|
114
|
+
return step.value;
|
|
115
|
+
}
|
|
116
|
+
/** App-check/emit execution: each actual compiler worker is admitted separately, with the original isolated environment. */
|
|
117
|
+
export async function emitContractsAsync(input) {
|
|
118
|
+
const program = emissionProgram(input);
|
|
119
|
+
let step = program.next();
|
|
120
|
+
while (!step.done) {
|
|
121
|
+
const { worker, repoRoot, app } = step.value;
|
|
122
|
+
const result = completedNode(await runHeavyNode(input.execute, { kind: 'tsc', label: `app emit ${app} ${path.basename(worker)}`, args: [worker, repoRoot, app], cwd: repoRoot, env: { PATH: readEnv('PATH') ?? '' }, maxBuffer: 256 * 1024 * 1024 }), 'contract compiler');
|
|
123
|
+
step = program.next(workerOutput(result, app));
|
|
124
|
+
}
|
|
125
|
+
return step.value;
|
|
126
|
+
}
|
package/lint/chunk.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// chunk.mjs - split a long argument list under the command-line limit (Windows allows 32,767 characters) and run the parts with bounded concurrency.
|
|
2
|
+
import { isHostHeld, isNodeExecutionRefusal } from '../runtime/scripts/api/node/run-node.mjs';
|
|
2
3
|
/** The command-line characters one argument costs: its length, a separating space and a pair of quotes. */
|
|
3
4
|
const cost = (arg) => String(arg).length + 3;
|
|
4
5
|
/**
|
|
@@ -26,18 +27,26 @@ export function chunkByChars(items, budget = 24_000) {
|
|
|
26
27
|
export async function mapBounded(chunks, task, limit = 3) {
|
|
27
28
|
const out = new Array(chunks.length);
|
|
28
29
|
let next = 0;
|
|
30
|
+
let held;
|
|
29
31
|
const worker = async () => {
|
|
30
|
-
while (next < chunks.length) {
|
|
32
|
+
while (next < chunks.length && held === undefined) {
|
|
31
33
|
const at = next++;
|
|
32
34
|
try {
|
|
33
35
|
out[at] = await task(chunks[at], at);
|
|
34
36
|
}
|
|
35
37
|
catch (error) {
|
|
36
|
-
|
|
38
|
+
if (isNodeExecutionRefusal(error)) {
|
|
39
|
+
if (held === undefined || isHostHeld(error))
|
|
40
|
+
held = error;
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
out[at] = { error: `could not start a lint child: ${String(error?.message ?? error)}` };
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
};
|
|
40
47
|
await Promise.all(Array.from({ length: Math.min(limit, chunks.length) }, worker));
|
|
48
|
+
if (held !== undefined)
|
|
49
|
+
throw held;
|
|
41
50
|
return out;
|
|
42
51
|
}
|
|
43
52
|
/** Merge the `{ results, error }` records of the chunks, in chunk order: every result kept, every distinct error reported. */
|
package/lint/run.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
|
|
1
2
|
import { type CanonDeclarations, type LintSideStats } from '../cache/lint-family.mjs';
|
|
2
3
|
import type { CheckSession } from '../cache/types.mjs';
|
|
3
4
|
type Dynamic = any;
|
|
@@ -62,7 +63,7 @@ export declare function runLinter({ cwd, pkg, bin, args, bound }: {
|
|
|
62
63
|
bin: string;
|
|
63
64
|
args: string[];
|
|
64
65
|
bound?: string | null;
|
|
65
|
-
}): Promise<LinterRun>;
|
|
66
|
+
}, execute?: HeavyNodeRun): Promise<LinterRun>;
|
|
66
67
|
/**
|
|
67
68
|
* The lint scope a flat config declares: `ignored(file)` for its global ignores (a block holding only `ignores` and `name`), which prune
|
|
68
69
|
* the walk, and `source(file)` for a file some block's `files` select while that block's own `ignores` do not (standard flat-config
|
package/lint/run.mjs
CHANGED
|
@@ -30,7 +30,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
30
30
|
import fs from 'node:fs';
|
|
31
31
|
import path from 'node:path';
|
|
32
32
|
import { createRequire } from 'node:module';
|
|
33
|
-
import {
|
|
33
|
+
import { completedNode, isHostHeld, isNodeExecutionRefusal, runHeavyNode } from '../runtime/scripts/api/node/run-node.mjs';
|
|
34
34
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
35
|
import { linterReport, mergeReports, sonarReport, sourceRootsOf } from '../report/sonar.mjs';
|
|
36
36
|
import { SIDES, appRelativeMessages, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
@@ -119,28 +119,8 @@ export function eslintHeapArgs(env = process.env) {
|
|
|
119
119
|
const mb = Number(env[ESLINT_HEAP_ENV]);
|
|
120
120
|
return Number.isInteger(mb) && mb > 0 ? [`--max-old-space-size=${mb}`] : [];
|
|
121
121
|
}
|
|
122
|
-
/** The child `node` ran with `args` from `cwd`: its exit status, signal, spawn error and the text of both streams, once it closes. */
|
|
123
|
-
function runNode(args, options) {
|
|
124
|
-
return new Promise((resolve) => {
|
|
125
|
-
let child;
|
|
126
|
-
try {
|
|
127
|
-
child = spawn(process.execPath, args, { ...options, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
resolve({ status: 2, signal: null, error: error, stdout: '', stderr: String(error?.message ?? error) });
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const out = [];
|
|
134
|
-
const err = [];
|
|
135
|
-
let failure;
|
|
136
|
-
child.stdout.on('data', (chunk) => out.push(chunk));
|
|
137
|
-
child.stderr.on('data', (chunk) => err.push(chunk));
|
|
138
|
-
child.on('error', (error) => { failure = error; });
|
|
139
|
-
child.on('close', (status, signal) => resolve({ status, signal, error: failure, stdout: Buffer.concat(out).toString('utf8'), stderr: Buffer.concat(err).toString('utf8') }));
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
122
|
/** A linter's json results through the app's own install, run from `cwd` (a side folder): `{ results }` or `{ error }`. The child runs while the caller goes on. */
|
|
143
|
-
export async function runLinter({ cwd, pkg, bin, args, bound = null }) {
|
|
123
|
+
export async function runLinter({ cwd, pkg, bin, args, bound = null }, execute) {
|
|
144
124
|
let entry;
|
|
145
125
|
try {
|
|
146
126
|
const manifest = createRequire(path.join(cwd, 'package.json')).resolve(`${pkg}/package.json`);
|
|
@@ -151,7 +131,7 @@ export async function runLinter({ cwd, pkg, bin, args, bound = null }) {
|
|
|
151
131
|
return { error: `${pkg} is not installed for ${cwd}` };
|
|
152
132
|
}
|
|
153
133
|
const heap = pkg === 'eslint' ? eslintHeapArgs() : [];
|
|
154
|
-
const run = await
|
|
134
|
+
const run = completedNode(await runHeavyNode(execute, { kind: 'eslint', label: `app lint ${pkg} ${path.basename(cwd)}`, args: [...heap, ...(bound ? linterBoundArgs() : []), entry, ...args], cwd, maxBuffer: 64 * 1024 * 1024, ...(bound ? { env: { ...process.env, [BOUND_ENV]: bound } } : {}) }), pkg);
|
|
155
135
|
// ESLint prints its report on stdout; stylelint uses stderr. JSON does not prove a completed child: only exits 0/1
|
|
156
136
|
// without a spawn error or signal are measurable, and exit 1 must carry findings.
|
|
157
137
|
let results;
|
|
@@ -348,7 +328,11 @@ function checkedEslintResults({ results, expected, cwd, scope, changed }) {
|
|
|
348
328
|
const ESLINT_ARGV_BUDGET = 24_000;
|
|
349
329
|
const ESLINT_CONCURRENCY = 3;
|
|
350
330
|
/** A launch that threw instead of returning a record is a tool error of the run (exit 2), not a crash of the CLI. */
|
|
351
|
-
const spawnFailure = (error) =>
|
|
331
|
+
const spawnFailure = (error) => {
|
|
332
|
+
if (isNodeExecutionRefusal(error))
|
|
333
|
+
throw error;
|
|
334
|
+
return { error: `could not start eslint: ${String(error?.message ?? error)}` };
|
|
335
|
+
};
|
|
352
336
|
/** Starts the ESLint child of every side that has work and returns one record per side: skipped, a scope error, or the running child. */
|
|
353
337
|
async function launchEslint({ app, repoRoot, opts, changed, existing, workspace, onFe, run = runLinter }) {
|
|
354
338
|
const launches = [];
|
|
@@ -386,7 +370,12 @@ export async function eslintRepository({ repoRoot, app = readRepoDeclaration(loa
|
|
|
386
370
|
if (!app.sides)
|
|
387
371
|
throw new Error('ESLint runs at the app root, the folder of hfs.json');
|
|
388
372
|
const launches = await launchEslint({ app, repoRoot, opts, changed, existing, workspace, onFe, run });
|
|
389
|
-
const
|
|
373
|
+
const settled = await Promise.allSettled(launches.map(launch => launch.run ?? null));
|
|
374
|
+
const rejected = settled.filter((run) => run.status === 'rejected');
|
|
375
|
+
const failed = rejected.find(run => isHostHeld(run.reason)) ?? rejected[0];
|
|
376
|
+
if (failed !== undefined)
|
|
377
|
+
throw failed.reason;
|
|
378
|
+
const runs = settled.map(run => run.status === 'fulfilled' ? run.value : null);
|
|
390
379
|
launches.forEach((launch, at) => {
|
|
391
380
|
const { side } = launch;
|
|
392
381
|
if (launch.skipped) {
|
|
@@ -528,7 +517,7 @@ export async function cachedLinters({ repoRoot, app, opts, workspace, onFe, run,
|
|
|
528
517
|
return { results: sub.raw, errors: sub.errors };
|
|
529
518
|
},
|
|
530
519
|
stylelint: styled ? async (files) => {
|
|
531
|
-
const linted = await runLinter({ cwd, pkg: 'stylelint', bin: 'stylelint', args: [...(files ?? [onFe ? `${onFe}/src/**/*.css` : STYLE_GLOB]), '--formatter', 'json'] });
|
|
520
|
+
const linted = await (run ?? runLinter)({ cwd, pkg: 'stylelint', bin: 'stylelint', args: [...(files ?? [onFe ? `${onFe}/src/**/*.css` : STYLE_GLOB]), '--formatter', 'json'] });
|
|
532
521
|
const appRelative = appRelativeMessages(STYLE_SIDE, cwd);
|
|
533
522
|
for (const result of linted.results ?? [])
|
|
534
523
|
for (const warning of result.warnings ?? [])
|
|
@@ -551,13 +540,13 @@ export async function cachedLinters({ repoRoot, app, opts, workspace, onFe, run,
|
|
|
551
540
|
return { eslint, stylelint: style, sides };
|
|
552
541
|
}
|
|
553
542
|
/** Starts stylelint over the fe stylesheets (the running child), or returns null when no stylesheet is in scope. */
|
|
554
|
-
function launchStylelint({ repoRoot, opts, changed, existing, onFe }) {
|
|
543
|
+
function launchStylelint({ repoRoot, opts, changed, existing, onFe, run }) {
|
|
555
544
|
if (opts.side === 'be')
|
|
556
545
|
return null;
|
|
557
546
|
const styles = onSide(existing, STYLE_SIDE).filter((file) => file.endsWith('.css'));
|
|
558
547
|
if (changed !== null && !styles.length)
|
|
559
548
|
return null;
|
|
560
|
-
return runLinter({ cwd: path.join(repoRoot, STYLE_SIDE), pkg: 'stylelint', bin: 'stylelint', args: [...(changed ? styles : [onFe ? `${onFe}/src/**/*.css` : STYLE_GLOB]), '--formatter', 'json', ...(opts.fix ? ['--fix'] : [])] });
|
|
549
|
+
return (run ?? runLinter)({ cwd: path.join(repoRoot, STYLE_SIDE), pkg: 'stylelint', bin: 'stylelint', args: [...(changed ? styles : [onFe ? `${onFe}/src/**/*.css` : STYLE_GLOB]), '--formatter', 'json', ...(opts.fix ? ['--fix'] : [])] });
|
|
561
550
|
}
|
|
562
551
|
/**
|
|
563
552
|
* Run the whole lint of the app at `repoRoot`. `hfsCheck(repoRoot)` returns the `starci app check` result (`{ findings, tracked }`); the CLI
|
|
@@ -613,8 +602,14 @@ export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles =
|
|
|
613
602
|
const cached = cache !== undefined && changed === null && !opts.fix ? cache : null;
|
|
614
603
|
const cachedRun = app && cached ? cachedLinters({ app, repoRoot, opts, workspace, onFe, run: linter, cache: cached }) : null;
|
|
615
604
|
const eslintRun = app ? (cachedRun ? cachedRun.then((answer) => answer.eslint) : eslintRepository({ app, repoRoot, opts, changed: changed ? [...changed] : null, existing, workspace, onFe, run: linter })) : null;
|
|
616
|
-
const styleLaunch = app && !cachedRun ? launchStylelint({ repoRoot, opts, changed, existing, onFe }) : null;
|
|
605
|
+
const styleLaunch = app && !cachedRun ? launchStylelint({ repoRoot, opts, changed, existing, onFe, run: linter }) : null;
|
|
617
606
|
const checkRun = app ? Promise.resolve().then(() => hfsCheck(repoRoot)).then((value) => ({ value, error: undefined }), (error) => ({ value: undefined, error })) : null;
|
|
607
|
+
// A transport refusal does not abandon a sibling child or let its rejection escape unobserved.
|
|
608
|
+
const completed = await Promise.allSettled([eslintRun, styleLaunch, checkRun]);
|
|
609
|
+
const refused = completed.filter((run) => run.status === 'rejected');
|
|
610
|
+
const failure = refused.find(run => isHostHeld(run.reason)) ?? refused[0];
|
|
611
|
+
if (failure !== undefined)
|
|
612
|
+
throw failure.reason;
|
|
618
613
|
// 1. ESLint, once per side, from the side folder with that side's config.
|
|
619
614
|
const eslint = eslintRun ? await eslintRun : { engine: { sides: {} }, errors: [], raw: [], findings: [] };
|
|
620
615
|
engines.eslint = eslint.engine;
|
|
@@ -649,6 +644,8 @@ export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles =
|
|
|
649
644
|
let checked = { findings: [] };
|
|
650
645
|
if (checkRun) {
|
|
651
646
|
const settled = await checkRun;
|
|
647
|
+
if (isNodeExecutionRefusal(settled.error))
|
|
648
|
+
throw settled.error;
|
|
652
649
|
if (settled.error)
|
|
653
650
|
errors.push(`starci app check could not run: ${String(settled.error?.message ?? settled.error)}`);
|
|
654
651
|
else
|
package/package.json
CHANGED
|
@@ -68,6 +68,18 @@ families:
|
|
|
68
68
|
package: '@starci/grammar'
|
|
69
69
|
entry: '@starci/grammar/common'
|
|
70
70
|
styleEntry: '@starci/grammar/common.css'
|
|
71
|
+
semanticRoles:
|
|
72
|
+
page:
|
|
73
|
+
export: Page
|
|
74
|
+
entries: ['@starci/grammar/common', '@starci/grammar/<family>']
|
|
75
|
+
segment: { attribute: header, member: tabs }
|
|
76
|
+
bodyContainers: [PaneGrid, Pane, Stack, PaneSection, Section]
|
|
77
|
+
# Native body transparency follows the renderer's exact context, status and intrinsic-element API.
|
|
78
|
+
bodyContainerConstraints:
|
|
79
|
+
Pane: { providesContext: pane }
|
|
80
|
+
Stack: { literalProps: { as: [div, header, footer] } }
|
|
81
|
+
PaneSection: { requiresContext: pane, absentProps: [error], literalProps: { as: [div, header, footer] } }
|
|
82
|
+
Section: { absentProps: [error], literalProps: { kind: [region] } }
|
|
71
83
|
peers: [react, '@heroui/react', '@xyflow/react', react-aria-components]
|
|
72
84
|
packageSnapshot: common/DNA.yaml
|
|
73
85
|
scope: |
|
|
@@ -31,35 +31,35 @@ pins:
|
|
|
31
31
|
source: packages/event-stream/package.json
|
|
32
32
|
why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
|
|
33
33
|
'@starci/grammar':
|
|
34
|
-
version: 0.21.
|
|
34
|
+
version: 0.21.9
|
|
35
35
|
group: starci
|
|
36
36
|
install: registry
|
|
37
37
|
side: fe
|
|
38
38
|
source: packages/grammar/package.json
|
|
39
39
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
40
40
|
'@starci/eslint-canon-be':
|
|
41
|
-
version: 3.11.
|
|
41
|
+
version: 3.11.16
|
|
42
42
|
group: starci
|
|
43
43
|
install: registry
|
|
44
44
|
side: be
|
|
45
45
|
source: packages/eslint-canon-be/package.json
|
|
46
46
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
47
47
|
'@starci/eslint-canon-fe':
|
|
48
|
-
version: 9.9.
|
|
48
|
+
version: 9.9.9
|
|
49
49
|
group: starci
|
|
50
50
|
install: registry
|
|
51
51
|
side: fe
|
|
52
52
|
source: packages/eslint-canon-fe/package.json
|
|
53
53
|
why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
|
|
54
54
|
'@starci/eslint-grammar-core':
|
|
55
|
-
version: 0.21.
|
|
55
|
+
version: 0.21.9
|
|
56
56
|
group: starci
|
|
57
57
|
install: registry
|
|
58
58
|
side: fe
|
|
59
59
|
source: packages/eslint-grammar-core/package.json
|
|
60
60
|
why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
|
|
61
61
|
'@starci/eslint-grammar-offset-pop':
|
|
62
|
-
version: 0.21.
|
|
62
|
+
version: 0.21.9
|
|
63
63
|
group: starci
|
|
64
64
|
install: registry
|
|
65
65
|
side: fe
|
|
@@ -106,7 +106,7 @@ pins:
|
|
|
106
106
|
source: packages/ai-router/package.json
|
|
107
107
|
why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
|
|
108
108
|
'@starci/hfs':
|
|
109
|
-
version: 5.0.
|
|
109
|
+
version: 5.0.5
|
|
110
110
|
group: starci
|
|
111
111
|
install: registry
|
|
112
112
|
side: both
|
|
@@ -1,3 +1,32 @@
|
|
|
1
1
|
import type { SpawnSyncOptions, SpawnSyncReturns } from 'node:child_process';
|
|
2
|
+
/** A concrete Node child whose admission belongs to the CLI's machine-wide governor. */
|
|
3
|
+
export interface HeavyNodeRequest {
|
|
4
|
+
readonly kind: 'tsc' | 'eslint' | 'test';
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly args: readonly string[];
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
/** When supplied, this is the entire child environment, not an overlay. */
|
|
9
|
+
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
10
|
+
readonly timeoutMs?: number | undefined;
|
|
11
|
+
readonly maxBuffer: number;
|
|
12
|
+
}
|
|
13
|
+
/** Normal red exits are measurements; an interrupted child is not one. */
|
|
14
|
+
export interface HeavyNodeResult {
|
|
15
|
+
readonly status: number | null;
|
|
16
|
+
readonly signal: NodeJS.Signals | null;
|
|
17
|
+
readonly stdout: string;
|
|
18
|
+
readonly stderr: string;
|
|
19
|
+
readonly error?: NodeJS.ErrnoException | undefined;
|
|
20
|
+
}
|
|
21
|
+
/** The CLI admits and captures each actual child, never the composite app command. */
|
|
22
|
+
export type HeavyNodeRun = (request: HeavyNodeRequest) => Promise<HeavyNodeResult>;
|
|
23
|
+
/** A real governor hold reached no verdict; transports preserve its CLI exit instead of treating it as linter output. */
|
|
24
|
+
export declare const isHostHeld: (error: unknown) => boolean;
|
|
25
|
+
/** Execution infrastructure reached no complete measurement; the contract finder cannot turn it into a cacheable source finding. */
|
|
26
|
+
export declare const isNodeExecutionRefusal: (error: unknown) => boolean;
|
|
27
|
+
/** Keeps the runtime's mutation fence and temporary-directory custody before crossing the CLI execution port. */
|
|
28
|
+
export declare function runHeavyNode(run: HeavyNodeRun | undefined, request: HeavyNodeRequest): Promise<HeavyNodeResult>;
|
|
29
|
+
/** A partial compiler/test report cannot be interpreted or stored as a completed measurement. */
|
|
30
|
+
export declare function completedNode(result: HeavyNodeResult, label: string): HeavyNodeResult;
|
|
2
31
|
/** Runs `node <args>`; options (cwd, env, timeout, input, maxBuffer, stdio) pass through. */
|
|
3
32
|
export declare const runNode: (args: readonly string[], options?: SpawnSyncOptions) => SpawnSyncReturns<string>;
|
|
@@ -1,3 +1,22 @@
|
|
|
1
1
|
import { nodeSpawn } from './lib.mjs';
|
|
2
|
+
import { assertMutationFence } from '../../lib/mutation-fence.mjs';
|
|
3
|
+
import { withTempEnv } from '../../../engine/temp-root.mjs';
|
|
4
|
+
/** A real governor hold reached no verdict; transports preserve its CLI exit instead of treating it as linter output. */
|
|
5
|
+
export const isHostHeld = (error) => error instanceof Error && 'code' in error && error.code === 'HOST_HELD' && 'exitCode' in error && error.exitCode === 75;
|
|
6
|
+
/** Execution infrastructure reached no complete measurement; the contract finder cannot turn it into a cacheable source finding. */
|
|
7
|
+
export const isNodeExecutionRefusal = (error) => error instanceof Error && 'code' in error && typeof error.code === 'string' && ['HOST_HELD', 'HEAVY_DEFERRED', 'CLI_TOOL_INCOMPLETE', 'CLI_TOOL_MISSING', 'CLI_TOOL_NOT_STARTED', 'HFS_NODE_GOVERNOR_MISSING', 'HFS_NODE_INCOMPLETE'].includes(error.code);
|
|
8
|
+
/** Keeps the runtime's mutation fence and temporary-directory custody before crossing the CLI execution port. */
|
|
9
|
+
export async function runHeavyNode(run, request) {
|
|
10
|
+
if (run === undefined)
|
|
11
|
+
throw Object.assign(new Error('HFS_NODE_GOVERNOR_MISSING: run this check through starci-harness'), { code: 'HFS_NODE_GOVERNOR_MISSING' });
|
|
12
|
+
assertMutationFence({ kind: 'node-effect', args: request.args });
|
|
13
|
+
return run({ ...request, env: withTempEnv({ env: request.env ?? process.env }).env });
|
|
14
|
+
}
|
|
15
|
+
/** A partial compiler/test report cannot be interpreted or stored as a completed measurement. */
|
|
16
|
+
export function completedNode(result, label) {
|
|
17
|
+
if (result.error !== undefined || result.signal !== null || result.status === null)
|
|
18
|
+
throw Object.assign(new Error(`${label} did not complete`), { code: 'HFS_NODE_INCOMPLETE' });
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
2
21
|
/** Runs `node <args>`; options (cwd, env, timeout, input, maxBuffer, stdio) pass through. */
|
|
3
22
|
export const runNode = (args, options = {}) => nodeSpawn(args, options);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HeavyNodeRun, type HeavyNodeResult } from '../api/node/run-node.mjs';
|
|
1
2
|
export declare const BOOT_PROOF_CODE = "BE_APP_BOOT_PROOF_MISSING";
|
|
2
3
|
export declare const BOOT_PROOF_TIMEOUT = "BOOT_PROOF_TIMEOUT";
|
|
3
4
|
export declare const BOOT_PROOF_TIMEOUT_ENV = "STARCI_BOOT_PROOF_TIMEOUT_MS";
|
|
@@ -6,7 +7,7 @@ export declare const wiringFixturePath: (app: string) => string;
|
|
|
6
7
|
export type WorkerResult = {
|
|
7
8
|
status?: number | null;
|
|
8
9
|
signal?: string | null;
|
|
9
|
-
error?:
|
|
10
|
+
error?: HeavyNodeResult['error'];
|
|
10
11
|
stdout?: string;
|
|
11
12
|
} | null | undefined;
|
|
12
13
|
/** The injectable spawn seam of the worker. */
|
|
@@ -62,7 +63,12 @@ export interface AppWiringInput {
|
|
|
62
63
|
timeout?: number;
|
|
63
64
|
run?: WorkerRun;
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
+
/** The synchronous finder and its existing worker doubles drive the same proof program as app check. */
|
|
67
|
+
export declare function appWiring(input: AppWiringInput): WiringResult;
|
|
68
|
+
/** The cache's asynchronous execution seam admits each boot attempt, with the same validation, timeout retry and result judgement. */
|
|
69
|
+
export declare function appWiringAsync(input: Omit<AppWiringInput, 'run'> & {
|
|
70
|
+
execute?: HeavyNodeRun | undefined;
|
|
71
|
+
}): Promise<WiringResult>;
|
|
66
72
|
/** Optional standalone entry; coordinator connects the same seam to app check --gate wiring. */
|
|
67
73
|
export interface AppWiringMainIo {
|
|
68
74
|
cwd?: string;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
|
-
import { runNode } from '../api/node/run-node.mjs';
|
|
11
|
+
import { runNode, runHeavyNode } from '../api/node/run-node.mjs';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { invocationDir } from '../lib/roots.mjs';
|
|
14
14
|
import { isMain } from '../lib/is-main.mjs';
|
|
@@ -174,7 +174,7 @@ function wiringApps(repo, files, prefix) {
|
|
|
174
174
|
});
|
|
175
175
|
return [...new Set([...declared, ...observed])];
|
|
176
176
|
}
|
|
177
|
-
function appWiringFinding({ repoRoot, beRoot, prefix, app, files, fixtures, timeout
|
|
177
|
+
function* appWiringFinding({ repoRoot, beRoot, prefix, app, files, fixtures, timeout }) {
|
|
178
178
|
const file = `${prefix}${modulePath(app)}`;
|
|
179
179
|
if (!/^[a-z][a-z0-9-]*$/.test(app) || !fs.existsSync(path.join(repoRoot, file)) || (files && !files.includes(file))) {
|
|
180
180
|
return refusal(file, 'app module is missing');
|
|
@@ -184,10 +184,9 @@ function appWiringFinding({ repoRoot, beRoot, prefix, app, files, fixtures, time
|
|
|
184
184
|
const args = [WORKER, '--worker', JSON.stringify({ repoRoot: beRoot, app, fixture: fixtureExists ? fixture : null, timeout })];
|
|
185
185
|
const budget = workerBudget(timeout);
|
|
186
186
|
const options = { cwd: beRoot, encoding: 'utf8', env: {}, windowsHide: true, timeout: budget, maxBuffer: 1024 * 1024 };
|
|
187
|
-
|
|
188
|
-
let result = spawnWorker();
|
|
187
|
+
let result = yield { args, options };
|
|
189
188
|
if (isTimeout(result))
|
|
190
|
-
result =
|
|
189
|
+
result = yield { args, options };
|
|
191
190
|
if (isTimeout(result))
|
|
192
191
|
return timedOut(file, budget);
|
|
193
192
|
let proof;
|
|
@@ -199,7 +198,7 @@ function appWiringFinding({ repoRoot, beRoot, prefix, app, files, fixtures, time
|
|
|
199
198
|
return refusal(file, SAFE_REASONS.has(proof?.reason) ? proof?.reason : WORKER_FAILURE);
|
|
200
199
|
return null;
|
|
201
200
|
}
|
|
202
|
-
|
|
201
|
+
function* wiringProgram({ repoRoot, repo = readJson(repoRoot, 'hfs.json'), files, fixtures = {}, timeout = DEFAULT_TIMEOUT }) {
|
|
203
202
|
if (repo?.edition !== 'lite' || repo?.profile === 'fe' || repo?.kind === 'fe')
|
|
204
203
|
return { ok: true, status: 'skipped', findings: [], apps: [] };
|
|
205
204
|
const appRoot = Boolean(repo.sides);
|
|
@@ -208,12 +207,33 @@ export function appWiring({ repoRoot, repo = readJson(repoRoot, 'hfs.json'), fil
|
|
|
208
207
|
const apps = wiringApps(repo, files, prefix);
|
|
209
208
|
const findings = [];
|
|
210
209
|
for (const app of apps) {
|
|
211
|
-
const finding = appWiringFinding({ repoRoot, beRoot, prefix, app, files, fixtures, timeout
|
|
210
|
+
const finding = yield* appWiringFinding({ repoRoot, beRoot, prefix, app, files, fixtures, timeout });
|
|
212
211
|
if (finding)
|
|
213
212
|
findings.push(finding);
|
|
214
213
|
}
|
|
215
214
|
return { ok: findings.length === 0, status: 'checked', apps, findings };
|
|
216
215
|
}
|
|
216
|
+
/** The synchronous finder and its existing worker doubles drive the same proof program as app check. */
|
|
217
|
+
export function appWiring(input) {
|
|
218
|
+
const program = wiringProgram(input);
|
|
219
|
+
let step = program.next();
|
|
220
|
+
while (!step.done) {
|
|
221
|
+
const { args, options } = step.value;
|
|
222
|
+
step = program.next(input.run ? input.run(process.execPath, args, options) : runNode(args, options));
|
|
223
|
+
}
|
|
224
|
+
return step.value;
|
|
225
|
+
}
|
|
226
|
+
/** The cache's asynchronous execution seam admits each boot attempt, with the same validation, timeout retry and result judgement. */
|
|
227
|
+
export async function appWiringAsync(input) {
|
|
228
|
+
const program = wiringProgram(input);
|
|
229
|
+
let step = program.next();
|
|
230
|
+
while (!step.done) {
|
|
231
|
+
const { args, options } = step.value;
|
|
232
|
+
const result = await runHeavyNode(input.execute, { kind: 'test', label: 'app check wiring boot proof', args, cwd: options.cwd, env: options.env, timeoutMs: options.timeout, maxBuffer: options.maxBuffer });
|
|
233
|
+
step = program.next(result);
|
|
234
|
+
}
|
|
235
|
+
return step.value;
|
|
236
|
+
}
|
|
217
237
|
export function appWiringMain(argv, { cwd = invocationDir(), stdout = (text) => process.stdout.write(text), stderr = (text) => process.stderr.write(text) } = {}) {
|
|
218
238
|
let directory = cwd, json = false;
|
|
219
239
|
for (let at = 0; at < argv.length; at += 1) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Imported JSX components receive repeated data contracts; executable prop values remain compared. */
|
|
2
2
|
import type { Node, SourceFile } from 'typescript';
|
|
3
3
|
import type { TypeScriptApi } from './config.mjs';
|
|
4
|
+
/** The one app status-owner binding of FE-ERROR-1, shared by clone and body-flow proofs. */
|
|
5
|
+
export declare function canonicalStatusOwnerBinding(module: string | null, name: string | null): boolean;
|
|
4
6
|
/** Canon data prop objects qualify through an imported component binding, never its chosen name or file path. */
|
|
5
7
|
export declare function canonJsxCloneNodes(ts: TypeScriptApi, sourceFile: SourceFile): Set<Node>;
|
|
@@ -152,12 +152,16 @@ function nestedDataObjects(ts, expression, bindingOf, excluded) {
|
|
|
152
152
|
nestedDataObjects(ts, element, bindingOf, excluded);
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
/** The one app status-owner binding of FE-ERROR-1, shared by clone and body-flow proofs. */
|
|
156
|
+
export function canonicalStatusOwnerBinding(module, name) {
|
|
157
|
+
return name === 'SlotView' && module !== null && /(?:^|\/)components\/composites\/SlotView(?:\/index)?(?:\.[cm]?tsx?)?$/.test(module);
|
|
158
|
+
}
|
|
155
159
|
/** Resolve the canonical status owner through its import binding, including aliases and namespaces. */
|
|
156
160
|
function statusOwner(ts, tag, bindingOf) {
|
|
157
161
|
const binding = ts.isIdentifier(tag) ? bindingOf(tag) : ts.isPropertyAccessExpression(tag) && ts.isIdentifier(tag.expression) ? bindingOf(tag.expression) : null;
|
|
158
162
|
const module = binding?.kind === 'component' || binding?.kind === 'namespace' ? binding.module : null;
|
|
159
163
|
const name = binding?.kind === 'component' ? binding.name : ts.isPropertyAccessExpression(tag) ? tag.name.text : null;
|
|
160
|
-
return
|
|
164
|
+
return canonicalStatusOwnerBinding(module, name);
|
|
161
165
|
}
|
|
162
166
|
/** The mandated status wiring breaks clone windows; its render body remains ordinary compared code. */
|
|
163
167
|
function statusRecipe(ts, opening, bindingOf, excluded) {
|
|
@@ -15,12 +15,20 @@ export declare function contractFindings({ repoRoot, files, repo, resolver }: {
|
|
|
15
15
|
* `openapi.json` and `status` one of `fresh`, `stale`, `not-committed`, `none` (nothing emitted, nothing committed; GraphQL only),
|
|
16
16
|
* `left-behind` (committed, nothing emitted) or `emit-failed`.
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
interface ContractEmitInput {
|
|
19
19
|
repoRoot: string;
|
|
20
20
|
files: string[];
|
|
21
21
|
repo: Repo;
|
|
22
22
|
emit: Parsed;
|
|
23
|
-
}
|
|
23
|
+
}
|
|
24
|
+
/** The synchronous source finder and asynchronous app cache share every emission comparison and failure rule. */
|
|
25
|
+
export declare function contractEmitFindings(input: ContractEmitInput): {
|
|
24
26
|
findings: Finding[];
|
|
25
|
-
apps:
|
|
27
|
+
apps: Parsed[];
|
|
26
28
|
};
|
|
29
|
+
/** Awaits actual governed compiler children before the existing drift proof can be recorded. */
|
|
30
|
+
export declare function contractEmitFindingsAsync(input: ContractEmitInput): Promise<{
|
|
31
|
+
findings: Finding[];
|
|
32
|
+
apps: Parsed[];
|
|
33
|
+
}>;
|
|
34
|
+
export {};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// resolvers the app root composes, and the OpenAPI 3.1 document of the typed operation table `apps/<app>/src/operations.ts`.
|
|
7
7
|
// That the committed snapshot equals what the app emits now is contractEmitFindings (full pass only, never --fast): it emits every
|
|
8
8
|
// api app into a temp directory through the injected `emit` (packages/hfs/emit) and compares hashes; an emit that cannot run is a finding.
|
|
9
|
+
import { isNodeExecutionRefusal } from '../../api/node/run-node.mjs';
|
|
9
10
|
import { createHash } from 'node:crypto';
|
|
10
11
|
import fs from 'node:fs';
|
|
11
12
|
import path from 'node:path';
|
|
@@ -145,16 +146,7 @@ function artifactFindings({ repoRoot, tracked, scratch, app, artifact, what, emi
|
|
|
145
146
|
findings.push(found(CONTRACT_SNAPSHOT_DRIFT, snapshot, `${snapshot} (${committed.slice(0, 12)}) differs from what ${app.name} emits now (${String(fresh).slice(0, 12)}); run \`npm run contract:emit\` and commit the result`, { app: app.name, drift: 'stale' }));
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
-
* R23, full pass: each committed `contracts/<app>/schema.graphql` and `contracts/<app>/openapi.json` equals what `emit` writes for
|
|
150
|
-
* the app now. `emit({ repoRoot, declaration, outDir })` is `emitContracts` of packages/hfs/emit and answers `{ written, skipped }`;
|
|
151
|
-
* it is injected because the runtime scripts do not import the hfs package. Every api app is emitted on its own into a temp
|
|
152
|
-
* directory, so one app that cannot emit does not hide the others; an uncommitted emitted snapshot is a finding here too. Answers
|
|
153
|
-
* `{ findings, apps }`, `apps` being what was judged: `[{ app, artifact, status }]`, `artifact` being `schema.graphql` or
|
|
154
|
-
* `openapi.json` and `status` one of `fresh`, `stale`, `not-committed`, `none` (nothing emitted, nothing committed; GraphQL only),
|
|
155
|
-
* `left-behind` (committed, nothing emitted) or `emit-failed`.
|
|
156
|
-
*/
|
|
157
|
-
export function contractEmitFindings({ repoRoot, files, repo, emit }) {
|
|
149
|
+
function* contractProgram({ repoRoot, files, repo }) {
|
|
158
150
|
const findings = [];
|
|
159
151
|
const apps = [];
|
|
160
152
|
if (repo.profile !== 'be')
|
|
@@ -166,9 +158,14 @@ export function contractEmitFindings({ repoRoot, files, repo, emit }) {
|
|
|
166
158
|
const artifacts = [{ artifact: SNAPSHOT_OF.graphql, what: 'serves no GraphQL any more' }, { artifact: SNAPSHOT_OF.json, what: 'declares no operations any more' }];
|
|
167
159
|
let emitted;
|
|
168
160
|
try {
|
|
169
|
-
|
|
161
|
+
const response = yield { repoRoot, declaration: { apps: [app] }, outDir: scratch };
|
|
162
|
+
if ('error' in response)
|
|
163
|
+
throw response.error;
|
|
164
|
+
emitted = response.value;
|
|
170
165
|
}
|
|
171
166
|
catch (error) {
|
|
167
|
+
if (isNodeExecutionRefusal(error))
|
|
168
|
+
throw error;
|
|
172
169
|
emitFailure(app, artifacts, error, apps, findings);
|
|
173
170
|
continue;
|
|
174
171
|
}
|
|
@@ -183,3 +180,45 @@ export function contractEmitFindings({ repoRoot, files, repo, emit }) {
|
|
|
183
180
|
}
|
|
184
181
|
return { findings, apps };
|
|
185
182
|
}
|
|
183
|
+
/** The synchronous source finder and asynchronous app cache share every emission comparison and failure rule. */
|
|
184
|
+
export function contractEmitFindings(input) {
|
|
185
|
+
const program = contractProgram(input);
|
|
186
|
+
let step = program.next();
|
|
187
|
+
try {
|
|
188
|
+
while (!step.done) {
|
|
189
|
+
let response;
|
|
190
|
+
try {
|
|
191
|
+
response = { value: input.emit(step.value) };
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
response = { error };
|
|
195
|
+
}
|
|
196
|
+
step = program.next(response);
|
|
197
|
+
}
|
|
198
|
+
return step.value;
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
program.return({ findings: [], apps: [] });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Awaits actual governed compiler children before the existing drift proof can be recorded. */
|
|
205
|
+
export async function contractEmitFindingsAsync(input) {
|
|
206
|
+
const program = contractProgram(input);
|
|
207
|
+
let step = program.next();
|
|
208
|
+
try {
|
|
209
|
+
while (!step.done) {
|
|
210
|
+
let response;
|
|
211
|
+
try {
|
|
212
|
+
response = { value: await input.emit(step.value) };
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
response = { error };
|
|
216
|
+
}
|
|
217
|
+
step = program.next(response);
|
|
218
|
+
}
|
|
219
|
+
return step.value;
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
program.return({ findings: [], apps: [] });
|
|
223
|
+
}
|
|
224
|
+
}
|
package/src/main.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
|
|
1
2
|
import { type LockResult } from '../scaffold/app.mjs';
|
|
2
3
|
import type { GhSpawn } from '../ci/github.mjs';
|
|
3
4
|
import type { Prompt } from '../runtime/scripts/hfs/secret-prompt.mjs';
|
|
@@ -39,6 +40,7 @@ interface AppIo {
|
|
|
39
40
|
stackIo?: Record<string, unknown> | undefined;
|
|
40
41
|
runLinter?: LinterRunner | undefined;
|
|
41
42
|
checkApp?: ((args: Dynamic) => Dynamic) | undefined;
|
|
43
|
+
runHeavyNode?: HeavyNodeRun | undefined;
|
|
42
44
|
/** --quiet: no progress on stderr (the diagnosis stays). */
|
|
43
45
|
quiet?: boolean | undefined;
|
|
44
46
|
stderrIsTty?: boolean | undefined;
|
|
@@ -57,5 +59,5 @@ export declare function appRootAbove(dir: string): string;
|
|
|
57
59
|
* the HFS check and `stackIo` Swarm tools; production defaults execute every check. `hostIdentity` ({ label, read }) is the
|
|
58
60
|
* harness CLI's own secret.env SOPS_AGE_KEY, which every verb that opens a sealed member (secret, stack, release, db) reads last for the age identity (decryptionIdentity); `prompt` replaces its masked prompt.
|
|
59
61
|
*/
|
|
60
|
-
export declare function main(argv: string[], { cwd: baseCwd, harness, stdout, stderr, presets, prettier, lock, run, stackMain, env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, checkApp, stderrIsTty, }?: MainOptions): Promise<number>;
|
|
62
|
+
export declare function main(argv: string[], { cwd: baseCwd, harness, stdout, stderr, presets, prettier, lock, run, stackMain, env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, runHeavyNode, checkApp, stderrIsTty, }?: MainOptions): Promise<number>;
|
|
61
63
|
export {};
|
package/src/main.mjs
CHANGED
|
@@ -83,6 +83,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
83
83
|
// Every finding names a why code and carries its Vietnamese text. check, lint and slot read the app, never write to it. Exit codes:
|
|
84
84
|
// 0 clean, 1 error findings, 2 a refusal or bad usage.
|
|
85
85
|
import fs from 'node:fs';
|
|
86
|
+
import { isHostHeld } from '../runtime/scripts/api/node/run-node.mjs';
|
|
86
87
|
import { createRequire } from 'node:module';
|
|
87
88
|
import path from 'node:path';
|
|
88
89
|
import { pathToFileURL } from 'node:url';
|
|
@@ -99,13 +100,13 @@ import { HFS_DECLARATION_FILE, HfsSlotsError, loadSlotManifest, readRepoDeclarat
|
|
|
99
100
|
import { main as syncMain } from '../sync/cli.mjs';
|
|
100
101
|
import { runWorkHygiene } from '../sync/hygiene.mjs';
|
|
101
102
|
import { SyncError, bundledPresets } from '../sync/index.mjs';
|
|
102
|
-
import {
|
|
103
|
+
import { emitContractsAsync } from '../emit/contracts.mjs';
|
|
103
104
|
import { ScaffoldError, newService, newSpec } from '../scaffold/service.mjs';
|
|
104
105
|
import { EditionRefusal } from '../scaffold/edition-gate.mjs';
|
|
105
106
|
import { addKind } from '../scaffold/add.mjs';
|
|
106
107
|
import { scaffoldApp } from '../scaffold/app.mjs';
|
|
107
108
|
import { newImages } from '../scaffold/image.mjs';
|
|
108
|
-
import { cachedLinters, eslintRepository, lintRepository, parseLintArgs, printLintText } from '../lint/run.mjs';
|
|
109
|
+
import { cachedLinters, eslintRepository, lintRepository, parseLintArgs, printLintText, runLinter as nativeLinter } from '../lint/run.mjs';
|
|
109
110
|
import { runSonarForApp, runTestForApp, runTestShard, runTypesForApp } from '../cache/app-families.mjs';
|
|
110
111
|
import { CACHE_ENV, createDiskSession } from '../cache/session.mjs';
|
|
111
112
|
import { DEFAULT_FAMILIES, runAppFamilies } from '../cache/run.mjs';
|
|
@@ -359,7 +360,7 @@ function openSession({ repoRoot, tracked, io, full, use, planned }) {
|
|
|
359
360
|
async function appPass({ repoRoot, harness, only, presets, prettier, io, selected, full, use = 'normal', session: given }) {
|
|
360
361
|
const tracked = trackedFiles(repoRoot);
|
|
361
362
|
const session = given ?? openSession({ repoRoot, tracked, io, full, use: only === undefined ? use : 'bypass', planned: tracked.length * Math.max(1, selected.size) });
|
|
362
|
-
const result = await runAppFamilies({ repoRoot, tracked, session, selected, harness, presets, prettier, only, edition: undefined });
|
|
363
|
+
const result = await runAppFamilies({ repoRoot, tracked, session, selected, harness, presets, prettier, only, edition: undefined, execute: io.runHeavyNode });
|
|
363
364
|
if (given !== undefined)
|
|
364
365
|
return result;
|
|
365
366
|
const flushed = session.flush();
|
|
@@ -529,9 +530,9 @@ async function checkRun({ io, selected, full, use, coverageFrom }) {
|
|
|
529
530
|
eslint = await eslintRepository({ repoRoot, changed: null, existing: [], run: runLinter });
|
|
530
531
|
}
|
|
531
532
|
if (session !== undefined && selected.has('types'))
|
|
532
|
-
extra.push(...runTypesForApp(session, { repoRoot, tracked, env: io.env }).findings);
|
|
533
|
+
extra.push(...(await runTypesForApp(session, { repoRoot, tracked, env: io.env, execute: io.runHeavyNode })).findings);
|
|
533
534
|
if (session !== undefined && selected.has('test')) {
|
|
534
|
-
const tests = await runTestForApp(session, { repoRoot, tracked });
|
|
535
|
+
const tests = await runTestForApp(session, { repoRoot, tracked, execute: io.runHeavyNode });
|
|
535
536
|
extra.push(...tests.findings);
|
|
536
537
|
}
|
|
537
538
|
if (session !== undefined && selected.has('sonar'))
|
|
@@ -589,7 +590,7 @@ async function checkMain(opts, io) {
|
|
|
589
590
|
const total = Number(match?.[2]);
|
|
590
591
|
if (match === null || !Number.isSafeInteger(index) || !Number.isSafeInteger(total) || index < 1 || total < index)
|
|
591
592
|
throw new Error('--shard needs <i>/<n> with 1 <= i <= n');
|
|
592
|
-
return runTestShard(repoRoot, index, total);
|
|
593
|
+
return runTestShard(repoRoot, index, total, io.runHeavyNode);
|
|
593
594
|
}
|
|
594
595
|
// A coverage report judged on its own (the merge job of a sharded run) runs no family; with selectors it comes on top of them.
|
|
595
596
|
const selected = explicit.size > 0 ? explicit : coverageFrom === undefined ? new Set(DEFAULT_FAMILIES) : new Set();
|
|
@@ -609,13 +610,13 @@ async function checkMain(opts, io) {
|
|
|
609
610
|
writeRunLog(repoRoot, { command: `check${[...new Set(listFlag(opts, 'gate') ?? [])].map((gate) => ` --gate ${gate}`).join('')}${opts['no-cache'] === true ? ' --no-cache' : ''}${coverageFrom === undefined ? '' : ` --coverage-from ${JSON.stringify(coverageFrom)}`}`, seconds, exit, families: result.families.map((report) => ({ ...report, seconds: 0 })) }, io.env);
|
|
610
611
|
return exit;
|
|
611
612
|
}
|
|
612
|
-
function emitMain(opts, { cwd: repoRoot, stdout: out }) {
|
|
613
|
+
async function emitMain(opts, { cwd: repoRoot, stdout: out, runHeavyNode }) {
|
|
613
614
|
if (opts.positional.length)
|
|
614
615
|
throw new Error('starci app emit takes no path');
|
|
615
616
|
const be = readRepoDeclaration(loadSlotManifest(), repoRoot).sides?.be;
|
|
616
617
|
if (!be)
|
|
617
618
|
throw new Error('starci app emit runs at the app root (the folder of hfs.json)');
|
|
618
|
-
const { written, skipped, standIns } =
|
|
619
|
+
const { written, skipped, standIns } = await emitContractsAsync({ repoRoot: path.join(repoRoot, 'be'), declaration: be, execute: runHeavyNode });
|
|
619
620
|
for (const file of written)
|
|
620
621
|
out(`wrote be/${file}\n`);
|
|
621
622
|
const skipSummary = skipped.length ? `, ${skipped.join(', ')} serve no GraphQL` : '';
|
|
@@ -737,7 +738,7 @@ function errorMessage(error) {
|
|
|
737
738
|
* the HFS check and `stackIo` Swarm tools; production defaults execute every check. `hostIdentity` ({ label, read }) is the
|
|
738
739
|
* harness CLI's own secret.env SOPS_AGE_KEY, which every verb that opens a sealed member (secret, stack, release, db) reads last for the age identity (decryptionIdentity); `prompt` replaces its masked prompt.
|
|
739
740
|
*/
|
|
740
|
-
export async function main(argv, { cwd: baseCwd = process.cwd(), harness, stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier, lock, run, stackMain, env = process.env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, checkApp, stderrIsTty = process.stderr.isTTY === true, } = {}) {
|
|
741
|
+
export async function main(argv, { cwd: baseCwd = process.cwd(), harness, stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier, lock, run, stackMain, env = process.env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, runHeavyNode, checkApp, stderrIsTty = process.stderr.isTTY === true, } = {}) {
|
|
741
742
|
try {
|
|
742
743
|
const global = globalOptions(argv, path.resolve(baseCwd));
|
|
743
744
|
const [verb, ...given] = global.argv;
|
|
@@ -752,10 +753,12 @@ export async function main(argv, { cwd: baseCwd = process.cwd(), harness, stdout
|
|
|
752
753
|
return 2;
|
|
753
754
|
}
|
|
754
755
|
const { edition, argv: rest } = editionOption(verb, given);
|
|
755
|
-
return await dispatchMain(verb, rest, { cwd: global.cwd, harness, edition, quiet: global.quiet, stderrIsTty, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, checkApp });
|
|
756
|
+
return await dispatchMain(verb, rest, { cwd: global.cwd, harness, edition, quiet: global.quiet, stderrIsTty, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runHeavyNode, runLinter: runLinter ?? ((spec) => nativeLinter(spec, runHeavyNode)), checkApp });
|
|
756
757
|
}
|
|
757
758
|
catch (error) {
|
|
758
759
|
stderr(errorMessage(error));
|
|
760
|
+
if (isHostHeld(error))
|
|
761
|
+
return 75;
|
|
759
762
|
return 2;
|
|
760
763
|
}
|
|
761
764
|
}
|