@noctcore/harness 0.1.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 ADDED
@@ -0,0 +1,44 @@
1
+ # @noctcore/harness
2
+
3
+ A **portable Structure-Lock runner**. It reads a repo's `.nightcore/harness.json` and runs each
4
+ declared check, reddening the build on any violation — so a project's structure lock is enforced in
5
+ its **own CI**, with no Nightcore install, no server, and no account.
6
+
7
+ ```bash
8
+ npx @noctcore/harness check
9
+ ```
10
+
11
+ Run it from the repo root (or pass `--dir <path>`). If there is no `.nightcore/harness.json`, the
12
+ runner exits `0` — the lock is opt-in-by-presence.
13
+
14
+ ## What it does
15
+
16
+ For every enabled check that declares a `command`, the runner prints the command, runs it in the
17
+ target directory (bounded by the check's `timeoutMs`, default 5 minutes), and records the outcome.
18
+ It runs **all** checks (it does not stop at the first failure), so one CI run shows the whole
19
+ failure set. It exits `0` when every check passed and `1` when any failed (printing a fix
20
+ instruction that lists each failed check with its command and captured output). `--json` emits a
21
+ machine-readable result to stdout instead.
22
+
23
+ ```
24
+ harness [check] [--dir <path>] [--json] [--version] [--help]
25
+ ```
26
+
27
+ ## What it is NOT
28
+
29
+ - **Not a SaaS or telemetry.** The runner makes zero network calls at run time. Everything it reads
30
+ is committed in your repo. It has zero runtime dependencies.
31
+ - **Not an integrity attestation.** It enforces **whatever checks are present** in your
32
+ `.nightcore/harness.json` and your committed rule files. It does **not** verify that those files
33
+ match what Nightcore originally generated — the artifacts are yours to edit. A weakened rule is
34
+ enforced in its weakened form. The control against silent weakening is **PR review of the diff**
35
+ (a re-export or a hand-edit shows up in `git diff`), not a signature or hash check.
36
+
37
+ The `command` strings in `.nightcore/harness.json` are executed. In your own CI this is the same
38
+ trust level as any `package.json` script or workflow step — your own committed, PR-reviewed config.
39
+ The runner prints every command before running it so a reviewer reading CI logs sees exactly what
40
+ executed.
41
+
42
+ ## Requirements
43
+
44
+ Node ≥ 22.
package/dist/cli.cjs ADDED
@@ -0,0 +1,506 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/cli.ts
32
+ var cli_exports = {};
33
+ __export(cli_exports, {
34
+ nodeIO: () => nodeIO,
35
+ runCli: () => runCli
36
+ });
37
+ module.exports = __toCommonJS(cli_exports);
38
+ var import_node_child_process2 = require("child_process");
39
+ var import_node_fs2 = require("fs");
40
+ var import_node_path3 = __toESM(require("path"), 1);
41
+ var import_node_process = __toESM(require("process"), 1);
42
+ var import_node_url2 = require("url");
43
+
44
+ // src/lint-meta/ctx.ts
45
+ var import_node_child_process = require("child_process");
46
+ var import_node_fs = require("fs");
47
+ var import_node_path = __toESM(require("path"), 1);
48
+ function toPosixRel(rel) {
49
+ return rel.replace(/\\/g, "/");
50
+ }
51
+ function normalizeText(text) {
52
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
53
+ }
54
+ var EXEC_MAX_BUFFER = 16 * 1024 * 1024;
55
+ function createNodeCtx(root) {
56
+ return {
57
+ root,
58
+ read(rel) {
59
+ const abs = import_node_path.default.join(root, toPosixRel(rel));
60
+ if (!(0, import_node_fs.existsSync)(abs)) return null;
61
+ return normalizeText((0, import_node_fs.readFileSync)(abs, "utf8"));
62
+ },
63
+ exists(rel) {
64
+ return (0, import_node_fs.existsSync)(import_node_path.default.join(root, toPosixRel(rel)));
65
+ },
66
+ glob(pattern) {
67
+ return Array.from((0, import_node_fs.globSync)(pattern, { cwd: root })).map(toPosixRel);
68
+ },
69
+ exec(cmd) {
70
+ const res = (0, import_node_child_process.spawnSync)(cmd, {
71
+ cwd: root,
72
+ shell: true,
73
+ encoding: "utf8",
74
+ stdio: ["ignore", "pipe", "pipe"],
75
+ maxBuffer: EXEC_MAX_BUFFER
76
+ });
77
+ return {
78
+ code: typeof res.status === "number" ? res.status : 1,
79
+ stdout: res.stdout ?? "",
80
+ stderr: res.stderr ?? ""
81
+ };
82
+ }
83
+ };
84
+ }
85
+
86
+ // src/lint-meta/registry.ts
87
+ var import_node_url = require("url");
88
+ var DEFAULT_REGISTRY_RELATIVE_PATH = ".nightcore/lint-meta/registry.js";
89
+ var defaultImporter = (absPath) => import((0, import_node_url.pathToFileURL)(absPath).href);
90
+ function isMetaRule(v) {
91
+ if (typeof v !== "object" || v === null) return false;
92
+ const r = v;
93
+ return typeof r.id === "string" && typeof r.run === "function";
94
+ }
95
+ function extractRules(mod) {
96
+ if (typeof mod !== "object" || mod === null) return null;
97
+ const m = mod;
98
+ const def = m.default;
99
+ const candidates = [
100
+ m.META_RULES,
101
+ def?.META_RULES,
102
+ m.default
103
+ ];
104
+ for (const candidate of candidates) {
105
+ if (Array.isArray(candidate) && candidate.every(isMetaRule)) return candidate;
106
+ }
107
+ return null;
108
+ }
109
+ async function loadRegistry(absRegistryPath, importer = defaultImporter) {
110
+ let mod;
111
+ try {
112
+ mod = await importer(absRegistryPath);
113
+ } catch (err) {
114
+ return { rules: [], error: err instanceof Error ? err.message : String(err) };
115
+ }
116
+ const rules = extractRules(mod);
117
+ if (rules === null) {
118
+ return {
119
+ rules: [],
120
+ error: "the registry must export `META_RULES` (a named export, or the default) as an array of { id, run } rule objects"
121
+ };
122
+ }
123
+ return { rules };
124
+ }
125
+
126
+ // src/lint-meta/run.ts
127
+ function runMetaRules(rules, ctx, onRule) {
128
+ const outcomes = [];
129
+ for (const rule of rules) {
130
+ onRule?.(rule);
131
+ const ciCritical = rule.ciCritical === true;
132
+ try {
133
+ outcomes.push({ id: rule.id, ciCritical, violations: rule.run(ctx), threw: null });
134
+ } catch (err) {
135
+ outcomes.push({
136
+ id: rule.id,
137
+ ciCritical,
138
+ violations: [],
139
+ threw: err instanceof Error ? err.message : String(err)
140
+ });
141
+ }
142
+ }
143
+ return outcomes;
144
+ }
145
+ function reportMetaOutcomes(outcomes) {
146
+ let criticalCount = 0;
147
+ let totalViolations = 0;
148
+ const lines = [];
149
+ for (const outcome of outcomes) {
150
+ if (outcome.threw !== null) {
151
+ lines.push(`[ERROR] ${outcome.id}: rule threw \u2014 ${outcome.threw}`);
152
+ criticalCount += 1;
153
+ continue;
154
+ }
155
+ for (const v of outcome.violations) {
156
+ totalViolations += 1;
157
+ const tag = outcome.ciCritical ? "ERROR" : "info";
158
+ lines.push(`[${tag}] ${v.rule} (${v.file}): ${v.message}`);
159
+ if (outcome.ciCritical) criticalCount += 1;
160
+ }
161
+ }
162
+ return { criticalCount, totalViolations, lines };
163
+ }
164
+ function exitCodeFor(report) {
165
+ return report.criticalCount > 0 ? 1 : 0;
166
+ }
167
+
168
+ // src/manifest.ts
169
+ var import_node_path2 = __toESM(require("path"), 1);
170
+ var MANIFEST_RELATIVE_PATH = import_node_path2.default.join(".nightcore", "harness.json");
171
+ var DEFAULT_CHECK_TIMEOUT_MS = 3e5;
172
+ var SUPPORTED_SCHEMA_MAJOR = 1;
173
+ function manifestPath(dir) {
174
+ return import_node_path2.default.join(dir, MANIFEST_RELATIVE_PATH);
175
+ }
176
+ function resolveSchema(raw) {
177
+ if (raw === void 0 || raw === null) return { tooNew: false, found: SUPPORTED_SCHEMA_MAJOR };
178
+ const n = typeof raw === "number" ? raw : Number(raw);
179
+ if (!Number.isFinite(n)) return { tooNew: true, found: Number.NaN };
180
+ const major = Math.floor(n);
181
+ return { tooNew: major > SUPPORTED_SCHEMA_MAJOR, found: major };
182
+ }
183
+ function planCheck(entry) {
184
+ if (typeof entry !== "object" || entry === null) return null;
185
+ const cfg = entry;
186
+ if (typeof cfg.name !== "string" || cfg.name.trim() === "") return null;
187
+ if (cfg.enabled === false) return null;
188
+ const kind = typeof cfg.kind === "string" ? cfg.kind : "";
189
+ if (kind === "shell") return null;
190
+ const command = typeof cfg.command === "string" ? cfg.command.trim() : "";
191
+ if (command === "") return null;
192
+ const tokens = command.split(/\s+/).filter((t) => t.length > 0);
193
+ const program = tokens[0];
194
+ if (program === void 0) return null;
195
+ const args = tokens.slice(1);
196
+ const declared = cfg.timeoutMs;
197
+ const timeoutMs = typeof declared === "number" && Number.isFinite(declared) && declared > 0 ? Math.floor(declared) : DEFAULT_CHECK_TIMEOUT_MS;
198
+ return { name: cfg.name, kind, command, program, args, timeoutMs };
199
+ }
200
+ function loadChecks(dir, read) {
201
+ const raw = read(manifestPath(dir));
202
+ if (raw === null) return { kind: "no-config" };
203
+ let value;
204
+ try {
205
+ value = JSON.parse(raw);
206
+ } catch {
207
+ return { kind: "no-config" };
208
+ }
209
+ if (typeof value !== "object" || value === null) return { kind: "no-config" };
210
+ const root = value;
211
+ const schema = resolveSchema(root.schemaVersion);
212
+ if (schema.tooNew) return { kind: "schema-too-new", found: schema.found };
213
+ const entries = root.checks;
214
+ if (!Array.isArray(entries)) return { kind: "no-config" };
215
+ const checks = [];
216
+ for (const entry of entries) {
217
+ const planned = planCheck(entry);
218
+ if (planned !== null) checks.push(planned);
219
+ }
220
+ return { kind: "ready", checks };
221
+ }
222
+
223
+ // src/run.ts
224
+ var TAIL_LIMIT = 4e3;
225
+ function tailOutput(stdout, stderr) {
226
+ let combined = stdout;
227
+ if (stderr.length > 0) combined += `
228
+ ${stderr}`;
229
+ if (combined.length > TAIL_LIMIT) {
230
+ return `\u2026${combined.slice(combined.length - TAIL_LIMIT)}`;
231
+ }
232
+ return combined;
233
+ }
234
+ function timeoutMessage(timeoutMs) {
235
+ return `timed out after ${timeoutMs}ms (the check was killed; it may hang or need a higher timeoutMs)`;
236
+ }
237
+ function interpret(res, timeoutMs) {
238
+ const timedOut = res.error?.code === "ETIMEDOUT" || res.signal != null;
239
+ if (timedOut) {
240
+ return { status: "failed", output: timeoutMessage(timeoutMs) };
241
+ }
242
+ if (res.error) {
243
+ const detail = res.error.message ?? res.error.code ?? "unknown error";
244
+ return { status: "failed", output: `failed to launch: ${detail}` };
245
+ }
246
+ if (res.status === 0) {
247
+ return { status: "passed", exitCode: 0 };
248
+ }
249
+ return {
250
+ status: "failed",
251
+ exitCode: res.status ?? void 0,
252
+ output: tailOutput(res.stdout, res.stderr)
253
+ };
254
+ }
255
+ function emptyPass() {
256
+ return { passed: true, checks: [] };
257
+ }
258
+ function runChecks(planned, runDir, spawn, onCommand) {
259
+ if (planned.length === 0) return emptyPass();
260
+ const checks = [];
261
+ let failedCheck;
262
+ for (const check of planned) {
263
+ onCommand?.(check);
264
+ const start = Date.now();
265
+ const res = spawn(check.program, check.args, {
266
+ cwd: runDir,
267
+ timeoutMs: check.timeoutMs
268
+ });
269
+ const durationMs = Date.now() - start;
270
+ const outcome = interpret(res, check.timeoutMs);
271
+ if (outcome.status === "failed" && failedCheck === void 0) {
272
+ failedCheck = check.name;
273
+ }
274
+ checks.push({
275
+ name: check.name,
276
+ kind: check.kind,
277
+ command: check.command,
278
+ status: outcome.status,
279
+ exitCode: outcome.exitCode,
280
+ output: outcome.output,
281
+ durationMs
282
+ });
283
+ }
284
+ const result = { passed: failedCheck === void 0, checks };
285
+ if (failedCheck !== void 0) result.failedCheck = failedCheck;
286
+ return result;
287
+ }
288
+ function fixInstruction(result) {
289
+ const failed = result.checks.filter((c) => c.status === "failed");
290
+ if (failed.length === 0) {
291
+ return "The Structure-Lock check failed. Fix the project's harness checks before this work can be verified or merged.";
292
+ }
293
+ let out = `The Structure-Lock check failed: ${failed.length} project harness check${failed.length === 1 ? "" : "s"} did not pass. They MUST all pass before this work can be verified or merged. Re-run each one locally and fix every violation it reports:`;
294
+ for (const c of failed) {
295
+ out += `
296
+
297
+ --- \`${c.name}\` ---
298
+ Command: ${c.command}
299
+
300
+ Output:
301
+ ${c.output ?? "(no output captured)"}`;
302
+ }
303
+ return out;
304
+ }
305
+
306
+ // src/cli.ts
307
+ var import_meta = {};
308
+ var HELP = `@noctcore/harness \u2014 portable Structure-Lock runner
309
+
310
+ Usage:
311
+ harness [check] [options] Run the checks declared in .nightcore/harness.json
312
+ harness lint-meta [options] Run the portable lint-meta rules from the enumerated registry
313
+
314
+ Options:
315
+ --dir <path> Target directory to operate in (default: current directory)
316
+ --registry <path> lint-meta only: the rule registry to load
317
+ (default: ${DEFAULT_REGISTRY_RELATIVE_PATH}, relative to --dir)
318
+ --json check only: emit the machine-readable result to stdout instead of a summary
319
+ --version Print the runner version and exit
320
+ --help Print this help and exit
321
+
322
+ Exit codes:
323
+ 0 every check/rule passed (or nothing is configured to enforce)
324
+ 1 a check failed, a rule reported a critical violation or threw, or the manifest requires a newer runner
325
+ 2 a usage error`;
326
+ function parseArgs(argv, cwd) {
327
+ let command = "check";
328
+ let sawCommand = false;
329
+ let dir = cwd;
330
+ let registry;
331
+ let json = false;
332
+ let help = false;
333
+ let version = false;
334
+ for (let i = 0; i < argv.length; i += 1) {
335
+ const arg = argv[i];
336
+ if (arg === void 0) continue;
337
+ if (arg === "--json") {
338
+ json = true;
339
+ } else if (arg === "--help" || arg === "-h") {
340
+ help = true;
341
+ } else if (arg === "--version" || arg === "-v") {
342
+ version = true;
343
+ } else if (arg === "--dir") {
344
+ const next = argv[i + 1];
345
+ if (next !== void 0) {
346
+ dir = next;
347
+ i += 1;
348
+ }
349
+ } else if (arg.startsWith("--dir=")) {
350
+ dir = arg.slice("--dir=".length);
351
+ } else if (arg === "--registry") {
352
+ const next = argv[i + 1];
353
+ if (next !== void 0) {
354
+ registry = next;
355
+ i += 1;
356
+ }
357
+ } else if (arg.startsWith("--registry=")) {
358
+ registry = arg.slice("--registry=".length);
359
+ } else if (!arg.startsWith("-") && !sawCommand) {
360
+ command = arg;
361
+ sawCommand = true;
362
+ }
363
+ }
364
+ return { command, dir: import_node_path3.default.resolve(cwd, dir), registry, json, help, version };
365
+ }
366
+ function readVersion() {
367
+ try {
368
+ const raw = (0, import_node_fs2.readFileSync)(new URL("../package.json", import_meta.url), "utf8");
369
+ const pkg = JSON.parse(raw);
370
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
371
+ } catch {
372
+ return "0.0.0";
373
+ }
374
+ }
375
+ function nodeIO() {
376
+ return {
377
+ cwd: import_node_process.default.cwd(),
378
+ read(absolutePath) {
379
+ try {
380
+ return (0, import_node_fs2.readFileSync)(absolutePath, "utf8");
381
+ } catch {
382
+ return null;
383
+ }
384
+ },
385
+ spawn(program, args, options) {
386
+ const res = (0, import_node_child_process2.spawnSync)(program, args, {
387
+ cwd: options.cwd,
388
+ timeout: options.timeoutMs,
389
+ killSignal: "SIGKILL",
390
+ encoding: "utf8",
391
+ stdio: ["ignore", "pipe", "pipe"],
392
+ maxBuffer: 16 * 1024 * 1024
393
+ });
394
+ const err = res.error;
395
+ return {
396
+ status: res.status,
397
+ signal: res.signal,
398
+ stdout: res.stdout ?? "",
399
+ stderr: res.stderr ?? "",
400
+ error: err ? { code: err.code, message: err.message } : void 0
401
+ };
402
+ },
403
+ stdout: (line) => import_node_process.default.stdout.write(`${line}
404
+ `),
405
+ stderr: (line) => import_node_process.default.stderr.write(`${line}
406
+ `),
407
+ importModule: defaultImporter
408
+ };
409
+ }
410
+ function runCheck(parsed, io) {
411
+ const outcome = loadChecks(parsed.dir, io.read);
412
+ if (outcome.kind === "no-config") {
413
+ if (parsed.json) {
414
+ io.stdout(JSON.stringify(emptyPass()));
415
+ } else {
416
+ io.stdout("No structure lock configured \u2014 nothing to enforce.");
417
+ }
418
+ return 0;
419
+ }
420
+ if (outcome.kind === "schema-too-new") {
421
+ const label = Number.isFinite(outcome.found) ? String(outcome.found) : "an unrecognized value";
422
+ io.stderr(
423
+ `This .nightcore/harness.json declares schemaVersion ${label}, which this runner does not understand. This bundle was authored by a newer Nightcore \u2014 upgrade @noctcore/harness.`
424
+ );
425
+ return 1;
426
+ }
427
+ const result = runChecks(outcome.checks, parsed.dir, io.spawn, (check) => {
428
+ const line = `\u2192 ${check.name}: ${check.command}`;
429
+ if (parsed.json) io.stderr(line);
430
+ else io.stdout(line);
431
+ });
432
+ if (parsed.json) {
433
+ io.stdout(JSON.stringify(result));
434
+ return result.passed ? 0 : 1;
435
+ }
436
+ for (const check of result.checks) {
437
+ const mark = check.status === "passed" ? "\u2713" : "\u2717";
438
+ const exit = check.exitCode != null ? ` (exit ${check.exitCode})` : "";
439
+ io.stdout(`${mark} ${check.name}${exit}`);
440
+ }
441
+ if (result.passed) {
442
+ const n = result.checks.length;
443
+ io.stdout(`
444
+ Structure lock passed (${n} check${n === 1 ? "" : "s"}).`);
445
+ return 0;
446
+ }
447
+ io.stderr(`
448
+ ${fixInstruction(result)}`);
449
+ return 1;
450
+ }
451
+ async function runLintMeta(parsed, io) {
452
+ const registryPath = parsed.registry ? import_node_path3.default.resolve(parsed.dir, parsed.registry) : import_node_path3.default.join(parsed.dir, DEFAULT_REGISTRY_RELATIVE_PATH);
453
+ if (io.read(registryPath) === null) {
454
+ io.stdout(
455
+ `No lint-meta registry at ${registryPath} \u2014 nothing to enforce. (Point --registry at your rule registry, or commit one at ${DEFAULT_REGISTRY_RELATIVE_PATH}.)`
456
+ );
457
+ return 0;
458
+ }
459
+ const loaded = await loadRegistry(registryPath, io.importModule ?? defaultImporter);
460
+ if (loaded.error !== void 0) {
461
+ io.stderr(`Failed to load the lint-meta registry at ${registryPath}: ${loaded.error}`);
462
+ return 1;
463
+ }
464
+ const n = loaded.rules.length;
465
+ io.stdout(`lint-meta: running ${n} rule${n === 1 ? "" : "s"} from ${registryPath}`);
466
+ const ctx = createNodeCtx(parsed.dir);
467
+ const outcomes = runMetaRules(loaded.rules, ctx, (rule) => io.stdout(`\u2192 ${rule.id}`));
468
+ const report = reportMetaOutcomes(outcomes);
469
+ for (const line of report.lines) io.stderr(line);
470
+ if (report.lines.length === 0) io.stdout("lint-meta: no violations");
471
+ return exitCodeFor(report);
472
+ }
473
+ function runCli(argv, io = nodeIO()) {
474
+ const parsed = parseArgs(argv, io.cwd);
475
+ if (parsed.help) {
476
+ io.stdout(HELP);
477
+ return 0;
478
+ }
479
+ if (parsed.version) {
480
+ io.stdout(readVersion());
481
+ return 0;
482
+ }
483
+ if (parsed.command === "check") return runCheck(parsed, io);
484
+ if (parsed.command === "lint-meta") return runLintMeta(parsed, io);
485
+ io.stderr(`Unknown command: ${parsed.command}. Run \`harness --help\`.`);
486
+ return 2;
487
+ }
488
+ function isMainModule() {
489
+ try {
490
+ const entry = import_node_process.default.argv[1];
491
+ if (entry === void 0) return false;
492
+ return (0, import_node_fs2.realpathSync)(entry) === (0, import_node_fs2.realpathSync)((0, import_node_url2.fileURLToPath)(import_meta.url));
493
+ } catch {
494
+ return false;
495
+ }
496
+ }
497
+ if (isMainModule()) {
498
+ void Promise.resolve(runCli(import_node_process.default.argv.slice(2))).then((code) => {
499
+ import_node_process.default.exit(code);
500
+ });
501
+ }
502
+ // Annotate the CommonJS export names for ESM import in node:
503
+ 0 && (module.exports = {
504
+ nodeIO,
505
+ runCli
506
+ });
package/dist/cli.d.cts ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { F as FileReader, S as SpawnFn } from './run-B6_KI2bV.cjs';
3
+
4
+ /** Imports the module at an ABSOLUTE path. Real or faked (bounded-eval tests). */
5
+ type ModuleImporter = (absPath: string) => Promise<unknown>;
6
+
7
+ /** The side-effecting surface `runCli` depends on — real or faked in tests. */
8
+ interface CliIO {
9
+ cwd: string;
10
+ read: FileReader;
11
+ spawn: SpawnFn;
12
+ stdout: (line: string) => void;
13
+ stderr: (line: string) => void;
14
+ /**
15
+ * Dynamic module import for the `lint-meta` bounded eval — real (`defaultImporter`)
16
+ * or a recording fake in tests. Optional: the `check` path never imports, and
17
+ * `runLintMeta` falls back to {@link defaultImporter} when it is absent.
18
+ */
19
+ importModule?: ModuleImporter;
20
+ }
21
+ /** The real Node-backed IO: `node:fs` reads and `node:child_process` spawns. */
22
+ declare function nodeIO(): CliIO;
23
+ /**
24
+ * Parse argv and dispatch. Returns the process exit code (never exits). The
25
+ * `check` path stays synchronous (`number`); the `lint-meta` path is async
26
+ * (bounded dynamic import), so the return type is `number | Promise<number>` and
27
+ * the bin entry awaits it.
28
+ */
29
+ declare function runCli(argv: string[], io?: CliIO): number | Promise<number>;
30
+
31
+ export { type CliIO, nodeIO, runCli };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { F as FileReader, S as SpawnFn } from './run-B6_KI2bV.js';
3
+
4
+ /** Imports the module at an ABSOLUTE path. Real or faked (bounded-eval tests). */
5
+ type ModuleImporter = (absPath: string) => Promise<unknown>;
6
+
7
+ /** The side-effecting surface `runCli` depends on — real or faked in tests. */
8
+ interface CliIO {
9
+ cwd: string;
10
+ read: FileReader;
11
+ spawn: SpawnFn;
12
+ stdout: (line: string) => void;
13
+ stderr: (line: string) => void;
14
+ /**
15
+ * Dynamic module import for the `lint-meta` bounded eval — real (`defaultImporter`)
16
+ * or a recording fake in tests. Optional: the `check` path never imports, and
17
+ * `runLintMeta` falls back to {@link defaultImporter} when it is absent.
18
+ */
19
+ importModule?: ModuleImporter;
20
+ }
21
+ /** The real Node-backed IO: `node:fs` reads and `node:child_process` spawns. */
22
+ declare function nodeIO(): CliIO;
23
+ /**
24
+ * Parse argv and dispatch. Returns the process exit code (never exits). The
25
+ * `check` path stays synchronous (`number`); the `lint-meta` path is async
26
+ * (bounded dynamic import), so the return type is `number | Promise<number>` and
27
+ * the bin entry awaits it.
28
+ */
29
+ declare function runCli(argv: string[], io?: CliIO): number | Promise<number>;
30
+
31
+ export { type CliIO, nodeIO, runCli };