@nudojs/cli 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,829 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __nudoCreateRequire } from "module";
3
+ const require = __nudoCreateRequire(import.meta.url);
4
+ import {
5
+ resetMemo
6
+ } from "./chunk-YDM4HHQ4.js";
7
+
8
+ // src/index.ts
9
+ import { readFileSync, existsSync, watch, readdirSync, statSync, writeFileSync, mkdirSync } from "fs";
10
+ import { resolve, dirname, relative, join, basename } from "path";
11
+ import { fileURLToPath } from "url";
12
+ import { Command } from "commander";
13
+ import { typeValueToString as typeValueToString2 } from "@nudojs/core";
14
+ import {
15
+ typeValueToZodSchema,
16
+ generateGuardFunction,
17
+ generateGuardFunctionFromAbs,
18
+ generateFunctionDtsLines,
19
+ analyzeFileAsync,
20
+ buildModuleGraph,
21
+ computeDirtySet,
22
+ topoSortDirty,
23
+ collectCallRecords,
24
+ stripGeneratedCaseDirectives,
25
+ insertGeneratedCaseDirectives,
26
+ unifiedDiff,
27
+ isNudoTargetPath,
28
+ collectDtsFromEntry,
29
+ evictAnalysisCachesForFiles
30
+ } from "@nudojs/service";
31
+ import { harvestDts, emitEnvModule } from "@nudojs/harvester";
32
+
33
+ // src/run-test.ts
34
+ import { isSubtypeOf, typeValueToString } from "@nudojs/core";
35
+ function buildTestReport(file, result) {
36
+ const outcomes = [];
37
+ for (const fn of result.functions) {
38
+ for (const c of fn.cases) {
39
+ if (c.source !== "directive") continue;
40
+ const actual = typeValueToString(c.result);
41
+ if (!c.expected) {
42
+ outcomes.push({
43
+ fn: fn.name,
44
+ caseName: c.name,
45
+ ok: true,
46
+ unchecked: true,
47
+ actual
48
+ });
49
+ continue;
50
+ }
51
+ const expected = typeValueToString(c.expected);
52
+ let ok = false;
53
+ try {
54
+ ok = isSubtypeOf(c.result, c.expected);
55
+ } catch {
56
+ ok = false;
57
+ }
58
+ outcomes.push({
59
+ fn: fn.name,
60
+ caseName: c.name,
61
+ ok,
62
+ unchecked: false,
63
+ expected,
64
+ actual
65
+ });
66
+ }
67
+ }
68
+ return {
69
+ file,
70
+ passed: outcomes.filter((o) => o.ok && !o.unchecked).length,
71
+ failed: outcomes.filter((o) => !o.ok).length,
72
+ unchecked: outcomes.filter((o) => o.unchecked).length,
73
+ outcomes
74
+ };
75
+ }
76
+ function formatTestReport(r) {
77
+ const lines = [];
78
+ lines.push(`nudo test ${r.file}`);
79
+ if (r.passed + r.failed + r.unchecked === 0) {
80
+ lines.push("No @nudo:case directives found.");
81
+ return lines.join("\n");
82
+ }
83
+ lines.push(r.failed === 0 ? "PASS" : "FAILED");
84
+ lines.push(` ${r.passed} passed \xB7 ${r.failed} failed \xB7 ${r.unchecked} unchecked`);
85
+ lines.push("");
86
+ for (const o of r.outcomes) {
87
+ if (o.unchecked) {
88
+ lines.push(` [unchecked] ${o.fn} case "${o.caseName}" \u2192 ${o.actual}`);
89
+ } else if (o.ok) {
90
+ lines.push(` [ok] ${o.fn} case "${o.caseName}" \u2192 ${o.actual}`);
91
+ } else {
92
+ lines.push(` [FAIL] ${o.fn} case "${o.caseName}"`);
93
+ lines.push(` expected: ${o.expected}`);
94
+ lines.push(` actual: ${o.actual}`);
95
+ }
96
+ }
97
+ return lines.join("\n");
98
+ }
99
+
100
+ // src/index.ts
101
+ var program = new Command();
102
+ function readPackageVersion() {
103
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
104
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
105
+ return pkg.version ?? "0.0.0";
106
+ }
107
+ program.name("nudo").description("Nudo type inference engine").version(readPackageVersion());
108
+ async function reemitUpdate(filePath, source, callsites) {
109
+ const stripped = stripGeneratedCaseDirectives(source);
110
+ const result = await analyzeFileAsync(filePath, stripped.source, void 0, callsites);
111
+ const emitOut = insertGeneratedCaseDirectives(stripped.source, result);
112
+ return { result, emitOut, removed: stripped.removed };
113
+ }
114
+ function collectExternalRecords(sites) {
115
+ const records = [];
116
+ for (const site of sites) {
117
+ const sitePath = resolve(site);
118
+ if (!existsSync(sitePath)) {
119
+ console.error(`Callsite file not found: ${sitePath}`);
120
+ process.exitCode = 1;
121
+ continue;
122
+ }
123
+ const siteFiles = statSync(sitePath).isDirectory() ? collectNudoFiles(sitePath) : [sitePath];
124
+ for (const sf of siteFiles) {
125
+ records.push(...collectCallRecords(sf, readFileSync(sf, "utf-8")));
126
+ }
127
+ }
128
+ return records.length > 0 ? records : void 0;
129
+ }
130
+ async function runInfer(file, options = {}) {
131
+ const filePath = resolve(file);
132
+ const source = readFileSync(filePath, "utf-8");
133
+ let result = await analyzeFileAsync(filePath, source, void 0, options.callsites);
134
+ let emitOut;
135
+ if (options.emit) {
136
+ if (options.emit.mode === "update") {
137
+ const re = await reemitUpdate(filePath, source, options.callsites);
138
+ result = re.result;
139
+ emitOut = re.emitOut;
140
+ } else {
141
+ emitOut = insertGeneratedCaseDirectives(source, result);
142
+ }
143
+ }
144
+ if (result.functions.length === 0 && !result.externalFunctions?.length) {
145
+ console.log("No functions with @nudo:case directives found.");
146
+ return;
147
+ }
148
+ const dtsLines = [];
149
+ for (const fn of result.functions) {
150
+ const loc = fn.loc;
151
+ const locStr = `${relative(process.cwd(), filePath)}:${loc.start.line}:${loc.start.column}`;
152
+ const header = options.showLoc ? `=== ${fn.name} (${locStr}) ===` : `=== ${fn.name} ===`;
153
+ console.log(`${header}
154
+ `);
155
+ if (options.dts) {
156
+ dtsLines.push(...generateFunctionDtsLines(fn));
157
+ }
158
+ if (fn.skipped) {
159
+ if (fn.combined) {
160
+ console.log(`Skipped (declared): ${typeValueToString2(fn.combined)}`);
161
+ } else {
162
+ console.log("Skipped (no return type declared)");
163
+ }
164
+ console.log();
165
+ continue;
166
+ }
167
+ for (const c of fn.cases) {
168
+ const argsStr = c.args.map(typeValueToString2).join(", ");
169
+ let line = `Case "${c.name}": (${argsStr}) => ${typeValueToString2(c.result)}`;
170
+ if (c.throws.kind !== "never") line += ` throws ${typeValueToString2(c.throws)}`;
171
+ console.log(line);
172
+ if (c.intension?.display) {
173
+ console.log(` intension: ${c.intension.display}`);
174
+ } else if (c.intension?.term || c.intension?.pred) {
175
+ const parts = [];
176
+ if (c.intension.term) parts.push(`term=${c.intension.term}`);
177
+ if (c.intension.pred) parts.push(`pred: ${c.intension.pred}`);
178
+ if (c.intension.conf) parts.push(`#${c.intension.conf}`);
179
+ console.log(` ${parts.join(" ")}`);
180
+ }
181
+ if (c.intension?.abs) {
182
+ console.log(` abs: ${c.intension.abs}`);
183
+ }
184
+ }
185
+ if (fn.entryOnly) {
186
+ console.log("# no call sites found; parameters default to unknown");
187
+ }
188
+ if (fn.cases.length > 1 && fn.combined) {
189
+ console.log(`
190
+ Combined: ${typeValueToString2(fn.combined)}`);
191
+ }
192
+ console.log();
193
+ }
194
+ if (result.externalFunctions && result.externalFunctions.length > 0) {
195
+ const byModule = /* @__PURE__ */ new Map();
196
+ for (const fn of result.externalFunctions) {
197
+ const mod = fn.fromModule ?? "";
198
+ const list = byModule.get(mod);
199
+ if (list) list.push(fn);
200
+ else byModule.set(mod, [fn]);
201
+ }
202
+ for (const [mod, fns] of byModule) {
203
+ const rel = relative(process.cwd(), mod) || mod;
204
+ console.log(`--- ${rel} (imported) ---
205
+ `);
206
+ for (const fn of fns) {
207
+ console.log(`=== ${fn.name} ===
208
+ `);
209
+ for (const c of fn.cases) {
210
+ const argsStr = c.args.map(typeValueToString2).join(", ");
211
+ let line = `Case "${c.name}": (${argsStr}) => ${typeValueToString2(c.result)}`;
212
+ if (c.throws.kind !== "never") line += ` throws ${typeValueToString2(c.throws)}`;
213
+ console.log(line);
214
+ }
215
+ if (fn.cases.length > 1 && fn.combined) {
216
+ console.log(`
217
+ Combined: ${typeValueToString2(fn.combined)}`);
218
+ }
219
+ console.log();
220
+ }
221
+ }
222
+ }
223
+ if (options.dts && dtsLines.length > 0) {
224
+ const dtsPath = filePath.replace(/\.[cm]?js$|\.ts$/, ".d.ts");
225
+ const dtsContent = dtsLines.join("\n") + "\n";
226
+ writeFileSync(dtsPath, dtsContent, "utf-8");
227
+ console.log(`Generated: ${relative(process.cwd(), dtsPath)}`);
228
+ }
229
+ if (result.diagnostics.length > 0) {
230
+ console.log("Diagnostics:\n");
231
+ for (const d of result.diagnostics) {
232
+ const loc = `${relative(process.cwd(), filePath)}:${d.range.start.line}:${d.range.start.column}`;
233
+ console.log(` [${d.severity}] ${loc} ${d.message}${d.code ? ` (${d.code})` : ""}`);
234
+ if (d.origin) {
235
+ console.log(` \u2192 value originates at ${d.origin.line}:${d.origin.column}`);
236
+ }
237
+ }
238
+ console.log();
239
+ }
240
+ if (options.emit && emitOut) {
241
+ const relPath = relative(process.cwd(), filePath) || filePath;
242
+ const skippedLines = emitOut.skipped.map((s) => ` ${s.fn}: ${s.reason}${s.detail ? ` (${s.detail})` : ""}`);
243
+ if (emitOut.source === source) {
244
+ console.log("No changes.");
245
+ for (const line of skippedLines) console.log(line);
246
+ return;
247
+ }
248
+ if (options.emit.dryRun) {
249
+ console.log(`Would emit cases \u2192 ${relPath} (dry run)`);
250
+ for (const w of emitOut.written) console.log(` ${w.fn}: ${w.cases.join(", ")}`);
251
+ for (const line of skippedLines) console.log(line);
252
+ console.log();
253
+ process.stdout.write(unifiedDiff(source, emitOut.source, relPath));
254
+ if (options.emit.exitOnDiff) process.exitCode = 1;
255
+ return;
256
+ }
257
+ writeFileSync(filePath, emitOut.source, "utf-8");
258
+ const directiveCount = emitOut.written.reduce((n, w) => n + w.cases.length, 0);
259
+ console.log(
260
+ `Emitted cases \u2192 ${relPath} (${directiveCount} directive(s) across ${emitOut.written.length} function(s))`
261
+ );
262
+ for (const w of emitOut.written) console.log(` ${w.fn}: ${w.cases.join(", ")}`);
263
+ for (const line of skippedLines) console.log(line);
264
+ }
265
+ }
266
+ async function runInferJson(file, externalRecords) {
267
+ const filePath = resolve(file);
268
+ const source = readFileSync(filePath, "utf-8");
269
+ const result = await analyzeFileAsync(filePath, source, void 0, externalRecords);
270
+ const { serializeInferJson } = await import("@nudojs/service");
271
+ console.log(JSON.stringify(serializeInferJson(result, filePath), null, 2));
272
+ }
273
+ program.command("infer").description("Infer types from a JS/TS file (or a directory of them) \u2014 functions with @nudo:case directives use them; all other functions are inferred from call sites (whole-program analysis)").argument("<file>", "Path to the JS/TS file (or directory)").option("--dts", "Generate .d.ts file").option("--loc", "Show source locations in output").option("--json", "Output as JSON").option("--callsites <paths...>", "Usage-site files (tests/apps) to harvest real call shapes from; their calls to this file's exports become synthesized cases").option("--emit-cases [mode]", "Write synthesized call-site cases back into the source as @nudo:case directives (call@ prefix); mode: update (default: add)").option("--dry-run", "With --emit-cases: print a unified diff instead of writing to disk").option("--exit-on-diff", "With --dry-run: exit with code 1 when the diff is non-empty").action(
274
+ async (file, opts) => {
275
+ let emit;
276
+ if (opts.emitCases !== void 0) {
277
+ let mode;
278
+ if (opts.emitCases === true) mode = "add";
279
+ else if (opts.emitCases === "update") mode = "update";
280
+ else {
281
+ console.error(`Invalid --emit-cases value: ${opts.emitCases} (expected: =update, or omit the value for add)`);
282
+ process.exitCode = 1;
283
+ return;
284
+ }
285
+ if (opts.json) {
286
+ console.error("--emit-cases cannot be combined with --json");
287
+ process.exitCode = 1;
288
+ return;
289
+ }
290
+ emit = { mode, dryRun: opts.dryRun === true, exitOnDiff: opts.exitOnDiff === true };
291
+ }
292
+ if (opts.exitOnDiff && !opts.dryRun) {
293
+ console.error("--exit-on-diff requires --dry-run");
294
+ process.exitCode = 1;
295
+ return;
296
+ }
297
+ let externalRecords;
298
+ if (opts.callsites?.length) {
299
+ externalRecords = collectExternalRecords(opts.callsites);
300
+ }
301
+ const target = resolve(file);
302
+ if (existsSync(target) && statSync(target).isDirectory()) {
303
+ const files = collectNudoFiles(target);
304
+ if (files.length === 0) {
305
+ console.log("No nudo files found in directory.");
306
+ return;
307
+ }
308
+ if (opts.json) {
309
+ console.error("--json requires a single file, not a directory");
310
+ process.exitCode = 1;
311
+ return;
312
+ }
313
+ for (const f of files) {
314
+ try {
315
+ await runInfer(f, { dts: opts.dts, showLoc: opts.loc, callsites: externalRecords, emit });
316
+ } catch (err) {
317
+ console.error(`Error analyzing ${relative(process.cwd(), f)}:`, err.message);
318
+ }
319
+ }
320
+ return;
321
+ }
322
+ if (opts.json) {
323
+ await runInferJson(file, externalRecords);
324
+ } else {
325
+ await runInfer(file, { dts: opts.dts, showLoc: opts.loc, callsites: externalRecords, emit });
326
+ }
327
+ }
328
+ );
329
+ async function runCheck(file, opts = {}) {
330
+ const filePath = resolve(file);
331
+ const source = readFileSync(filePath, "utf-8");
332
+ const { checkSource, formatCheckReport, serializeCheckJson, pTrue } = await import("@nudojs/core");
333
+ const { defaultLoadModule: loadModule } = await import("@nudojs/service");
334
+ const algebraReport = checkSource(filePath, source, pTrue, {
335
+ loadModule,
336
+ fromFile: filePath
337
+ });
338
+ if (opts.json) {
339
+ console.log(JSON.stringify(serializeCheckJson(algebraReport), null, 2));
340
+ } else {
341
+ console.log(formatCheckReport(algebraReport, { verbose: true }));
342
+ }
343
+ if (!algebraReport.ok) {
344
+ process.exitCode = 1;
345
+ }
346
+ }
347
+ function resolveTargets(path) {
348
+ const resolved = resolve(path);
349
+ if (!existsSync(resolved)) {
350
+ console.error(`Not found: ${resolved}`);
351
+ process.exitCode = 1;
352
+ return [];
353
+ }
354
+ if (statSync(resolved).isDirectory()) {
355
+ const files = collectNudoFiles(resolved);
356
+ if (files.length === 0) {
357
+ console.error(`No nudo files found in directory: ${resolved}`);
358
+ process.exitCode = 1;
359
+ }
360
+ return files;
361
+ }
362
+ return [resolved];
363
+ }
364
+ program.command("types").description("Type-as-computation view: show term + constraints from the algebra (not just extensional shape)").argument("<file>", "Path to the JS/TS file or a directory of them").option("--fn <name>", "Only analyze this function").option("--assume <pred...>", "Assume constraints, e.g. x>0 y>=1").option("--generalize", "Show polymorphic signatures via symbolic execution").action(
365
+ async (file, opts) => {
366
+ const targets = resolveTargets(file);
367
+ if (targets.length === 0) return;
368
+ for (const t of targets) {
369
+ await runTypes(t, opts);
370
+ if (targets.length > 1) console.log("");
371
+ }
372
+ }
373
+ );
374
+ async function runTypes(filePath, opts) {
375
+ const { readFileSync: readFileSync2 } = await import("fs");
376
+ const { basename: basename2 } = await import("path");
377
+ const algebra = await import("@nudojs/core");
378
+ const source = readFileSync2(filePath, "utf8");
379
+ let phi = algebra.pTrue;
380
+ const assumeIds = /* @__PURE__ */ new Set();
381
+ for (const a of opts.assume ?? []) {
382
+ const m = /^([A-Za-z_$][\w$]*)\s*(>=|>)\s*(-?\d+(?:\.\d+)?)$/.exec(a.trim());
383
+ if (!m) {
384
+ console.error(`\u65E0\u6CD5\u89E3\u6790 --assume: ${a}\uFF08\u652F\u6301 x>0 / x>=1\uFF09`);
385
+ continue;
386
+ }
387
+ const id = m[1];
388
+ const n = Number(m[3]);
389
+ phi = algebra.gtNum(algebra.v(id), n);
390
+ assumeIds.add(id);
391
+ }
392
+ const list = opts.fn ? [opts.fn] : algebra.listFunctionNames(source);
393
+ if (list.length === 0) {
394
+ console.error(`\u672A\u627E\u5230\u51FD\u6570: ${basename2(filePath)}`);
395
+ process.exitCode = 1;
396
+ return;
397
+ }
398
+ const { defaultLoadModule: loadModule } = await import("@nudojs/service");
399
+ console.log(`nudo types ${basename2(filePath)}`);
400
+ if (assumeIds.size > 0) {
401
+ console.log(`assume: ${[...assumeIds].map((id) => `${id} > 0`).join(", ")}`);
402
+ }
403
+ if (opts.generalize) {
404
+ console.log("mode: generalize (symbolic \u03B1)\n");
405
+ } else {
406
+ console.log("");
407
+ }
408
+ for (const name of list) {
409
+ if (opts.generalize) {
410
+ const g = algebra.generalizeFromAst(name, source, {
411
+ refine: { loadModule, fromFile: filePath }
412
+ });
413
+ if (!g) continue;
414
+ console.log(g.display);
415
+ console.log("");
416
+ continue;
417
+ }
418
+ const args = algebra.buildArgsFromAssume(source, name, assumeIds);
419
+ const result = algebra.analyzeFn(source, name, args, phi);
420
+ const label = `${name}(${args.map((a) => algebra.formatShape(a)).join(", ")})`;
421
+ console.log(algebra.formatAbsMultiline(result, label));
422
+ console.log("");
423
+ }
424
+ }
425
+ program.command("check").description("Check JS/TS file(s) or directory(s) for type errors \u2014 exits with code 1 when errors are found").argument("<paths...>", "File(s) or directory(s) to check").option("--json", "Emit stable CheckJson (CI / Agent contract; single file only)").action(async (paths, opts) => {
426
+ const targets = [];
427
+ for (const p of paths) {
428
+ targets.push(...resolveTargets(p));
429
+ }
430
+ if (targets.length === 0) return;
431
+ if (opts.json && targets.length > 1) {
432
+ console.error("--json requires a single file, not multiple targets");
433
+ process.exitCode = 1;
434
+ return;
435
+ }
436
+ for (const t of targets) {
437
+ await runCheck(t, opts);
438
+ }
439
+ });
440
+ program.command("test").description("Run @nudo:case directives as assertions (case-as-test); exit 1 on failure").argument("<file>", "Path to the JS/TS file or a directory of them").action(async (file) => {
441
+ const targets = resolveTargets(file);
442
+ if (targets.length === 0) return;
443
+ for (const filePath of targets) {
444
+ const source = readFileSync(filePath, "utf-8");
445
+ try {
446
+ const result = await analyzeFileAsync(filePath, source);
447
+ const report = buildTestReport(filePath, result);
448
+ console.log(formatTestReport(report));
449
+ if (report.failed > 0) process.exitCode = 1;
450
+ } catch (err) {
451
+ console.error(`nudo test failed to analyze ${filePath}: ${err.message}`);
452
+ process.exitCode = 1;
453
+ }
454
+ }
455
+ });
456
+ var displayPath = (p) => {
457
+ const rel = relative(process.cwd(), p);
458
+ return rel === "" || rel.startsWith("..") ? p : rel;
459
+ };
460
+ async function doctorFile(filePath, records) {
461
+ const report = { file: displayPath(filePath), functions: 0, entryOnly: 0, uncovered: [] };
462
+ let source;
463
+ try {
464
+ source = readFileSync(filePath, "utf-8");
465
+ } catch (err) {
466
+ report.error = err.message;
467
+ return report;
468
+ }
469
+ try {
470
+ const result = await analyzeFileAsync(filePath, source, void 0, records);
471
+ report.functions = result.functions.length;
472
+ report.entryOnly = result.functions.filter((fn) => fn.entryOnly).length;
473
+ report.uncovered = result.functions.filter((fn) => fn.cases.length === 0 && !fn.skipped && !fn.entryOnly).map((fn) => fn.name);
474
+ if (records) {
475
+ const { emitOut, removed } = await reemitUpdate(filePath, source, records);
476
+ if (emitOut.source !== source) {
477
+ report.drift = {
478
+ added: emitOut.written.reduce((n, w) => n + w.cases.length, 0),
479
+ removed: removed.length
480
+ };
481
+ }
482
+ }
483
+ } catch (err) {
484
+ report.error = err.message;
485
+ }
486
+ return report;
487
+ }
488
+ async function runDoctor(paths, opts) {
489
+ const targetPaths = paths.length > 0 ? paths : ["."];
490
+ const externalRecords = opts.callsites?.length ? collectExternalRecords(opts.callsites) : void 0;
491
+ const files = [];
492
+ const reports = [];
493
+ for (const p of targetPaths) {
494
+ const abs = resolve(p);
495
+ if (!existsSync(abs)) {
496
+ reports.push({ file: displayPath(abs), functions: 0, entryOnly: 0, uncovered: [], error: `File not found: ${abs}` });
497
+ continue;
498
+ }
499
+ files.push(...statSync(abs).isDirectory() ? collectNudoFiles(abs) : [abs]);
500
+ }
501
+ for (const filePath of files) {
502
+ reports.push(await doctorFile(filePath, externalRecords));
503
+ }
504
+ const driftCount = reports.filter((r) => r.drift).length;
505
+ const errorCount = reports.filter((r) => r.error).length;
506
+ const uncoveredTotal = reports.reduce((n, r) => n + r.uncovered.length, 0);
507
+ const failed = driftCount > 0 || errorCount > 0;
508
+ if (opts.json) {
509
+ console.log(
510
+ JSON.stringify(
511
+ {
512
+ // ok 与退出码一致:drift/报错 → false;uncovered 仅为信息级不影响
513
+ ok: !failed,
514
+ files: reports.map((r) => ({
515
+ file: r.file,
516
+ functions: r.functions,
517
+ entryOnly: r.entryOnly,
518
+ uncovered: r.uncovered,
519
+ ...r.drift ? { drift: r.drift } : {},
520
+ ...r.error ? { error: r.error } : {}
521
+ })),
522
+ summary: { files: reports.length, drift: driftCount, errors: errorCount, uncovered: uncoveredTotal }
523
+ },
524
+ null,
525
+ 2
526
+ )
527
+ );
528
+ } else {
529
+ if (reports.length === 0) {
530
+ console.log("No files to check.");
531
+ return;
532
+ }
533
+ for (const r of reports) {
534
+ console.log(`${r.file}`);
535
+ if (r.error) {
536
+ console.log(` \u2717 error: ${r.error}`);
537
+ continue;
538
+ }
539
+ const entryInfo = r.entryOnly > 0 ? `, ${r.entryOnly} entry-only` : "";
540
+ console.log(` \xB7 ${r.functions} function(s)${entryInfo}`);
541
+ if (r.uncovered.length > 0) {
542
+ console.log(` \u26A0 uncovered (no cases): ${r.uncovered.join(", ")}`);
543
+ }
544
+ if (r.drift) {
545
+ const refresh = `nudo infer ${r.file} --callsites ${(opts.callsites ?? []).join(" ")} --emit-cases=update`;
546
+ console.log(
547
+ ` \u2717 drift: ${r.drift.added + r.drift.removed} directive(s) changed (+${r.drift.added} new, -${r.drift.removed} removed) \u2014 refresh with: ${refresh.trim()}`
548
+ );
549
+ }
550
+ }
551
+ console.log(
552
+ `
553
+ Summary: ${reports.length} file(s) \xB7 ${driftCount} drift \xB7 ${errorCount} error(s) \xB7 ${uncoveredTotal} uncovered function(s)`
554
+ );
555
+ console.log(failed ? "Result: FAIL (drift or errors found)" : "Result: OK (uncovered function(s) are informational only)");
556
+ }
557
+ if (failed) process.exitCode = 1;
558
+ }
559
+ program.command("doctor").description("Health-check JS files: functions without cases, call-site solidification drift (--callsites), analysis errors \u2014 exits 1 on drift/errors").argument("[paths...]", "File(s) or directory(s) to check (default: current directory)").option("--callsites <paths...>", "Usage-site files (tests/apps): re-solidify per current call shapes and report drift when directives would change").option("--json", "Output as JSON").action(async (paths, opts) => {
560
+ await runDoctor(paths, opts);
561
+ });
562
+ program.command("watch").description("Watch file(s) for changes and re-run inference").argument("<path>", "File or directory to watch").option("--dts", "Generate .d.ts files on each change").action((watchPath, opts) => {
563
+ const resolved = resolve(watchPath);
564
+ const isDir = existsSync(resolved) && statSync(resolved).isDirectory();
565
+ const getFiles = () => {
566
+ if (!isDir) return [resolved];
567
+ return collectNudoFiles(resolved);
568
+ };
569
+ let graph = buildModuleGraph(getFiles());
570
+ const runAll = async () => {
571
+ console.clear();
572
+ console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Analyzing...
573
+ `);
574
+ for (const f of getFiles()) {
575
+ try {
576
+ await runInfer(f, { dts: opts.dts, showLoc: true });
577
+ } catch (err) {
578
+ console.error(`Error analyzing ${relative(process.cwd(), f)}:`, err.message);
579
+ }
580
+ }
581
+ graph = buildModuleGraph(getFiles());
582
+ console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Watching for changes...`);
583
+ };
584
+ const runIncremental = async (changedFiles) => {
585
+ const files = getFiles();
586
+ const tracked = new Set(files);
587
+ const dirtyUnion = /* @__PURE__ */ new Set();
588
+ for (const cf of changedFiles) {
589
+ for (const d of computeDirtySet(graph.dependents, cf)) dirtyUnion.add(d);
590
+ }
591
+ const dirty = [...dirtyUnion].filter((f) => tracked.has(f));
592
+ if (dirty.length === 0) return;
593
+ const ordered = topoSortDirty(graph.imports, dirty);
594
+ console.clear();
595
+ console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Analyzing (incremental)...
596
+ `);
597
+ const t0 = performance.now();
598
+ resetMemo();
599
+ evictAnalysisCachesForFiles(ordered);
600
+ for (const f of ordered) {
601
+ try {
602
+ await runInfer(f, { dts: opts.dts, showLoc: true });
603
+ } catch (err) {
604
+ console.error(`Error analyzing ${relative(process.cwd(), f)}:`, err.message);
605
+ }
606
+ }
607
+ const elapsed = Math.round(performance.now() - t0);
608
+ console.log(`Incremental: re-analyzed ${ordered.length}, skipped ${files.length - ordered.length} (${elapsed}ms)`);
609
+ console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Watching for changes...`);
610
+ graph = buildModuleGraph(getFiles());
611
+ };
612
+ runAll().catch(() => {
613
+ });
614
+ let debounceTimer = null;
615
+ const watchTarget = isDir ? resolved : dirname(resolved);
616
+ const pendingChanged = /* @__PURE__ */ new Set();
617
+ watch(watchTarget, { recursive: isDir }, (_event, filename) => {
618
+ if (!filename) return;
619
+ const fullPath = isDir ? join(watchTarget, filename) : resolved;
620
+ if (!isNudoTargetPath(fullPath)) return;
621
+ if (!existsSync(fullPath)) return;
622
+ pendingChanged.add(fullPath);
623
+ if (debounceTimer) clearTimeout(debounceTimer);
624
+ debounceTimer = setTimeout(() => {
625
+ const changedFiles = [...pendingChanged];
626
+ pendingChanged.clear();
627
+ runIncremental(changedFiles).catch(() => {
628
+ });
629
+ }, 200);
630
+ });
631
+ });
632
+ function collectNudoFiles(dir) {
633
+ const results = [];
634
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
635
+ const fullPath = join(dir, entry.name);
636
+ if (entry.isDirectory() && entry.name !== "node_modules") {
637
+ results.push(...collectNudoFiles(fullPath));
638
+ } else if (entry.isFile() && isNudoTargetPath(fullPath)) {
639
+ results.push(fullPath);
640
+ }
641
+ }
642
+ return results;
643
+ }
644
+ async function runGenerate(file, format, output) {
645
+ const filePath = resolve(file);
646
+ const source = readFileSync(filePath, "utf-8");
647
+ const result = await analyzeFileAsync(filePath, source);
648
+ const functions = result.functions.filter(
649
+ (f) => f.cases.some((c) => c.source === "directive")
650
+ );
651
+ if (functions.length === 0) {
652
+ console.log("No functions with @nudo:case directives found.");
653
+ return;
654
+ }
655
+ const zodChunks = [];
656
+ const guardChunks = [];
657
+ const dtsChunks = [];
658
+ for (const fn of functions) {
659
+ const caseResults = fn.cases.filter((c) => c.source === "directive");
660
+ const baseName = fn.name;
661
+ if (format === "zod" || format === "all") {
662
+ const lines = [`
663
+ // === ${baseName} Zod Schemas ===`];
664
+ for (const c of caseResults) {
665
+ const inputSchemas = c.args.map((a, i) => `arg${i}: ${typeValueToZodSchema(a)}`).join(", ");
666
+ const outputSchema = typeValueToZodSchema(c.result);
667
+ lines.push(`// Case "${c.name}":`);
668
+ lines.push(`// Input: { ${inputSchemas} }`);
669
+ lines.push(`// Output: ${outputSchema}`);
670
+ }
671
+ zodChunks.push(lines.join("\n"));
672
+ }
673
+ if (format === "guard" || format === "all") {
674
+ const lines = [`
675
+ // === ${baseName} Type Guards ===`];
676
+ for (const c of caseResults) {
677
+ const guardName = `is${baseName}${c.name.charAt(0).toUpperCase() + c.name.slice(1)}Output`;
678
+ if (c.abs) {
679
+ lines.push(generateGuardFunctionFromAbs(guardName, c.abs));
680
+ } else {
681
+ lines.push(generateGuardFunction(guardName, c.result));
682
+ }
683
+ }
684
+ guardChunks.push(lines.join("\n"));
685
+ }
686
+ if (format === "dts" || format === "all") {
687
+ dtsChunks.push(generateFunctionDtsLines(fn).join("\n"));
688
+ }
689
+ }
690
+ const stem = basename(filePath).replace(/\.[cm]?[jt]s$/, "");
691
+ if (output) {
692
+ const outDir = resolve(output);
693
+ mkdirSync(outDir, { recursive: true });
694
+ const written = [];
695
+ if (zodChunks.length > 0) {
696
+ const p = join(outDir, `${stem}.nudo.zod.ts`);
697
+ writeFileSync(p, zodChunks.join("\n") + "\n", "utf-8");
698
+ written.push(p);
699
+ }
700
+ if (guardChunks.length > 0) {
701
+ const p = join(outDir, `${stem}.nudo.guard.ts`);
702
+ writeFileSync(p, guardChunks.join("\n") + "\n", "utf-8");
703
+ written.push(p);
704
+ }
705
+ if (dtsChunks.length > 0) {
706
+ const p = join(outDir, `${stem}.d.ts`);
707
+ writeFileSync(p, dtsChunks.join("\n") + "\n", "utf-8");
708
+ written.push(p);
709
+ }
710
+ for (const p of written) {
711
+ console.log(`wrote ${relative(process.cwd(), p)}`);
712
+ }
713
+ return;
714
+ }
715
+ for (const chunk of [...zodChunks, ...guardChunks, ...dtsChunks]) {
716
+ console.log(chunk);
717
+ }
718
+ }
719
+ program.command("generate").description("Generate runtime validators from inferred types").argument("<file>", "JavaScript file to analyze").option("--format <format>", "Output format: zod, guard, dts, all", "all").option("--output <dir>", "Write validator files to this directory (omit for stdout)").action(async (file, options) => {
720
+ const format = options.format;
721
+ if (!["zod", "guard", "dts", "all"].includes(format)) {
722
+ console.error(`Unknown --format ${format}; expected zod | guard | dts | all`);
723
+ process.exitCode = 1;
724
+ return;
725
+ }
726
+ await runGenerate(file, format, options.output);
727
+ });
728
+ program.command("emit").description("Emit TypeScript .d.ts declarations from inferred types (npm compatibility exit)").argument("<file>", "JavaScript file to analyze").option("--output <dir>", "Write <stem>.d.ts to this directory (omit for stdout)").action(async (file, options) => {
729
+ await runGenerate(file, "dts", options.output);
730
+ });
731
+ program.command("guard").description("Generate runtime type-guard functions from inferred result types").argument("<file>", "JavaScript file to analyze").option("--output <dir>", "Write <stem>.nudo.guard.ts to this directory (omit for stdout)").action(async (file, options) => {
732
+ await runGenerate(file, "guard", options.output);
733
+ });
734
+ var HARVEST_MAX_FILES = 200;
735
+ function runHarvest(pkg, outOpt) {
736
+ const typesDir = resolve(process.cwd(), "node_modules", "@types", pkg);
737
+ if (!existsSync(typesDir)) {
738
+ console.error(`Error: ${relative(process.cwd(), typesDir)} not found.`);
739
+ console.error(`Install the package first, e.g. pnpm add -D @types/${pkg}`);
740
+ process.exitCode = 1;
741
+ return;
742
+ }
743
+ let entry = join(typesDir, "index.d.ts");
744
+ const pkgJsonPath = join(typesDir, "package.json");
745
+ if (existsSync(pkgJsonPath)) {
746
+ try {
747
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
748
+ const declared = pkgJson.types ?? pkgJson.typings;
749
+ if (typeof declared === "string") {
750
+ const candidate = resolve(typesDir, declared);
751
+ if (existsSync(candidate)) entry = candidate;
752
+ }
753
+ } catch {
754
+ }
755
+ }
756
+ if (!existsSync(entry)) {
757
+ console.error(`Error: no .d.ts entry found in ${relative(process.cwd(), typesDir)} (tried ${basename(entry)}).`);
758
+ process.exitCode = 1;
759
+ return;
760
+ }
761
+ const files = collectDtsFromEntry(entry, HARVEST_MAX_FILES);
762
+ if (files.length === 0) {
763
+ console.error(`Error: no .d.ts files collected from ${relative(process.cwd(), entry)}.`);
764
+ process.exitCode = 1;
765
+ return;
766
+ }
767
+ const env = harvestDts(files);
768
+ const code = emitEnvModule(env, `@types/${pkg}`);
769
+ const out = resolve(outOpt ?? `nudo-harvest-${pkg}.ts`);
770
+ writeFileSync(out, code, "utf-8");
771
+ console.log(`Harvested @types/${pkg} \u2192 ${relative(process.cwd(), out) || out}`);
772
+ console.log(` files: ${env.stats.files}`);
773
+ console.log(` symbols: ${env.stats.symbols}`);
774
+ console.log(` skipped: ${env.stats.skipped}`);
775
+ console.log(`
776
+ Usage \u2014 add this directive at the top of your JS file:`);
777
+ const outDir = dirname(out);
778
+ const hintPath = resolve(process.cwd()) === outDir ? basename(out) : out;
779
+ console.log(` /// @nudo:env ${hintPath}`);
780
+ }
781
+ program.command("harvest").description("Convert @types/<pkg> .d.ts declarations into a Nudo env file (TS source using T.* constructors)").argument("[pkg]", "Package name under @types (e.g. node)").option("--out <file>", "Output .ts env file (default: ./nudo-harvest-<pkg>.ts)").option("--auto [dir]", "Scan directory (default .) for bare imports and report auto-harvestable @types packages").action(async (pkg, opts) => {
782
+ if (opts.auto !== void 0) {
783
+ const dir = resolve(typeof opts.auto === "string" ? opts.auto : ".");
784
+ const files = existsSync(dir) && statSync(dir).isDirectory() ? collectNudoFiles(dir) : existsSync(dir) ? [dir] : [];
785
+ if (files.length === 0) {
786
+ console.error(`No inference targets under ${dir}`);
787
+ process.exitCode = 1;
788
+ return;
789
+ }
790
+ const { collectBarePackages, harvestPackageCached, formatHarvestSummary } = await import("@nudojs/service");
791
+ const seen = /* @__PURE__ */ new Set();
792
+ const report = [];
793
+ for (const f of files) {
794
+ let src;
795
+ try {
796
+ src = readFileSync(f, "utf-8");
797
+ } catch {
798
+ continue;
799
+ }
800
+ for (const p of collectBarePackages(src)) {
801
+ if (seen.has(p)) continue;
802
+ seen.add(p);
803
+ const h = harvestPackageCached(p, dirname(f));
804
+ if (h) report.push(formatHarvestSummary(h));
805
+ else report.push(`${p}: no .d.ts / @types (skipped)`);
806
+ }
807
+ }
808
+ if (report.length === 0) {
809
+ console.log("No bare imports found (or nothing to harvest).");
810
+ return;
811
+ }
812
+ console.log(`auto harvest candidates under ${relative(process.cwd(), dir) || "."}:
813
+ `);
814
+ for (const line of report) console.log(line);
815
+ console.log(`
816
+ Analysis injects these automatically; use \`nudo harvest <pkg>\` to write a persistent env file.`);
817
+ return;
818
+ }
819
+ if (!pkg) {
820
+ console.error("Error: <pkg> is required (or use --auto).");
821
+ process.exitCode = 1;
822
+ return;
823
+ }
824
+ runHarvest(pkg, opts.out);
825
+ });
826
+ program.parseAsync(process.argv).catch((err) => {
827
+ console.error(err instanceof Error ? process.env.NUDO_DEBUG ? err.stack : err.message : err);
828
+ process.exitCode = 1;
829
+ });