@hublo/sentinel 0.1.0-alpha.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -25
- package/dist/bin/sentinel.js +275 -36
- package/dist/{chunk-UG74KTIU.js → chunk-HKGCWPRT.js} +151 -79
- package/dist/index.d.ts +31 -17
- package/dist/index.js +1 -1
- package/package.json +6 -6
- package/dist/bin/sentinel.js.map +0 -1
- package/dist/chunk-UG74KTIU.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/bin/sentinel.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
availableTargets,
|
|
4
|
+
describeFramework,
|
|
4
5
|
dispatch,
|
|
5
6
|
palette,
|
|
6
7
|
readNxProjectName,
|
|
8
|
+
readOwnPackage,
|
|
7
9
|
readOwnVersion,
|
|
8
10
|
readProjectPackageJson,
|
|
9
11
|
registerAdapters,
|
|
10
12
|
resolve,
|
|
11
13
|
resolveBin
|
|
12
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-HKGCWPRT.js";
|
|
13
15
|
|
|
14
16
|
// bin/sentinel.ts
|
|
15
17
|
import { program } from "commander";
|
|
@@ -105,7 +107,7 @@ function resolveContext(cwd2, opts2) {
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
// src/core/domain.ts
|
|
108
|
-
var VERBS = ["run", "inspect", "
|
|
110
|
+
var VERBS = ["run", "inspect", "init", "migrate", "report", "status"];
|
|
109
111
|
var TARGETS = [
|
|
110
112
|
"lint",
|
|
111
113
|
"format",
|
|
@@ -124,19 +126,45 @@ async function analyse(params) {
|
|
|
124
126
|
let worstCode = 0;
|
|
125
127
|
let done = 0;
|
|
126
128
|
for (const module of params.modules) {
|
|
127
|
-
|
|
129
|
+
let flavour2;
|
|
130
|
+
if (params.flavour) {
|
|
131
|
+
flavour2 = params.flavour;
|
|
132
|
+
} else {
|
|
133
|
+
const detection = describeFramework(readProjectPackageJson(module.root));
|
|
134
|
+
flavour2 = detection.flavour;
|
|
135
|
+
if (detection.ambiguous) {
|
|
136
|
+
const warn = palette(process.stderr);
|
|
137
|
+
process.stderr.write(
|
|
138
|
+
warn.warn(
|
|
139
|
+
`sentinel: ${module.name}: flavour is ambiguous, detected "${flavour2}" (from ${detection.source}), also found ${detection.conflicts.join(", ")}. Pass --flavour to be explicit.`
|
|
140
|
+
) + "\n"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
128
144
|
const ctx = {
|
|
129
145
|
module: module.name,
|
|
130
146
|
cwd: module.root,
|
|
131
147
|
flavour: flavour2,
|
|
132
148
|
ci: params.ci,
|
|
133
|
-
fix: params.fix
|
|
149
|
+
fix: params.fix,
|
|
150
|
+
maxDiagnostics: params.maxDiagnostics
|
|
134
151
|
};
|
|
135
152
|
for (const target of params.targets) {
|
|
136
153
|
let adapter;
|
|
137
154
|
try {
|
|
138
155
|
adapter = resolve(target, flavour2, params.runner);
|
|
139
|
-
} catch {
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (params.targetsExplicit) {
|
|
158
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
159
|
+
results.push({
|
|
160
|
+
project: module.name,
|
|
161
|
+
target,
|
|
162
|
+
flavour: flavour2,
|
|
163
|
+
ok: false,
|
|
164
|
+
status: "unsupported",
|
|
165
|
+
data: { reason }
|
|
166
|
+
});
|
|
167
|
+
}
|
|
140
168
|
continue;
|
|
141
169
|
}
|
|
142
170
|
try {
|
|
@@ -150,7 +178,14 @@ async function analyse(params) {
|
|
|
150
178
|
);
|
|
151
179
|
}
|
|
152
180
|
const result = await adapter.run(ctx);
|
|
153
|
-
results.push({
|
|
181
|
+
results.push({
|
|
182
|
+
project: module.name,
|
|
183
|
+
target,
|
|
184
|
+
flavour: flavour2,
|
|
185
|
+
ok: result.ok,
|
|
186
|
+
status: result.ok ? "ok" : "failed",
|
|
187
|
+
data: {}
|
|
188
|
+
});
|
|
154
189
|
worstCode = Math.max(worstCode, result.code);
|
|
155
190
|
} else if (params.verb === "report") {
|
|
156
191
|
const result = await adapter.report(ctx);
|
|
@@ -159,21 +194,43 @@ async function analyse(params) {
|
|
|
159
194
|
target,
|
|
160
195
|
flavour: flavour2,
|
|
161
196
|
ok: result.ok,
|
|
197
|
+
status: result.ok ? "ok" : "failed",
|
|
162
198
|
data: result.metrics ?? {}
|
|
163
199
|
});
|
|
164
200
|
worstCode = Math.max(worstCode, result.code);
|
|
165
201
|
} else if (params.verb === "inspect") {
|
|
166
202
|
const config = await adapter.inspect(ctx);
|
|
167
|
-
results.push({
|
|
203
|
+
results.push({
|
|
204
|
+
project: module.name,
|
|
205
|
+
target,
|
|
206
|
+
flavour: flavour2,
|
|
207
|
+
ok: true,
|
|
208
|
+
status: "ok",
|
|
209
|
+
data: config
|
|
210
|
+
});
|
|
168
211
|
} else {
|
|
169
212
|
const status = await adapter.status(ctx);
|
|
170
213
|
const ok = !status.adopted || status.conformant;
|
|
171
|
-
results.push({
|
|
214
|
+
results.push({
|
|
215
|
+
project: module.name,
|
|
216
|
+
target,
|
|
217
|
+
flavour: flavour2,
|
|
218
|
+
ok,
|
|
219
|
+
status: ok ? "ok" : "failed",
|
|
220
|
+
data: status
|
|
221
|
+
});
|
|
172
222
|
if (!ok) worstCode = Math.max(worstCode, 1);
|
|
173
223
|
}
|
|
174
224
|
} catch (error) {
|
|
175
225
|
const message = error instanceof Error ? error.message : String(error);
|
|
176
|
-
results.push({
|
|
226
|
+
results.push({
|
|
227
|
+
project: module.name,
|
|
228
|
+
target,
|
|
229
|
+
flavour: flavour2,
|
|
230
|
+
ok: false,
|
|
231
|
+
status: "failed",
|
|
232
|
+
data: { error: message }
|
|
233
|
+
});
|
|
177
234
|
worstCode = Math.max(worstCode, 1);
|
|
178
235
|
}
|
|
179
236
|
}
|
|
@@ -182,16 +239,34 @@ async function analyse(params) {
|
|
|
182
239
|
return { results, worstCode };
|
|
183
240
|
}
|
|
184
241
|
function generateSummary(result) {
|
|
185
|
-
const { project, target, flavour: flavour2, ok, data } = result;
|
|
242
|
+
const { project, target, flavour: flavour2, ok, status, data } = result;
|
|
186
243
|
const details = data && typeof data === "object" ? data : { value: data };
|
|
187
|
-
return { project, target, flavour: flavour2, ok, ...details };
|
|
244
|
+
return { project, target, flavour: flavour2, ok, status, ...details };
|
|
188
245
|
}
|
|
189
|
-
function generateSummaries(results) {
|
|
190
|
-
|
|
246
|
+
function generateSummaries(results, requestedTargets) {
|
|
247
|
+
const available = new Set(availableTargets());
|
|
248
|
+
return {
|
|
249
|
+
schemaVersion: 2,
|
|
250
|
+
executed: requestedTargets.filter((t) => available.has(t)),
|
|
251
|
+
skipped: requestedTargets.filter((t) => !available.has(t)),
|
|
252
|
+
results: results.map(generateSummary)
|
|
253
|
+
};
|
|
191
254
|
}
|
|
192
255
|
|
|
193
256
|
// src/core/render.ts
|
|
194
|
-
var HEAD_KEYS = /* @__PURE__ */ new Set([
|
|
257
|
+
var HEAD_KEYS = /* @__PURE__ */ new Set([
|
|
258
|
+
"project",
|
|
259
|
+
"module",
|
|
260
|
+
"target",
|
|
261
|
+
"flavour",
|
|
262
|
+
"ok",
|
|
263
|
+
"status",
|
|
264
|
+
"reason",
|
|
265
|
+
// The structured diagnostics are for `--json` consumers; the human row stays the concise
|
|
266
|
+
// `errors=N` line rather than dumping every diagnostic inline.
|
|
267
|
+
"diagnostics",
|
|
268
|
+
"diagnosticsTruncated"
|
|
269
|
+
]);
|
|
195
270
|
function isDeferredRules(value) {
|
|
196
271
|
return Array.isArray(value) && value.every((entry) => typeof entry === "object" && entry !== null && "rule" in entry);
|
|
197
272
|
}
|
|
@@ -199,9 +274,16 @@ function renderValue(key, value, p) {
|
|
|
199
274
|
if ((key === "errors" || key === "implicitAny") && typeof value === "number") {
|
|
200
275
|
return value > 0 ? p.fail(String(value)) : p.ok(String(value));
|
|
201
276
|
}
|
|
277
|
+
if (key === "implicitAny" && value === "deferred") return p.warn("deferred");
|
|
202
278
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
203
279
|
}
|
|
280
|
+
function renderUnsupportedRow(item, p) {
|
|
281
|
+
const reason = typeof item.reason === "string" ? ` ${p.dim(`(${item.reason})`)}` : "";
|
|
282
|
+
const head = ` ${p.dim("\xB7")} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`;
|
|
283
|
+
return `${head} ${p.dim("\u2014")} ${p.warn("unsupported")}${reason}`;
|
|
284
|
+
}
|
|
204
285
|
function renderSummary(item, p) {
|
|
286
|
+
if (item.status === "unsupported") return renderUnsupportedRow(item, p);
|
|
205
287
|
const ok = item.ok === true;
|
|
206
288
|
const mark = ok ? p.ok("\u2713") : p.fail("\u2717");
|
|
207
289
|
const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`;
|
|
@@ -218,10 +300,11 @@ function renderSummary(item, p) {
|
|
|
218
300
|
return lines.join("\n");
|
|
219
301
|
}
|
|
220
302
|
function statusCoverage(items) {
|
|
221
|
-
const
|
|
303
|
+
const relevant = items.filter((i) => i.status !== "unsupported");
|
|
304
|
+
const adopted = relevant.filter((i) => i.adopted === true);
|
|
222
305
|
const conformant = adopted.filter((i) => i.conformant === true);
|
|
223
306
|
return {
|
|
224
|
-
total:
|
|
307
|
+
total: relevant.length,
|
|
225
308
|
adopted: adopted.length,
|
|
226
309
|
conformant: conformant.length,
|
|
227
310
|
drifted: adopted.length - conformant.length
|
|
@@ -231,6 +314,7 @@ function presetShort(preset) {
|
|
|
231
314
|
return typeof preset === "string" ? preset.replace("@hublo/sentinel/tsconfig/", "") : "?";
|
|
232
315
|
}
|
|
233
316
|
function renderStatusRow(item, p) {
|
|
317
|
+
if (item.status === "unsupported") return renderUnsupportedRow(item, p);
|
|
234
318
|
const adopted = item.adopted === true;
|
|
235
319
|
const conformant = item.conformant === true;
|
|
236
320
|
const mark = !adopted ? p.dim("\xB7") : conformant ? p.ok("\u2713") : p.fail("\u2717");
|
|
@@ -248,21 +332,116 @@ function renderStatusSummary(items, p) {
|
|
|
248
332
|
return ` ${p.strong("coverage:")} ${c.adopted}/${c.total} adopted ${p.dim("\xB7")} ${c.conformant}/${c.adopted} conformant${drift}`;
|
|
249
333
|
}
|
|
250
334
|
|
|
335
|
+
// src/core/workspace-prep.ts
|
|
336
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
337
|
+
import { dirname, join as join3 } from "path";
|
|
338
|
+
var OVERRIDE_KEY = "i18next>typescript";
|
|
339
|
+
var NATIVE_TS_ALIAS = "@typescript/native";
|
|
340
|
+
var WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
341
|
+
var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
|
|
342
|
+
var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
|
|
343
|
+
function findWorkspaceRoot(startDir) {
|
|
344
|
+
let dir = startDir;
|
|
345
|
+
for (; ; ) {
|
|
346
|
+
if (existsSync2(join3(dir, WORKSPACE_ROOT_MARKER))) return dir;
|
|
347
|
+
const parent = dirname(dir);
|
|
348
|
+
if (parent === dir) return void 0;
|
|
349
|
+
dir = parent;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function declaredNativeTs(pkg) {
|
|
353
|
+
const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
|
|
354
|
+
if (!spec) return void 0;
|
|
355
|
+
const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
|
|
356
|
+
return version || void 0;
|
|
357
|
+
}
|
|
358
|
+
function ensureI18nextSingleton(root, dryRun) {
|
|
359
|
+
const pkgPath = join3(root, "package.json");
|
|
360
|
+
if (!existsSync2(pkgPath)) return void 0;
|
|
361
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
|
|
362
|
+
const want = declaredNativeTs(pkg);
|
|
363
|
+
if (!want) return void 0;
|
|
364
|
+
const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
|
|
365
|
+
if (have === want) return void 0;
|
|
366
|
+
if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
|
|
367
|
+
pkg.pnpm ??= {};
|
|
368
|
+
pkg.pnpm.overrides ??= {};
|
|
369
|
+
pkg.pnpm.overrides[OVERRIDE_KEY] = want;
|
|
370
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
371
|
+
return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
|
|
372
|
+
}
|
|
373
|
+
function ensureReleaseAgeAllowList(root, dryRun) {
|
|
374
|
+
const yamlPath = join3(root, WORKSPACE_YAML);
|
|
375
|
+
if (!existsSync2(yamlPath)) return void 0;
|
|
376
|
+
const own = readOwnPackage().name;
|
|
377
|
+
const lines = readFileSync2(yamlPath, "utf8").split("\n");
|
|
378
|
+
const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
|
|
379
|
+
if (keyIdx === -1) return void 0;
|
|
380
|
+
let lastItemIdx = keyIdx;
|
|
381
|
+
let indent = " ";
|
|
382
|
+
for (let i = keyIdx + 1; i < lines.length; i++) {
|
|
383
|
+
const match = lines[i]?.match(LIST_ITEM);
|
|
384
|
+
if (!match) break;
|
|
385
|
+
indent = match[1] ?? indent;
|
|
386
|
+
lastItemIdx = i;
|
|
387
|
+
if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
|
|
388
|
+
}
|
|
389
|
+
if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
|
|
390
|
+
lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
|
|
391
|
+
writeFileSync(yamlPath, lines.join("\n"));
|
|
392
|
+
return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
|
|
393
|
+
}
|
|
394
|
+
function ensureWorkspacePrep(opts2) {
|
|
395
|
+
const dryRun = Boolean(opts2.dryRun);
|
|
396
|
+
return [
|
|
397
|
+
ensureI18nextSingleton(opts2.root, dryRun),
|
|
398
|
+
ensureReleaseAgeAllowList(opts2.root, dryRun)
|
|
399
|
+
].filter((message) => message !== void 0);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/shared/node-version.ts
|
|
403
|
+
var MIN_NODE = "20.12.0";
|
|
404
|
+
function parts(version) {
|
|
405
|
+
const [major = 0, minor = 0, patch = 0] = version.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
406
|
+
return [major, minor, patch];
|
|
407
|
+
}
|
|
408
|
+
function checkNodeVersion(current, min = MIN_NODE) {
|
|
409
|
+
const [cMajor, cMinor, cPatch] = parts(current);
|
|
410
|
+
const [mMajor, mMinor, mPatch] = parts(min);
|
|
411
|
+
const ok = cMajor > mMajor || cMajor === mMajor && cMinor > mMinor || cMajor === mMajor && cMinor === mMinor && cPatch >= mPatch;
|
|
412
|
+
if (ok) return { ok: true };
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
message: `sentinel requires Node >= ${min}, but you are on ${current}. Switch with fnm/nvm (e.g. \`fnm use ${mMajor}\`) and re-run.`
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
251
419
|
// bin/sentinel.ts
|
|
420
|
+
var nodeCheck = checkNodeVersion(process.versions.node);
|
|
421
|
+
if (!nodeCheck.ok) {
|
|
422
|
+
process.stderr.write(`${nodeCheck.message}
|
|
423
|
+
`);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
registerAdapters();
|
|
252
427
|
program.name("sentinel").description("One CLI that guards code health: presets, analysis, and arch checks.").version(readOwnVersion()).configureHelp({ sortOptions: false }).showSuggestionAfterError(true).showHelpAfterError('(run "sentinel --help" for usage)').addHelpText(
|
|
253
428
|
"before",
|
|
254
429
|
[
|
|
255
430
|
"A check composes: verb + type + location.",
|
|
256
|
-
" verb what to do: --run --inspect --report --status --
|
|
431
|
+
" verb what to do: --run --inspect --report --status --init (--migrate: planned)",
|
|
257
432
|
" type which check: --lint --typescript ... (omit = all types; or --all)",
|
|
258
433
|
" where run from a MODULE dir \u2192 that module; from the ROOT \u2192 --module <name>,",
|
|
259
|
-
" --ci (affected), or all modules. --
|
|
434
|
+
" --ci (affected), or all modules. --init targets one module only.",
|
|
260
435
|
""
|
|
261
436
|
].join("\n")
|
|
262
|
-
).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--
|
|
437
|
+
).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--init", "set up a module: write its config stubs + the workspace prep it needs").option("--migrate", "planned: change an already-initialized setup (not available yet)").option("--report", "metrics and health report").option("--status", "adoption + conformity across modules (coverage + drift)").option("--lint", "linting").option("--format", "formatting").option("--typescript", "type checking").option("--build", "build").option("--test", "tests").option("--static-analysis", "cycles, complexity, duplication, centrality").option("--runtime-analysis", "bundle, Lighthouse, web vitals").option("--arch", "architecture boundaries").option("--all", "every target").option(
|
|
263
438
|
"--module <name>",
|
|
264
439
|
"from the workspace root: scope to one module (omit = all; inside a module dir, drop this)"
|
|
265
|
-
).option("--flavour <name>", `override the detected stack preset (${FLAVOURS.join(", ")})`).option("--runner <tool>", "override the default runner (e.g. eslint, biome)").option("--ci", "CI mode: from the root, only the affected modules; non-zero exit on failure").option("--fix", "auto-fix where applicable").option("--dry-run", "preview the changes without writing (--
|
|
440
|
+
).option("--flavour <name>", `override the detected stack preset (${FLAVOURS.join(", ")})`).option("--runner <tool>", "override the default runner (e.g. eslint, biome)").option("--ci", "CI mode: from the root, only the affected modules; non-zero exit on failure").option("--fix", "auto-fix where applicable").option("--dry-run", "preview the changes without writing (--init)").option("--json", "machine-readable JSON output (report/inspect/--dry-run)").option(
|
|
441
|
+
"--max-diagnostics <n>",
|
|
442
|
+
"cap the diagnostics embedded per module in --report (0 = no cap)",
|
|
443
|
+
"100"
|
|
444
|
+
).addHelpText(
|
|
266
445
|
"after",
|
|
267
446
|
[
|
|
268
447
|
"",
|
|
@@ -271,9 +450,17 @@ program.name("sentinel").description("One CLI that guards code health: presets,
|
|
|
271
450
|
" sentinel --report --typescript --module bff-admin # from root \u2192 one module",
|
|
272
451
|
" sentinel --report # from root \u2192 all types, all modules",
|
|
273
452
|
" sentinel --report --ci # from root \u2192 affected only",
|
|
274
|
-
" sentinel --
|
|
453
|
+
" sentinel --init --typescript --flavour react # set up the current module + workspace"
|
|
275
454
|
].join("\n")
|
|
276
|
-
).
|
|
455
|
+
).addHelpText("after", () => {
|
|
456
|
+
const available = availableTargets();
|
|
457
|
+
const planned = TARGETS.filter((t) => !available.includes(t));
|
|
458
|
+
return [
|
|
459
|
+
"",
|
|
460
|
+
`Available now: ${available.length ? available.map((t) => `--${t}`).join(", ") : "(none yet)"}`,
|
|
461
|
+
planned.length ? `Planned (ship in later tickets): ${planned.map((t) => `--${t}`).join(", ")}` : ""
|
|
462
|
+
].filter(Boolean).join("\n");
|
|
463
|
+
}).parse();
|
|
277
464
|
var opts = program.opts();
|
|
278
465
|
function toCamel(flag) {
|
|
279
466
|
return flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -310,13 +497,27 @@ if (opts.all && namedTargets.length > 0) {
|
|
|
310
497
|
);
|
|
311
498
|
}
|
|
312
499
|
var targets = opts.all || namedTargets.length === 0 ? [...TARGETS] : namedTargets;
|
|
313
|
-
|
|
314
|
-
|
|
500
|
+
var targetsExplicit = !(opts.all || namedTargets.length === 0);
|
|
501
|
+
var maxDiagnostics = Number(opts.maxDiagnostics);
|
|
502
|
+
if (!Number.isInteger(maxDiagnostics) || maxDiagnostics < 0) {
|
|
503
|
+
program.error(
|
|
504
|
+
`--max-diagnostics must be a non-negative integer (0 = no cap); got ${JSON.stringify(opts.maxDiagnostics)}.`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
if (opts.dryRun && verb !== "init") {
|
|
508
|
+
program.error("--dry-run only applies to --init (the read verbs never write).");
|
|
315
509
|
}
|
|
316
510
|
async function runVerb() {
|
|
317
511
|
const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: Boolean(opts.ci) });
|
|
318
512
|
const out = palette(process.stdout);
|
|
319
513
|
const err = palette(process.stderr);
|
|
514
|
+
if (opts.json && verb === "run") {
|
|
515
|
+
process.stderr.write(
|
|
516
|
+
err.warn(
|
|
517
|
+
"note: --json is ignored for --run (it streams the tool output); use --report --json for a machine envelope."
|
|
518
|
+
) + "\n"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
320
521
|
const started = Date.now();
|
|
321
522
|
const { results, worstCode } = await analyse({
|
|
322
523
|
verb,
|
|
@@ -324,12 +525,14 @@ async function runVerb() {
|
|
|
324
525
|
modules,
|
|
325
526
|
runner: opts.runner,
|
|
326
527
|
flavour,
|
|
528
|
+
targetsExplicit,
|
|
529
|
+
maxDiagnostics,
|
|
327
530
|
ci: Boolean(opts.ci),
|
|
328
531
|
fix: Boolean(opts.fix),
|
|
329
532
|
onProgress: (done, total, name) => process.stderr.write(err.dim(` [${done}/${total}] ${name}
|
|
330
533
|
`))
|
|
331
534
|
});
|
|
332
|
-
const summary = generateSummaries(results);
|
|
535
|
+
const summary = generateSummaries(results, targets);
|
|
333
536
|
if (opts.json && verb !== "run") {
|
|
334
537
|
const payload = verb === "status" ? { ...summary, coverage: statusCoverage(summary.results) } : summary;
|
|
335
538
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
@@ -345,28 +548,44 @@ async function runVerb() {
|
|
|
345
548
|
err.dim(` ${modules.length} module(s) [${scope}] in ${Date.now() - started}ms
|
|
346
549
|
`)
|
|
347
550
|
);
|
|
551
|
+
if (targetsExplicit && summary.skipped.length > 0) {
|
|
552
|
+
const avail = availableTargets();
|
|
553
|
+
process.stderr.write(
|
|
554
|
+
err.fail(
|
|
555
|
+
`sentinel: target(s) not available yet: ${summary.skipped.map((t) => `--${t}`).join(", ")}. Available now: ${avail.length ? avail.map((t) => `--${t}`).join(", ") : "(none yet)"}.`
|
|
556
|
+
) + "\n"
|
|
557
|
+
);
|
|
558
|
+
return 1;
|
|
559
|
+
}
|
|
348
560
|
return verb === "run" || opts.ci ? worstCode : 0;
|
|
349
561
|
}
|
|
350
|
-
async function
|
|
562
|
+
async function runInit() {
|
|
351
563
|
const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: false });
|
|
352
564
|
const [module] = modules;
|
|
353
565
|
if (scope === "all" || scope === "affected" || !module) {
|
|
354
566
|
program.error(
|
|
355
|
-
"--
|
|
567
|
+
"--init writes files: target one module (run from its directory, or pass --module). Adopting every module at once is intentionally not allowed \u2014 adopt gradually."
|
|
356
568
|
);
|
|
357
569
|
}
|
|
570
|
+
const available = availableTargets();
|
|
571
|
+
if (targetsExplicit) {
|
|
572
|
+
const unwired = targets.filter((type) => !available.includes(type));
|
|
573
|
+
if (unwired.length > 0) {
|
|
574
|
+
program.error(
|
|
575
|
+
`target(s) not available yet: ${unwired.map((t) => `--${t}`).join(", ")}. Available now: ${available.map((t) => `--${t}`).join(", ") || "(none yet)"}.`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const toRun = targets.filter((type) => available.includes(type));
|
|
358
580
|
let worst = 0;
|
|
359
|
-
for (const type of
|
|
581
|
+
for (const type of toRun) {
|
|
360
582
|
try {
|
|
361
583
|
const code = await dispatch({
|
|
362
584
|
verb,
|
|
363
585
|
target: type,
|
|
364
586
|
runner: opts.runner,
|
|
365
|
-
module: module.name,
|
|
366
587
|
cwd: module.root,
|
|
367
588
|
flavour,
|
|
368
|
-
ci: Boolean(opts.ci),
|
|
369
|
-
fix: Boolean(opts.fix),
|
|
370
589
|
dryRun: Boolean(opts.dryRun),
|
|
371
590
|
json: Boolean(opts.json)
|
|
372
591
|
});
|
|
@@ -375,16 +594,36 @@ async function runUpdate() {
|
|
|
375
594
|
process.stderr.write(`
|
|
376
595
|
sentinel (${type}): ${asMessage(err)}
|
|
377
596
|
`);
|
|
378
|
-
if (targets.length === 1) return 1;
|
|
379
597
|
worst = Math.max(worst, 1);
|
|
380
598
|
}
|
|
381
599
|
}
|
|
600
|
+
if (worst === 0) {
|
|
601
|
+
const root = findWorkspaceRoot(module.root);
|
|
602
|
+
if (root) {
|
|
603
|
+
const changes = ensureWorkspacePrep({ root, dryRun: Boolean(opts.dryRun) });
|
|
604
|
+
const prefix = opts.dryRun ? " dry run: " : " ";
|
|
605
|
+
for (const change of changes) process.stderr.write(`${prefix}${change}
|
|
606
|
+
`);
|
|
607
|
+
if (changes.length > 0 && !opts.dryRun) {
|
|
608
|
+
process.stderr.write(
|
|
609
|
+
palette(process.stderr).dim(" run `pnpm install` to apply the workspace changes\n")
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
382
614
|
return worst;
|
|
383
615
|
}
|
|
384
|
-
|
|
385
|
-
(verb === "
|
|
616
|
+
async function main() {
|
|
617
|
+
if (verb === "migrate") {
|
|
618
|
+
program.error("--migrate is planned and not available yet; use --init to set a module up.");
|
|
619
|
+
}
|
|
620
|
+
const runSelectedVerb = verb === "init" ? runInit : runVerb;
|
|
621
|
+
const exitCode = await runSelectedVerb();
|
|
622
|
+
process.exit(exitCode);
|
|
623
|
+
}
|
|
624
|
+
main().catch((error) => {
|
|
386
625
|
process.stderr.write(`
|
|
387
|
-
sentinel: ${asMessage(
|
|
626
|
+
sentinel: ${asMessage(error)}
|
|
388
627
|
`);
|
|
389
628
|
process.exit(1);
|
|
390
629
|
});
|