@nuvio/cli 0.5.5 → 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/dist/cli-entry.js CHANGED
@@ -3,12 +3,35 @@
3
3
  // src/cli.ts
4
4
  import { resolve } from "path";
5
5
 
6
- // src/init.ts
7
- import { createInterface } from "readline";
6
+ // src/detect-pm.ts
7
+ import { existsSync } from "fs";
8
+ import { join } from "path";
9
+ function detectPackageManager(root, override) {
10
+ if (override) return override;
11
+ if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
12
+ if (existsSync(join(root, "package-lock.json"))) return "npm";
13
+ if (existsSync(join(root, "yarn.lock"))) return "yarn";
14
+ if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock")))
15
+ return "bun";
16
+ return "npm";
17
+ }
18
+ function installCommand(pm, version) {
19
+ const pkgs = `@nuvio/vite-plugin@${version} @nuvio/overlay@${version}`;
20
+ switch (pm) {
21
+ case "pnpm":
22
+ return `pnpm add -D ${pkgs}`;
23
+ case "yarn":
24
+ return `yarn add -D ${pkgs}`;
25
+ case "bun":
26
+ return `bun add -d ${pkgs}`;
27
+ default:
28
+ return `npm install -D ${pkgs}`;
29
+ }
30
+ }
8
31
 
9
32
  // src/detect-project.ts
10
- import { existsSync, readFileSync } from "fs";
11
- import { join } from "path";
33
+ import { existsSync as existsSync2, readFileSync } from "fs";
34
+ import { join as join2 } from "path";
12
35
 
13
36
  // src/messages.ts
14
37
  var MSG = {
@@ -50,8 +73,8 @@ function detectTailwind(root, pkg) {
50
73
  if (hasDep(pkg, "tailwindcss")) return true;
51
74
  const cssCandidates = ["src/index.css", "src/App.css"];
52
75
  for (const rel of cssCandidates) {
53
- const p = join(root, rel);
54
- if (!existsSync(p)) continue;
76
+ const p = join2(root, rel);
77
+ if (!existsSync2(p)) continue;
55
78
  const text = readFileSync(p, "utf8");
56
79
  if (text.includes("@tailwind") || text.includes('@import "tailwindcss"') || text.includes("@import 'tailwindcss'")) {
57
80
  return true;
@@ -60,8 +83,8 @@ function detectTailwind(root, pkg) {
60
83
  return false;
61
84
  }
62
85
  function detectProject(root) {
63
- const packageJsonPath = join(root, "package.json");
64
- if (!existsSync(packageJsonPath)) {
86
+ const packageJsonPath = join2(root, "package.json");
87
+ if (!existsSync2(packageJsonPath)) {
65
88
  throw new PreflightError(MSG.noPackageJson);
66
89
  }
67
90
  const packageJson = JSON.parse(
@@ -76,8 +99,8 @@ function detectProject(root) {
76
99
  let viteConfigPath = "";
77
100
  let viteConfigName = "";
78
101
  for (const name of VITE_CONFIGS) {
79
- const p = join(root, name);
80
- if (existsSync(p)) {
102
+ const p = join2(root, name);
103
+ if (existsSync2(p)) {
81
104
  viteConfigPath = p;
82
105
  viteConfigName = name;
83
106
  break;
@@ -104,243 +127,314 @@ function detectProject(root) {
104
127
  };
105
128
  }
106
129
 
107
- // src/detect-pm.ts
108
- import { existsSync as existsSync2 } from "fs";
109
- import { join as join2 } from "path";
110
- function detectPackageManager(root, override) {
111
- if (override) return override;
112
- if (existsSync2(join2(root, "pnpm-lock.yaml"))) return "pnpm";
113
- if (existsSync2(join2(root, "package-lock.json"))) return "npm";
114
- if (existsSync2(join2(root, "yarn.lock"))) return "yarn";
115
- if (existsSync2(join2(root, "bun.lockb")) || existsSync2(join2(root, "bun.lock")))
116
- return "bun";
117
- return "npm";
118
- }
119
- function installCommand(pm, version) {
120
- const pkgs = `@nuvio/vite-plugin@${version} @nuvio/overlay@${version}`;
121
- switch (pm) {
122
- case "pnpm":
123
- return `pnpm add -D ${pkgs}`;
124
- case "yarn":
125
- return `yarn add -D ${pkgs}`;
126
- case "bun":
127
- return `bun add -d ${pkgs}`;
128
- default:
129
- return `npm install -D ${pkgs}`;
130
- }
131
- }
132
-
133
- // src/install-packages.ts
134
- import { spawnSync } from "child_process";
130
+ // src/nuvio-deps.ts
135
131
  import { readFileSync as readFileSync2 } from "fs";
136
- function parseInstalledVersion(pkg, name) {
137
- const dev = pkg.devDependencies;
132
+ function readPackageJson(packageJsonPath) {
133
+ return JSON.parse(readFileSync2(packageJsonPath, "utf8"));
134
+ }
135
+ function getDependencyVersion(pkg, name) {
138
136
  const deps = pkg.dependencies;
139
- const raw = dev?.[name] ?? deps?.[name];
140
- if (!raw) return null;
141
- return raw.replace(/^[\^~]/, "");
137
+ const devDeps = pkg.devDependencies;
138
+ return deps?.[name] ?? devDeps?.[name];
142
139
  }
143
- function packagesNeedInstall(packageJsonPath, targetVersion) {
144
- const pkg = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
145
- for (const name of ["@nuvio/vite-plugin", "@nuvio/overlay"]) {
146
- const v = parseInstalledVersion(pkg, name);
147
- if (v !== targetVersion) return true;
148
- }
149
- return false;
140
+ function hasNuvioDependency(pkg, name) {
141
+ return Boolean(getDependencyVersion(pkg, name));
150
142
  }
151
- function runInstall(root, pm, version) {
152
- const cmd = installCommand(pm, version);
153
- const result = spawnSync(cmd, {
154
- cwd: root,
155
- shell: true,
156
- stdio: "inherit",
157
- env: process.env
158
- });
159
- if (result.status !== 0) {
160
- return {
161
- ok: false,
162
- message: `Install failed. Try manually:
163
- ${cmd}`
164
- };
165
- }
166
- return { ok: true };
143
+ function hasNuvioPackages(pkg) {
144
+ return hasNuvioDependency(pkg, "@nuvio/vite-plugin") && hasNuvioDependency(pkg, "@nuvio/overlay");
145
+ }
146
+ function isWorkspaceLinkedVersion(version) {
147
+ if (!version) return false;
148
+ return version.startsWith("workspace:") || version.startsWith("link:") || version.startsWith("file:");
149
+ }
150
+ function nuvioOverlayLinkKind(pkg) {
151
+ const raw = getDependencyVersion(pkg, "@nuvio/overlay");
152
+ if (!raw) return "missing";
153
+ return isWorkspaceLinkedVersion(raw) ? "workspace" : "npm";
167
154
  }
168
155
 
169
- // src/babel-traverse.ts
170
- import traverseImport from "@babel/traverse";
171
- var traverse = typeof traverseImport === "function" ? traverseImport : traverseImport.default;
172
- var babel_traverse_default = traverse;
156
+ // src/project-scan.ts
157
+ import { relative } from "path";
158
+ import {
159
+ buildSourceIndex,
160
+ detectProjectLibraries,
161
+ NUVIO_DEFAULT_SCAN_GLOBS
162
+ } from "@nuvio/vite-plugin/scan";
163
+ var SCAN_GLOBS = [...NUVIO_DEFAULT_SCAN_GLOBS, "app/**/*.{tsx,jsx}"];
164
+ function scanProject(root) {
165
+ const ctx = detectProject(root);
166
+ const detectedLibraries = detectProjectLibraries(root, ctx.packageJson);
167
+ const index = buildSourceIndex(root, SCAN_GLOBS, { detectedLibraries });
168
+ return { ctx, detectedLibraries, index };
169
+ }
170
+ function relPath(root, fileAbs) {
171
+ return relative(root, fileAbs).replace(/\\/g, "/");
172
+ }
173
+ function isTableHost(entry) {
174
+ return entry.hierarchyRole === "table" || entry.id.endsWith(".table") || entry.id.includes(".header.") || /\.row\./.test(entry.id);
175
+ }
176
+ function aggregateClassNameModes(entries) {
177
+ const counts = {};
178
+ for (const entry of entries) {
179
+ const mode = entry.classNameMode ?? "literal-only";
180
+ counts[mode] = (counts[mode] ?? 0) + 1;
181
+ }
182
+ return counts;
183
+ }
173
184
 
174
- // src/patch-vite-config.ts
175
- import * as t from "@babel/types";
176
- import { readFileSync as readFileSync3, writeFileSync } from "fs";
185
+ // src/telemetry.ts
186
+ import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
187
+ import { homedir } from "os";
188
+ import { join as join3 } from "path";
189
+ import { randomUUID } from "crypto";
190
+ import os from "os";
191
+ import { PostHog } from "posthog-node";
177
192
 
178
- // src/babel-generator.ts
179
- import generateImport from "@babel/generator";
180
- var generate = typeof generateImport === "function" ? generateImport : generateImport.default;
181
- var babel_generator_default = generate;
193
+ // src/nuvio-posthog-token.ts
194
+ var NUVIO_POSTHOG_TOKEN = "phc_CJnWrLU4hB4aA88DJrPnma2WBMQqVHxUMVvrsye3R6x2";
182
195
 
183
- // src/parse-ts.ts
184
- import { parse } from "@babel/parser";
185
- var PARSE_OPTS = {
186
- sourceType: "module",
187
- plugins: ["typescript", "jsx"]
188
- };
189
- function parseTs(source, filename = "file.tsx") {
190
- return parse(source, {
191
- ...PARSE_OPTS,
192
- sourceFilename: filename
193
- });
194
- }
195
- function printTs(ast, source) {
196
- const out = babel_generator_default(ast, { retainLines: true }, source);
197
- return out.code.endsWith("\n") ? out.code : `${out.code}
198
- `;
199
- }
196
+ // src/version.ts
197
+ import { createRequire } from "module";
198
+ var require2 = createRequire(import.meta.url);
199
+ var NUVIO_VERSION = require2("../package.json").version;
200
200
 
201
- // src/patch-vite-config.ts
202
- function hasNuvioImport(ast) {
203
- let found = false;
204
- babel_traverse_default(ast, {
205
- ImportDeclaration(path) {
206
- if (path.node.source.value === "@nuvio/vite-plugin") found = true;
207
- }
208
- });
209
- return found;
210
- }
211
- function hasNuvioPluginCall(ast) {
212
- let found = false;
213
- babel_traverse_default(ast, {
214
- CallExpression(path) {
215
- if (t.isIdentifier(path.node.callee, { name: "nuvio" })) found = true;
216
- }
217
- });
218
- return found;
201
+ // src/telemetry.ts
202
+ var POSTHOG_HOST = "https://us.i.posthog.com";
203
+ function telemetryFilePath() {
204
+ return join3(homedir(), ".nuvio", "telemetry.json");
219
205
  }
220
- var OVERLAY_DEP = "@nuvio/overlay";
221
- function excludeListsOverlay(expr) {
222
- if (!expr || !t.isArrayExpression(expr)) return false;
223
- return expr.elements.some(
224
- (el) => t.isStringLiteral(el) && el.value === OVERLAY_DEP
225
- );
206
+ var FORBIDDEN_PROP_KEYS = /* @__PURE__ */ new Set([
207
+ "cwd",
208
+ "root",
209
+ "file",
210
+ "path",
211
+ "name",
212
+ "message",
213
+ "stack"
214
+ ]);
215
+ var SHUTDOWN_TIMEOUT_MS = 3e3;
216
+ var client = null;
217
+ var sessionAnonymousId = null;
218
+ var shutdownDone = false;
219
+ var signalHandlersRegistered = false;
220
+ function telemetryDebug(message, detail) {
221
+ if (process.env.NUVIO_TELEMETRY_DEBUG !== "1") return;
222
+ if (detail !== void 0) {
223
+ console.error(`[nuvio telemetry] ${message}`, detail);
224
+ return;
225
+ }
226
+ console.error(`[nuvio telemetry] ${message}`);
226
227
  }
227
- function ensureOptimizeDepsExclude(ast) {
228
- let patched = false;
229
- babel_traverse_default(ast, {
230
- CallExpression(path) {
231
- const callee = path.node.callee;
232
- if (!t.isIdentifier(callee, { name: "defineConfig" })) return;
233
- const arg = path.node.arguments[0];
234
- if (!t.isObjectExpression(arg)) return;
235
- let optimizeDeps;
236
- for (const prop of arg.properties) {
237
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key, { name: "optimizeDeps" })) {
238
- optimizeDeps = prop;
239
- break;
240
- }
241
- }
242
- if (!optimizeDeps) {
243
- arg.properties.push(
244
- t.objectProperty(
245
- t.identifier("optimizeDeps"),
246
- t.objectExpression([
247
- t.objectProperty(
248
- t.identifier("exclude"),
249
- t.arrayExpression([t.stringLiteral(OVERLAY_DEP)])
250
- )
251
- ])
252
- )
253
- );
254
- patched = true;
255
- return;
256
- }
257
- if (!t.isObjectExpression(optimizeDeps.value)) return;
258
- let excludeProp;
259
- for (const p of optimizeDeps.value.properties) {
260
- if (t.isObjectProperty(p) && t.isIdentifier(p.key, { name: "exclude" })) {
261
- excludeProp = p;
262
- break;
263
- }
264
- }
265
- if (!excludeProp) {
266
- optimizeDeps.value.properties.push(
267
- t.objectProperty(
268
- t.identifier("exclude"),
269
- t.arrayExpression([t.stringLiteral(OVERLAY_DEP)])
270
- )
271
- );
272
- patched = true;
273
- return;
274
- }
275
- if (t.isArrayExpression(excludeProp.value) && !excludeListsOverlay(excludeProp.value)) {
276
- excludeProp.value.elements.push(t.stringLiteral(OVERLAY_DEP));
277
- patched = true;
278
- }
279
- }
280
- });
281
- return patched;
228
+ function isTelemetryEnabled() {
229
+ const flag = process.env.NUVIO_TELEMETRY;
230
+ if (flag === "0") return false;
231
+ if (flag?.toLowerCase() === "false") return false;
232
+ return true;
282
233
  }
283
- function viteConfigHasOverlayOptimizeExclude(filePath) {
284
- const source = readFileSync3(filePath, "utf8");
285
- return /optimizeDeps\s*:\s*\{[^}]*exclude\s*:\s*\[[^\]]*@nuvio\/overlay/.test(
286
- source
287
- ) || /exclude\s*:\s*\[[^\]]*["']@nuvio\/overlay["']/.test(source);
234
+ function posthogToken() {
235
+ return process.env.NUVIO_POSTHOG_TOKEN ?? NUVIO_POSTHOG_TOKEN;
288
236
  }
289
- function appendNuvioPlugin(ast) {
290
- let patched = false;
291
- babel_traverse_default(ast, {
292
- ObjectProperty(path) {
293
- if (!t.isIdentifier(path.node.key, { name: "plugins" })) return;
294
- if (!t.isArrayExpression(path.node.value)) return;
295
- path.node.value.elements.push(t.callExpression(t.identifier("nuvio"), []));
296
- patched = true;
297
- }
298
- });
299
- return patched;
237
+ function tokenIsConfigured(token) {
238
+ return Boolean(token && token.startsWith("phc_"));
300
239
  }
301
- function patchViteConfigFile(filePath) {
302
- const source = readFileSync3(filePath, "utf8");
303
- let ast;
240
+ function readOrCreateAnonymousId() {
241
+ if (sessionAnonymousId) return sessionAnonymousId;
304
242
  try {
305
- ast = parseTs(source, filePath);
243
+ const raw = readFileSync3(telemetryFilePath(), "utf8");
244
+ const parsed = JSON.parse(raw);
245
+ if (parsed.anonymousId) {
246
+ sessionAnonymousId = parsed.anonymousId;
247
+ return parsed.anonymousId;
248
+ }
306
249
  } catch {
307
- return { ok: false, error: "parse failed" };
308
- }
309
- const depsPatched = ensureOptimizeDepsExclude(ast);
310
- const alreadyPlugin = hasNuvioImport(ast) && hasNuvioPluginCall(ast);
311
- if (alreadyPlugin && !depsPatched) {
312
- return { ok: true, skipped: true };
313
250
  }
314
- if (!hasNuvioImport(ast)) {
315
- ast.program.body.unshift(
316
- t.importDeclaration(
317
- [t.importSpecifier(t.identifier("nuvio"), t.identifier("nuvio"))],
318
- t.stringLiteral("@nuvio/vite-plugin")
319
- )
251
+ const id = randomUUID();
252
+ sessionAnonymousId = id;
253
+ try {
254
+ mkdirSync(join3(homedir(), ".nuvio"), { recursive: true, mode: 448 });
255
+ writeFileSync(
256
+ telemetryFilePath(),
257
+ JSON.stringify({ anonymousId: id }, null, 2),
258
+ { mode: 384 }
320
259
  );
260
+ } catch {
321
261
  }
322
- if (!hasNuvioPluginCall(ast)) {
323
- if (!appendNuvioPlugin(ast)) {
324
- return { ok: false, error: "no static plugins array" };
325
- }
326
- }
327
- writeFileSync(filePath, printTs(ast, source), "utf8");
328
- return { ok: true, skipped: alreadyPlugin && depsPatched };
262
+ return id;
329
263
  }
330
- function viteConfigHasNuvio(filePath) {
331
- const source = readFileSync3(filePath, "utf8");
264
+ function getClient() {
265
+ if (!isTelemetryEnabled()) return null;
266
+ const token = posthogToken();
267
+ if (!tokenIsConfigured(token)) return null;
268
+ if (!client) {
269
+ client = new PostHog(token, {
270
+ host: POSTHOG_HOST,
271
+ flushAt: 1,
272
+ flushInterval: 0
273
+ });
274
+ telemetryDebug("PostHog client initialized", {
275
+ host: POSTHOG_HOST,
276
+ tokenPrefix: `${token.slice(0, 8)}\u2026`
277
+ });
278
+ }
279
+ return client;
280
+ }
281
+ function sanitizeProps(props) {
282
+ if (!props) return void 0;
283
+ const out = {};
284
+ for (const [key, value] of Object.entries(props)) {
285
+ if (FORBIDDEN_PROP_KEYS.has(key)) continue;
286
+ if (value === void 0) continue;
287
+ if (typeof value === "string" && /[/\\]/.test(value)) continue;
288
+ out[key] = value;
289
+ }
290
+ return Object.keys(out).length > 0 ? out : void 0;
291
+ }
292
+ function resolveCliInvokedCommand(help, command) {
293
+ if (help) return "help";
294
+ if (!command) return "none";
295
+ if (command === "init") return "init";
296
+ if (command === "doctor") return "doctor";
297
+ if (command === "scan") return "scan";
298
+ if (command === "stats") return "stats";
299
+ return "unknown";
300
+ }
301
+ function buildCliInvokedProps(command, pmOverride) {
302
+ const props = {
303
+ nuvio_version: NUVIO_VERSION,
304
+ os: process.platform,
305
+ arch: os.arch(),
306
+ node: process.version,
307
+ command
308
+ };
309
+ if (pmOverride) props.package_manager = pmOverride;
310
+ return props;
311
+ }
312
+ function buildCliTelemetryProps(pm, project) {
313
+ const props = {
314
+ nuvio_version: NUVIO_VERSION,
315
+ os: process.platform,
316
+ arch: os.arch(),
317
+ node: process.version
318
+ };
319
+ if (pm) props.package_manager = pm;
320
+ if (project) {
321
+ props.has_react = true;
322
+ props.has_vite = true;
323
+ props.has_tailwind = project.tailwindOk;
324
+ }
325
+ return props;
326
+ }
327
+ function preflightErrorCode(message) {
328
+ if (message === MSG.noPackageJson) return "preflight_no_package_json";
329
+ if (message === MSG.noVite) return "preflight_no_vite";
330
+ if (message === MSG.noReact) return "preflight_no_react";
331
+ if (message === MSG.noViteDep) return "preflight_no_vite_dep";
332
+ if (message === MSG.monorepoRoot || message === MSG.cliPackage) {
333
+ return "preflight_monorepo";
334
+ }
335
+ return "preflight_unknown";
336
+ }
337
+ function captureCliInvoked(command, pmOverride) {
338
+ captureCliEvent("nuvio_cli_invoked", buildCliInvokedProps(command, pmOverride));
339
+ }
340
+ function captureCliEvent(event, props) {
332
341
  try {
333
- const ast = parseTs(source, filePath);
334
- return hasNuvioImport(ast) && hasNuvioPluginCall(ast);
335
- } catch {
336
- return /nuvio\s*\(/.test(source);
342
+ if (!isTelemetryEnabled()) {
343
+ telemetryDebug(`skipped ${event} (telemetry disabled)`);
344
+ return;
345
+ }
346
+ const ph = getClient();
347
+ if (!ph) {
348
+ telemetryDebug(`skipped ${event} (no PostHog client \u2014 check token)`);
349
+ return;
350
+ }
351
+ const distinctId = readOrCreateAnonymousId();
352
+ ph.capture({
353
+ distinctId,
354
+ event,
355
+ properties: sanitizeProps(props)
356
+ });
357
+ telemetryDebug(`captured ${event}`, { distinctId });
358
+ } catch (error) {
359
+ telemetryDebug(`capture failed for ${event}`, error);
360
+ }
361
+ }
362
+ async function flushAndShutdownClient() {
363
+ if (!client) return;
364
+ const active = client;
365
+ client = null;
366
+ await Promise.race([
367
+ (async () => {
368
+ await active.flush();
369
+ await active.shutdown();
370
+ })(),
371
+ new Promise((_, reject) => {
372
+ setTimeout(
373
+ () => reject(new Error("telemetry shutdown timed out")),
374
+ SHUTDOWN_TIMEOUT_MS
375
+ );
376
+ })
377
+ ]);
378
+ }
379
+ async function shutdownTelemetry() {
380
+ if (shutdownDone) return;
381
+ shutdownDone = true;
382
+ try {
383
+ await flushAndShutdownClient();
384
+ telemetryDebug("flush + shutdown complete");
385
+ } catch (error) {
386
+ telemetryDebug("shutdown failed", error);
337
387
  }
338
388
  }
389
+ function registerTelemetrySignalHandlers() {
390
+ if (signalHandlersRegistered) return;
391
+ signalHandlersRegistered = true;
392
+ const onSignal = (signal) => {
393
+ void (async () => {
394
+ await shutdownTelemetry();
395
+ const code2 = signal === "SIGINT" ? 130 : 143;
396
+ process.exit(code2);
397
+ })();
398
+ };
399
+ process.once("SIGINT", onSignal);
400
+ process.once("SIGTERM", onSignal);
401
+ }
402
+
403
+ // src/babel-traverse.ts
404
+ import traverseImport from "@babel/traverse";
405
+ var traverse = typeof traverseImport === "function" ? traverseImport : traverseImport.default;
406
+ var babel_traverse_default = traverse;
339
407
 
340
408
  // src/patch-app-root.ts
341
- import * as t2 from "@babel/types";
409
+ import * as t from "@babel/types";
410
+ import fg from "fast-glob";
342
411
  import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
343
- import { join as join3 } from "path";
412
+ import { join as join4 } from "path";
413
+
414
+ // src/babel-generator.ts
415
+ import generateImport from "@babel/generator";
416
+ var generate = typeof generateImport === "function" ? generateImport : generateImport.default;
417
+ var babel_generator_default = generate;
418
+
419
+ // src/parse-ts.ts
420
+ import { parse } from "@babel/parser";
421
+ var PARSE_OPTS = {
422
+ sourceType: "module",
423
+ plugins: ["typescript", "jsx"]
424
+ };
425
+ function parseTs(source, filename = "file.tsx") {
426
+ return parse(source, {
427
+ ...PARSE_OPTS,
428
+ sourceFilename: filename
429
+ });
430
+ }
431
+ function printTs(ast, source) {
432
+ const out = babel_generator_default(ast, { retainLines: true }, source);
433
+ return out.code.endsWith("\n") ? out.code : `${out.code}
434
+ `;
435
+ }
436
+
437
+ // src/patch-app-root.ts
344
438
  var APP_CANDIDATES = [
345
439
  "src/App.tsx",
346
440
  "src/App.jsx",
@@ -349,7 +443,7 @@ var APP_CANDIDATES = [
349
443
  ];
350
444
  function resolveAppFile(root) {
351
445
  for (const rel of APP_CANDIDATES) {
352
- const p = join3(root, rel);
446
+ const p = join4(root, rel);
353
447
  if (existsSync3(p)) return p;
354
448
  }
355
449
  return null;
@@ -368,19 +462,19 @@ function hasDevShell(ast) {
368
462
  babel_traverse_default(ast, {
369
463
  JSXElement(path) {
370
464
  const name = path.node.openingElement.name;
371
- if (t2.isJSXIdentifier(name) && name.name === "NuvioDevShell") found = true;
465
+ if (t.isJSXIdentifier(name) && name.name === "NuvioDevShell") found = true;
372
466
  }
373
467
  });
374
468
  return found;
375
469
  }
376
470
  function unwrapJsx(node) {
377
471
  if (!node) return null;
378
- if (t2.isJSXElement(node) || t2.isJSXFragment(node)) return node;
379
- if (t2.isParenthesizedExpression(node)) return unwrapJsx(node.expression);
472
+ if (t.isJSXElement(node) || t.isJSXFragment(node)) return node;
473
+ if (t.isParenthesizedExpression(node)) return unwrapJsx(node.expression);
380
474
  return null;
381
475
  }
382
- var devShellElement = t2.jsxElement(
383
- t2.jsxOpeningElement(t2.jsxIdentifier("NuvioDevShell"), [], true),
476
+ var devShellElement = t.jsxElement(
477
+ t.jsxOpeningElement(t.jsxIdentifier("NuvioDevShell"), [], true),
384
478
  null,
385
479
  [],
386
480
  true
@@ -391,15 +485,15 @@ function appendDevShell(ast) {
391
485
  ReturnStatement(path) {
392
486
  const jsx = unwrapJsx(path.node.argument);
393
487
  if (!jsx) return;
394
- if (t2.isJSXFragment(jsx)) {
395
- jsx.children.push(t2.jsxText("\n "));
488
+ if (t.isJSXFragment(jsx)) {
489
+ jsx.children.push(t.jsxText("\n "));
396
490
  jsx.children.push(devShellElement);
397
491
  patched = true;
398
492
  } else {
399
- path.node.argument = t2.jsxFragment(
400
- t2.jsxOpeningFragment(),
401
- t2.jsxClosingFragment(),
402
- [jsx, t2.jsxText("\n "), devShellElement]
493
+ path.node.argument = t.jsxFragment(
494
+ t.jsxOpeningFragment(),
495
+ t.jsxClosingFragment(),
496
+ [jsx, t.jsxText("\n "), devShellElement]
403
497
  );
404
498
  patched = true;
405
499
  }
@@ -420,14 +514,14 @@ function patchAppRootFile(filePath) {
420
514
  }
421
515
  if (!hasOverlayImport(ast)) {
422
516
  ast.program.body.unshift(
423
- t2.importDeclaration(
517
+ t.importDeclaration(
424
518
  [
425
- t2.importSpecifier(
426
- t2.identifier("NuvioDevShell"),
427
- t2.identifier("NuvioDevShell")
519
+ t.importSpecifier(
520
+ t.identifier("NuvioDevShell"),
521
+ t.identifier("NuvioDevShell")
428
522
  )
429
523
  ],
430
- t2.stringLiteral("@nuvio/overlay")
524
+ t.stringLiteral("@nuvio/overlay")
431
525
  )
432
526
  );
433
527
  }
@@ -447,22 +541,32 @@ function appHasDevShell(filePath) {
447
541
  return /NuvioDevShell/.test(source);
448
542
  }
449
543
  }
544
+ function projectHasDevShell(root) {
545
+ const appFile = resolveAppFile(root);
546
+ if (appFile && appHasDevShell(appFile)) return true;
547
+ const files = fg.sync(["src/**/*.{tsx,jsx}"], {
548
+ cwd: root,
549
+ absolute: true,
550
+ onlyFiles: true
551
+ });
552
+ for (const file of files) {
553
+ if (appHasDevShell(file)) return true;
554
+ }
555
+ return false;
556
+ }
450
557
 
451
558
  // src/patch-main-styles.ts
452
559
  import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
453
- import { join as join4 } from "path";
560
+ import { join as join5 } from "path";
454
561
  var MAIN_CANDIDATES = ["src/main.tsx", "src/main.jsx", "main.tsx", "main.jsx"];
455
562
  var STYLE_IMPORT = 'import "@nuvio/overlay/style.css";';
456
563
  function overlayInstalledFromNpm(packageJsonPath) {
457
- const pkg = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
458
- const dev = pkg.devDependencies;
459
- const raw = dev?.["@nuvio/overlay"];
460
- if (!raw) return false;
461
- return !raw.startsWith("workspace:") && !raw.startsWith("link:") && !raw.startsWith("file:");
564
+ const pkg = readPackageJson(packageJsonPath);
565
+ return nuvioOverlayLinkKind(pkg) === "npm";
462
566
  }
463
567
  function resolveMainEntry(root) {
464
568
  for (const rel of MAIN_CANDIDATES) {
465
- const p = join4(root, rel);
569
+ const p = join5(root, rel);
466
570
  if (existsSync4(p)) return p;
467
571
  }
468
572
  return null;
@@ -490,205 +594,208 @@ function patchMainOverlayStyles(mainPath) {
490
594
  return { ok: true };
491
595
  }
492
596
 
493
- // src/patch-starter-id.ts
494
- import * as t3 from "@babel/types";
495
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
496
-
497
- // src/scan-ids.ts
498
- import { readFileSync as readFileSync6 } from "fs";
499
- import { join as join5 } from "path";
500
- import fg from "fast-glob";
501
- var ID_GLOB = ["src/**/*.{tsx,jsx}"];
502
- function projectHasPageTitleId(root) {
503
- const files = fg.sync(ID_GLOB, { cwd: root, absolute: true });
504
- for (const file of files) {
505
- const text = readFileSync6(file, "utf8");
506
- if (/data-nuvio-id=["']page\.title["']/.test(text)) {
507
- return true;
597
+ // src/patch-vite-config.ts
598
+ import * as t2 from "@babel/types";
599
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
600
+ function hasNuvioImport(ast) {
601
+ let found = false;
602
+ babel_traverse_default(ast, {
603
+ ImportDeclaration(path) {
604
+ if (path.node.source.value === "@nuvio/vite-plugin") found = true;
508
605
  }
509
- }
510
- return false;
606
+ });
607
+ return found;
511
608
  }
512
- function findHeadingFiles(root) {
513
- const files = fg.sync(ID_GLOB, { cwd: root, absolute: true });
514
- const ordered = [
515
- join5(root, "src/App.tsx"),
516
- join5(root, "src/App.jsx"),
517
- ...files.filter(
518
- (f) => !f.endsWith("App.tsx") && !f.endsWith("App.jsx")
519
- )
520
- ];
521
- const seen = /* @__PURE__ */ new Set();
522
- const out = [];
523
- for (const f of ordered) {
524
- if (!seen.has(f) && files.includes(f)) {
525
- seen.add(f);
526
- out.push(f);
609
+ function hasNuvioPluginCall(ast) {
610
+ let found = false;
611
+ babel_traverse_default(ast, {
612
+ CallExpression(path) {
613
+ if (t2.isIdentifier(path.node.callee, { name: "nuvio" })) found = true;
527
614
  }
528
- }
529
- for (const f of files) {
530
- if (!seen.has(f)) out.push(f);
531
- }
532
- return out;
615
+ });
616
+ return found;
533
617
  }
534
-
535
- // src/patch-starter-id.ts
536
- function patchFirstHeading(filePath) {
537
- const source = readFileSync7(filePath, "utf8");
538
- let ast;
539
- try {
540
- ast = parseTs(source, filePath);
541
- } catch {
542
- return { ok: false, error: "parse failed" };
543
- }
618
+ var OVERLAY_DEP = "@nuvio/overlay";
619
+ function excludeListsOverlay(expr) {
620
+ if (!expr || !t2.isArrayExpression(expr)) return false;
621
+ return expr.elements.some(
622
+ (el) => t2.isStringLiteral(el) && el.value === OVERLAY_DEP
623
+ );
624
+ }
625
+ function ensureOptimizeDepsExclude(ast) {
544
626
  let patched = false;
545
627
  babel_traverse_default(ast, {
546
- JSXOpeningElement(path) {
547
- if (patched) return;
548
- const name = path.node.name;
549
- if (!t3.isJSXIdentifier(name)) return;
550
- if (name.name !== "h1" && name.name !== "h2") return;
551
- for (const attr of path.node.attributes) {
552
- if (t3.isJSXAttribute(attr) && t3.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
553
- return;
554
- }
628
+ CallExpression(path) {
629
+ const callee = path.node.callee;
630
+ if (!t2.isIdentifier(callee, { name: "defineConfig" })) return;
631
+ const arg = path.node.arguments[0];
632
+ if (!t2.isObjectExpression(arg)) return;
633
+ let optimizeDeps;
634
+ for (const prop of arg.properties) {
635
+ if (t2.isObjectProperty(prop) && t2.isIdentifier(prop.key, { name: "optimizeDeps" })) {
636
+ optimizeDeps = prop;
637
+ break;
638
+ }
555
639
  }
556
- path.node.attributes.push(
557
- t3.jsxAttribute(
558
- t3.jsxIdentifier("data-nuvio-id"),
559
- t3.stringLiteral("page.title")
560
- )
561
- );
640
+ if (!optimizeDeps) {
641
+ arg.properties.push(
642
+ t2.objectProperty(
643
+ t2.identifier("optimizeDeps"),
644
+ t2.objectExpression([
645
+ t2.objectProperty(
646
+ t2.identifier("exclude"),
647
+ t2.arrayExpression([t2.stringLiteral(OVERLAY_DEP)])
648
+ )
649
+ ])
650
+ )
651
+ );
652
+ patched = true;
653
+ return;
654
+ }
655
+ if (!t2.isObjectExpression(optimizeDeps.value)) return;
656
+ let excludeProp;
657
+ for (const p of optimizeDeps.value.properties) {
658
+ if (t2.isObjectProperty(p) && t2.isIdentifier(p.key, { name: "exclude" })) {
659
+ excludeProp = p;
660
+ break;
661
+ }
662
+ }
663
+ if (!excludeProp) {
664
+ optimizeDeps.value.properties.push(
665
+ t2.objectProperty(
666
+ t2.identifier("exclude"),
667
+ t2.arrayExpression([t2.stringLiteral(OVERLAY_DEP)])
668
+ )
669
+ );
670
+ patched = true;
671
+ return;
672
+ }
673
+ if (t2.isArrayExpression(excludeProp.value) && !excludeListsOverlay(excludeProp.value)) {
674
+ excludeProp.value.elements.push(t2.stringLiteral(OVERLAY_DEP));
675
+ patched = true;
676
+ }
677
+ }
678
+ });
679
+ return patched;
680
+ }
681
+ function viteConfigHasOverlayOptimizeExclude(filePath) {
682
+ const source = readFileSync6(filePath, "utf8");
683
+ return /optimizeDeps\s*:\s*\{[^}]*exclude\s*:\s*\[[^\]]*@nuvio\/overlay/.test(
684
+ source
685
+ ) || /exclude\s*:\s*\[[^\]]*["']@nuvio\/overlay["']/.test(source);
686
+ }
687
+ function appendNuvioPlugin(ast) {
688
+ let patched = false;
689
+ babel_traverse_default(ast, {
690
+ ObjectProperty(path) {
691
+ if (!t2.isIdentifier(path.node.key, { name: "plugins" })) return;
692
+ if (!t2.isArrayExpression(path.node.value)) return;
693
+ path.node.value.elements.push(t2.callExpression(t2.identifier("nuvio"), []));
562
694
  patched = true;
563
695
  }
564
696
  });
565
- if (!patched) return { ok: false, error: "no h1/h2" };
566
- writeFileSync4(filePath, printTs(ast, source), "utf8");
567
- return { ok: true };
697
+ return patched;
568
698
  }
569
- function patchStarterId(root) {
570
- const files = findHeadingFiles(root);
571
- for (const file of files) {
572
- const source = readFileSync7(file, "utf8");
573
- if (!/<h[12][\s>]/.test(source) && !/<>[\s\S]*<h[12]/.test(source)) {
574
- try {
575
- const ast = parseTs(source, file);
576
- let has = false;
577
- babel_traverse_default(ast, {
578
- JSXOpeningElement(path) {
579
- const name = path.node.name;
580
- if (t3.isJSXIdentifier(name) && (name.name === "h1" || name.name === "h2"))
581
- has = true;
582
- }
583
- });
584
- if (!has) continue;
585
- } catch {
586
- continue;
587
- }
699
+ function patchViteConfigFile(filePath) {
700
+ const source = readFileSync6(filePath, "utf8");
701
+ let ast;
702
+ try {
703
+ ast = parseTs(source, filePath);
704
+ } catch {
705
+ return { ok: false, error: "parse failed" };
706
+ }
707
+ const depsPatched = ensureOptimizeDepsExclude(ast);
708
+ const alreadyPlugin = hasNuvioImport(ast) && hasNuvioPluginCall(ast);
709
+ if (alreadyPlugin && !depsPatched) {
710
+ return { ok: true, skipped: true };
711
+ }
712
+ if (!hasNuvioImport(ast)) {
713
+ ast.program.body.unshift(
714
+ t2.importDeclaration(
715
+ [t2.importSpecifier(t2.identifier("nuvio"), t2.identifier("nuvio"))],
716
+ t2.stringLiteral("@nuvio/vite-plugin")
717
+ )
718
+ );
719
+ }
720
+ if (!hasNuvioPluginCall(ast)) {
721
+ if (!appendNuvioPlugin(ast)) {
722
+ return { ok: false, error: "no static plugins array" };
588
723
  }
589
- const outcome = patchFirstHeading(file);
590
- if (outcome.ok) return { outcome, file };
591
724
  }
592
- return { outcome: { ok: false, error: "no heading" } };
725
+ writeFileSync4(filePath, printTs(ast, source), "utf8");
726
+ return { ok: true, skipped: alreadyPlugin && depsPatched };
593
727
  }
594
-
595
- // src/plan.ts
596
- function createPlan(root, pm) {
597
- const pmRun = pm === "pnpm" ? "pnpm dev" : pm === "yarn" ? "yarn dev" : pm === "bun" ? "bun run dev" : "npm run dev";
598
- return {
599
- root,
600
- pm,
601
- pmRun,
602
- installCommand: "",
603
- modify: [],
604
- create: [],
605
- warnings: [],
606
- tier: "full",
607
- failedSteps: []
608
- };
728
+ function viteConfigHasNuvio(filePath) {
729
+ const source = readFileSync6(filePath, "utf8");
730
+ try {
731
+ const ast = parseTs(source, filePath);
732
+ return hasNuvioImport(ast) && hasNuvioPluginCall(ast);
733
+ } catch {
734
+ return /nuvio\s*\(/.test(source);
735
+ }
609
736
  }
610
737
 
611
- // src/version.ts
612
- import { createRequire } from "module";
613
- var require2 = createRequire(import.meta.url);
614
- var NUVIO_VERSION = require2("../package.json").version;
615
-
616
- // src/write-nuvio-folder.ts
617
- import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
618
- import { dirname, join as join6 } from "path";
619
- import { fileURLToPath } from "url";
620
- var CLI_ROOT = join6(dirname(fileURLToPath(import.meta.url)), "..");
621
- function loadTemplate(name) {
622
- return readFileSync8(join6(CLI_ROOT, "templates", name), "utf8");
623
- }
624
- function render(tpl, vars) {
625
- let out = tpl;
626
- for (const [key, value] of Object.entries(vars)) {
627
- out = out.replaceAll(`{{${key}}}`, value);
738
+ // src/scan-ids.ts
739
+ import { readFileSync as readFileSync7 } from "fs";
740
+ import { join as join6 } from "path";
741
+ import fg2 from "fast-glob";
742
+ var ID_GLOB = ["src/**/*.{tsx,jsx}"];
743
+ function projectHasPageTitleId(root) {
744
+ const files = fg2.sync(ID_GLOB, { cwd: root, absolute: true });
745
+ for (const file of files) {
746
+ const text = readFileSync7(file, "utf8");
747
+ if (/data-nuvio-id=["']page\.title["']/.test(text)) {
748
+ return true;
749
+ }
628
750
  }
629
- return out;
751
+ return false;
630
752
  }
631
- function writeNuvioFolder(opts) {
632
- const dir = join6(opts.root, "nuvio");
633
- const created = [];
634
- mkdirSync(dir, { recursive: true });
635
- const vars = {
636
- NUVIO_VERSION: opts.version,
637
- PM_RUN: opts.pmRun,
638
- FAILED_STEPS: opts.failedSteps.join(", ") || "(none)"
639
- };
640
- const startHere = join6(dir, "START_HERE.md");
641
- writeFileSync5(
642
- startHere,
643
- render(loadTemplate("START_HERE.md.tpl"), vars),
644
- "utf8"
645
- );
646
- created.push("nuvio/START_HERE.md");
647
- const readme = join6(dir, "README.md");
648
- writeFileSync5(
649
- readme,
650
- render(loadTemplate("README.pointer.md.tpl"), vars),
651
- "utf8"
652
- );
653
- created.push("nuvio/README.md");
654
- const agent = join6(dir, "AGENT.md");
655
- if (!existsSync5(agent) || opts.forceAgent) {
656
- writeFileSync5(agent, render(loadTemplate("AGENT.md.tpl"), vars), "utf8");
657
- created.push("nuvio/AGENT.md");
753
+ function findHeadingFiles(root) {
754
+ const files = fg2.sync(ID_GLOB, { cwd: root, absolute: true });
755
+ const ordered = [
756
+ join6(root, "src/App.tsx"),
757
+ join6(root, "src/App.jsx"),
758
+ ...files.filter(
759
+ (f) => !f.endsWith("App.tsx") && !f.endsWith("App.jsx")
760
+ )
761
+ ];
762
+ const seen = /* @__PURE__ */ new Set();
763
+ const out = [];
764
+ for (const f of ordered) {
765
+ if (!seen.has(f) && files.includes(f)) {
766
+ seen.add(f);
767
+ out.push(f);
768
+ }
658
769
  }
659
- if (opts.failedSteps.length > 0) {
660
- const todo = join6(dir, "SETUP_TODO.md");
661
- writeFileSync5(
662
- todo,
663
- render(loadTemplate("SETUP_TODO.md.tpl"), vars),
664
- "utf8"
665
- );
666
- created.push("nuvio/SETUP_TODO.md");
770
+ for (const f of files) {
771
+ if (!seen.has(f)) out.push(f);
667
772
  }
668
- return created;
773
+ return out;
669
774
  }
670
775
 
671
776
  // src/verify.ts
672
- import { readFileSync as readFileSync9 } from "fs";
777
+ function optimizeDepsSatisfied(viteConfigPath, packageJsonPath) {
778
+ if (viteConfigHasOverlayOptimizeExclude(viteConfigPath)) return true;
779
+ const pkg = readPackageJson(packageJsonPath);
780
+ return nuvioOverlayLinkKind(pkg) === "workspace";
781
+ }
673
782
  function verifyProject(root, packageJsonPath, viteConfigPath) {
674
- const pkg = JSON.parse(readFileSync9(packageJsonPath, "utf8"));
675
- const dev = pkg.devDependencies;
676
- const depsOk = Boolean(dev?.["@nuvio/vite-plugin"]) && Boolean(dev?.["@nuvio/overlay"]);
677
- const appFile = resolveAppFile(root);
783
+ const pkg = readPackageJson(packageJsonPath);
784
+ const depsOk = hasNuvioPackages(pkg);
678
785
  const mainEntry = resolveMainEntry(root);
679
786
  return {
680
787
  deps: depsOk ? "OK" : "MISSING",
681
788
  vite: viteConfigHasNuvio(viteConfigPath) ? "OK" : "TODO",
682
789
  overlayCss: mainEntry && (mainHasOverlayStyles(mainEntry) || !overlayInstalledFromNpm(packageJsonPath)) ? "OK" : "TODO",
683
- optimizeDeps: viteConfigHasOverlayOptimizeExclude(viteConfigPath) ? "OK" : "TODO",
684
- shell: appFile && appHasDevShell(appFile) ? "OK" : "TODO",
790
+ optimizeDeps: optimizeDepsSatisfied(viteConfigPath, packageJsonPath) ? "OK" : "TODO",
791
+ shell: projectHasDevShell(root) ? "OK" : "TODO",
685
792
  starterId: projectHasPageTitleId(root) ? "OK" : "MISSING"
686
793
  };
687
794
  }
688
795
  function printVerification(v) {
689
796
  console.log("Verification:");
690
797
  console.log(
691
- ` devDependencies: @nuvio/vite-plugin, @nuvio/overlay \u2014 ${v.deps}`
798
+ ` dependencies: @nuvio/vite-plugin, @nuvio/overlay \u2014 ${v.deps}`
692
799
  );
693
800
  console.log(` vite.config: nuvio() \u2014 ${v.vite}`);
694
801
  console.log(` main.tsx: @nuvio/overlay/style.css \u2014 ${v.overlayCss}`);
@@ -697,214 +804,334 @@ function printVerification(v) {
697
804
  console.log(` Starter id page.title \u2014 ${v.starterId}`);
698
805
  }
699
806
 
700
- // src/telemetry.ts
701
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
702
- import { homedir } from "os";
703
- import { join as join7 } from "path";
704
- import { randomUUID } from "crypto";
705
- import os from "os";
706
- import { PostHog } from "posthog-node";
707
-
708
- // src/nuvio-posthog-token.ts
709
- var NUVIO_POSTHOG_TOKEN = "phc_CJnWrLU4hB4aA88DJrPnma2WBMQqVHxUMVvrsye3R6x2";
710
-
711
- // src/telemetry.ts
712
- var POSTHOG_HOST = "https://us.i.posthog.com";
713
- function telemetryFilePath() {
714
- return join7(homedir(), ".nuvio", "telemetry.json");
715
- }
716
- var FORBIDDEN_PROP_KEYS = /* @__PURE__ */ new Set([
717
- "cwd",
718
- "root",
719
- "file",
720
- "path",
721
- "name",
722
- "message",
723
- "stack"
724
- ]);
725
- var SHUTDOWN_TIMEOUT_MS = 3e3;
726
- var client = null;
727
- var sessionAnonymousId = null;
728
- var shutdownDone = false;
729
- var signalHandlersRegistered = false;
730
- function telemetryDebug(message, detail) {
731
- if (process.env.NUVIO_TELEMETRY_DEBUG !== "1") return;
732
- if (detail !== void 0) {
733
- console.error(`[nuvio telemetry] ${message}`, detail);
734
- return;
807
+ // src/doctor.ts
808
+ async function checkDevServerReachable(port) {
809
+ const url = `http://127.0.0.1:${port}/`;
810
+ try {
811
+ const res = await fetch(url, { signal: AbortSignal.timeout(1500) });
812
+ if (res.ok) {
813
+ return {
814
+ id: "dev_server",
815
+ label: `Dev server reachable (${url})`,
816
+ status: "pass"
817
+ };
818
+ }
819
+ return {
820
+ id: "dev_server",
821
+ label: "Dev server reachable",
822
+ status: "warn",
823
+ detail: `HTTP ${res.status} from ${url}`
824
+ };
825
+ } catch {
826
+ return {
827
+ id: "dev_server",
828
+ label: "Dev server reachable",
829
+ status: "warn",
830
+ detail: `Start pnpm dev \u2014 could not reach ${url}`
831
+ };
735
832
  }
736
- console.error(`[nuvio telemetry] ${message}`);
737
- }
738
- function isTelemetryEnabled() {
739
- const flag = process.env.NUVIO_TELEMETRY;
740
- if (flag === "0") return false;
741
- if (flag?.toLowerCase() === "false") return false;
742
- return true;
743
833
  }
744
- function posthogToken() {
745
- return process.env.NUVIO_POSTHOG_TOKEN ?? NUVIO_POSTHOG_TOKEN;
746
- }
747
- function tokenIsConfigured(token) {
748
- return Boolean(token && token.startsWith("phc_"));
834
+ function summarize(result) {
835
+ const total = result.checks.length;
836
+ const passed = result.passCount;
837
+ const label = result.failCount > 0 ? "nuvio not ready" : result.warnCount > 0 ? "nuvio partially ready" : "nuvio ready";
838
+ console.log(`
839
+ Result: ${passed}/${total} passed \u2014 ${label}`);
749
840
  }
750
- function readOrCreateAnonymousId() {
751
- if (sessionAnonymousId) return sessionAnonymousId;
841
+ async function runDoctor(opts) {
842
+ let scan;
752
843
  try {
753
- const raw = readFileSync10(telemetryFilePath(), "utf8");
754
- const parsed = JSON.parse(raw);
755
- if (parsed.anonymousId) {
756
- sessionAnonymousId = parsed.anonymousId;
757
- return parsed.anonymousId;
844
+ scan = scanProject(opts.cwd);
845
+ } catch (e) {
846
+ if (e instanceof PreflightError) {
847
+ console.error(e.message);
848
+ return 1;
758
849
  }
759
- } catch {
850
+ throw e;
760
851
  }
761
- const id = randomUUID();
762
- sessionAnonymousId = id;
763
- try {
764
- mkdirSync2(join7(homedir(), ".nuvio"), { recursive: true, mode: 448 });
765
- writeFileSync6(
766
- telemetryFilePath(),
767
- JSON.stringify({ anonymousId: id }, null, 2),
768
- { mode: 384 }
769
- );
770
- } catch {
852
+ const { ctx, detectedLibraries, index } = scan;
853
+ const pkg = ctx.packageJson;
854
+ const projectName = String(pkg.name ?? "project");
855
+ const verification = verifyProject(
856
+ ctx.root,
857
+ ctx.packageJsonPath,
858
+ ctx.viteConfigPath
859
+ );
860
+ const checks = [
861
+ {
862
+ id: "deps_plugin",
863
+ label: "@nuvio/vite-plugin installed",
864
+ status: verification.deps === "OK" ? "pass" : "fail"
865
+ },
866
+ {
867
+ id: "deps_overlay",
868
+ label: "@nuvio/overlay installed",
869
+ status: verification.deps === "OK" ? "pass" : "fail"
870
+ },
871
+ {
872
+ id: "vite_plugin",
873
+ label: "vite.config contains nuvio()",
874
+ status: verification.vite === "OK" ? "pass" : "fail"
875
+ },
876
+ {
877
+ id: "optimize_deps",
878
+ label: "optimizeDeps.exclude includes @nuvio/overlay",
879
+ status: verification.optimizeDeps === "OK" ? "pass" : "fail",
880
+ detail: verification.optimizeDeps === "OK" && nuvioOverlayLinkKind(pkg) === "workspace" ? "workspace install \u2014 optional" : void 0
881
+ },
882
+ {
883
+ id: "overlay_css",
884
+ label: "main entry imports @nuvio/overlay/style.css",
885
+ status: verification.overlayCss === "OK" ? "pass" : "fail",
886
+ detail: verification.overlayCss === "OK" && nuvioOverlayLinkKind(pkg) === "workspace" ? "workspace install \u2014 optional" : void 0
887
+ },
888
+ {
889
+ id: "dev_shell",
890
+ label: "NuvioDevShell mounted in app",
891
+ status: verification.shell === "OK" ? "pass" : "fail"
892
+ },
893
+ {
894
+ id: "tailwind",
895
+ label: "Tailwind detected",
896
+ status: ctx.tailwindOk ? "pass" : "warn",
897
+ detail: ctx.tailwindOk ? void 0 : "Style edits may not apply visually without Tailwind"
898
+ },
899
+ {
900
+ id: "editable_hosts",
901
+ label: "At least one data-nuvio-id indexed",
902
+ status: index.entries.length > 0 ? "pass" : "fail",
903
+ detail: index.entries.length > 0 ? `${index.entries.length} host(s)` : "Run dev \u2192 Make Editable, or add ids manually"
904
+ }
905
+ ];
906
+ if (detectedLibraries.length > 0) {
907
+ checks.push({
908
+ id: "libraries",
909
+ label: "Component libraries detected",
910
+ status: "pass",
911
+ detail: detectedLibraries.join(", ")
912
+ });
771
913
  }
772
- return id;
773
- }
774
- function getClient() {
775
- if (!isTelemetryEnabled()) return null;
776
- const token = posthogToken();
777
- if (!tokenIsConfigured(token)) return null;
778
- if (!client) {
779
- client = new PostHog(token, {
780
- host: POSTHOG_HOST,
781
- flushAt: 1,
782
- flushInterval: 0
914
+ if (index.duplicateErrors.length > 0) {
915
+ checks.push({
916
+ id: "duplicate_ids",
917
+ label: "No duplicate data-nuvio-id values",
918
+ status: "fail",
919
+ detail: `${index.duplicateErrors.length} duplicate id(s) \u2014 run nuvio scan`
783
920
  });
784
- telemetryDebug("PostHog client initialized", {
785
- host: POSTHOG_HOST,
786
- tokenPrefix: `${token.slice(0, 8)}\u2026`
921
+ } else {
922
+ checks.push({
923
+ id: "duplicate_ids",
924
+ label: "No duplicate data-nuvio-id values",
925
+ status: "pass"
787
926
  });
788
927
  }
789
- return client;
790
- }
791
- function sanitizeProps(props) {
792
- if (!props) return void 0;
793
- const out = {};
794
- for (const [key, value] of Object.entries(props)) {
795
- if (FORBIDDEN_PROP_KEYS.has(key)) continue;
796
- if (value === void 0) continue;
797
- if (typeof value === "string" && /[/\\]/.test(value)) continue;
798
- out[key] = value;
928
+ if (opts.checkDevServer !== false) {
929
+ checks.push(await checkDevServerReachable(opts.devServerPort ?? 5173));
799
930
  }
800
- return Object.keys(out).length > 0 ? out : void 0;
801
- }
802
- function resolveCliInvokedCommand(help, command) {
803
- if (help) return "help";
804
- if (!command) return "none";
805
- if (command === "init") return "init";
806
- return "unknown";
807
- }
808
- function buildCliInvokedProps(command, pmOverride) {
809
- const props = {
810
- nuvio_version: NUVIO_VERSION,
811
- os: process.platform,
812
- arch: os.arch(),
813
- node: process.version,
814
- command
931
+ const passCount = checks.filter((c) => c.status === "pass").length;
932
+ const warnCount = checks.filter((c) => c.status === "warn").length;
933
+ const failCount = checks.filter((c) => c.status === "fail").length;
934
+ const result = {
935
+ projectName,
936
+ checks,
937
+ passCount,
938
+ warnCount,
939
+ failCount
815
940
  };
816
- if (pmOverride) props.package_manager = pmOverride;
817
- return props;
818
- }
819
- function buildCliTelemetryProps(pm, project) {
820
- const props = {
821
- nuvio_version: NUVIO_VERSION,
822
- os: process.platform,
823
- arch: os.arch(),
824
- node: process.version
941
+ const pm = detectPackageManager(ctx.root);
942
+ const telemetry = {
943
+ ...buildCliTelemetryProps(pm, ctx),
944
+ pass_count: passCount,
945
+ warn_count: warnCount,
946
+ fail_count: failCount,
947
+ ready: failCount === 0
825
948
  };
826
- if (pm) props.package_manager = pm;
827
- if (project) {
828
- props.has_react = true;
829
- props.has_vite = true;
830
- props.has_tailwind = project.tailwindOk;
949
+ captureCliEvent("doctor_run", telemetry);
950
+ if (opts.json) {
951
+ console.log(JSON.stringify(result, null, 2));
952
+ return failCount > 0 ? 1 : 0;
831
953
  }
832
- return props;
954
+ console.log(`nuvio doctor \u2014 ${projectName}
955
+ `);
956
+ for (const check of checks) {
957
+ const icon = check.status === "pass" ? "\u2705" : check.status === "warn" ? "\u26A0" : "\u274C";
958
+ const suffix = check.detail ? ` \u2014 ${check.detail}` : "";
959
+ console.log(` ${icon} ${check.label}${suffix}`);
960
+ }
961
+ summarize(result);
962
+ return failCount > 0 ? 1 : 0;
833
963
  }
834
- function preflightErrorCode(message) {
835
- if (message === MSG.noPackageJson) return "preflight_no_package_json";
836
- if (message === MSG.noVite) return "preflight_no_vite";
837
- if (message === MSG.noReact) return "preflight_no_react";
838
- if (message === MSG.noViteDep) return "preflight_no_vite_dep";
839
- if (message === MSG.monorepoRoot || message === MSG.cliPackage) {
840
- return "preflight_monorepo";
964
+
965
+ // src/init.ts
966
+ import { createInterface } from "readline";
967
+
968
+ // src/install-packages.ts
969
+ import { spawnSync } from "child_process";
970
+ import { readFileSync as readFileSync8 } from "fs";
971
+ function parseInstalledVersion(pkg, name) {
972
+ const dev = pkg.devDependencies;
973
+ const deps = pkg.dependencies;
974
+ const raw = dev?.[name] ?? deps?.[name];
975
+ if (!raw) return null;
976
+ return raw.replace(/^[\^~]/, "");
977
+ }
978
+ function packagesNeedInstall(packageJsonPath, targetVersion) {
979
+ const pkg = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
980
+ for (const name of ["@nuvio/vite-plugin", "@nuvio/overlay"]) {
981
+ const v = parseInstalledVersion(pkg, name);
982
+ if (v !== targetVersion) return true;
841
983
  }
842
- return "preflight_unknown";
984
+ return false;
843
985
  }
844
- function captureCliInvoked(command, pmOverride) {
845
- captureCliEvent("nuvio_cli_invoked", buildCliInvokedProps(command, pmOverride));
986
+ function runInstall(root, pm, version) {
987
+ const cmd = installCommand(pm, version);
988
+ const result = spawnSync(cmd, {
989
+ cwd: root,
990
+ shell: true,
991
+ stdio: "inherit",
992
+ env: process.env
993
+ });
994
+ if (result.status !== 0) {
995
+ return {
996
+ ok: false,
997
+ message: `Install failed. Try manually:
998
+ ${cmd}`
999
+ };
1000
+ }
1001
+ return { ok: true };
846
1002
  }
847
- function captureCliEvent(event, props) {
1003
+
1004
+ // src/patch-starter-id.ts
1005
+ import * as t3 from "@babel/types";
1006
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
1007
+ function patchFirstHeading(filePath) {
1008
+ const source = readFileSync9(filePath, "utf8");
1009
+ let ast;
848
1010
  try {
849
- if (!isTelemetryEnabled()) {
850
- telemetryDebug(`skipped ${event} (telemetry disabled)`);
851
- return;
1011
+ ast = parseTs(source, filePath);
1012
+ } catch {
1013
+ return { ok: false, error: "parse failed" };
1014
+ }
1015
+ let patched = false;
1016
+ babel_traverse_default(ast, {
1017
+ JSXOpeningElement(path) {
1018
+ if (patched) return;
1019
+ const name = path.node.name;
1020
+ if (!t3.isJSXIdentifier(name)) return;
1021
+ if (name.name !== "h1" && name.name !== "h2") return;
1022
+ for (const attr of path.node.attributes) {
1023
+ if (t3.isJSXAttribute(attr) && t3.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
1024
+ return;
1025
+ }
1026
+ }
1027
+ path.node.attributes.push(
1028
+ t3.jsxAttribute(
1029
+ t3.jsxIdentifier("data-nuvio-id"),
1030
+ t3.stringLiteral("page.title")
1031
+ )
1032
+ );
1033
+ patched = true;
852
1034
  }
853
- const ph = getClient();
854
- if (!ph) {
855
- telemetryDebug(`skipped ${event} (no PostHog client \u2014 check token)`);
856
- return;
1035
+ });
1036
+ if (!patched) return { ok: false, error: "no h1/h2" };
1037
+ writeFileSync5(filePath, printTs(ast, source), "utf8");
1038
+ return { ok: true };
1039
+ }
1040
+ function patchStarterId(root) {
1041
+ const files = findHeadingFiles(root);
1042
+ for (const file of files) {
1043
+ const source = readFileSync9(file, "utf8");
1044
+ if (!/<h[12][\s>]/.test(source) && !/<>[\s\S]*<h[12]/.test(source)) {
1045
+ try {
1046
+ const ast = parseTs(source, file);
1047
+ let has = false;
1048
+ babel_traverse_default(ast, {
1049
+ JSXOpeningElement(path) {
1050
+ const name = path.node.name;
1051
+ if (t3.isJSXIdentifier(name) && (name.name === "h1" || name.name === "h2"))
1052
+ has = true;
1053
+ }
1054
+ });
1055
+ if (!has) continue;
1056
+ } catch {
1057
+ continue;
1058
+ }
857
1059
  }
858
- const distinctId = readOrCreateAnonymousId();
859
- ph.capture({
860
- distinctId,
861
- event,
862
- properties: sanitizeProps(props)
863
- });
864
- telemetryDebug(`captured ${event}`, { distinctId });
865
- } catch (error) {
866
- telemetryDebug(`capture failed for ${event}`, error);
1060
+ const outcome = patchFirstHeading(file);
1061
+ if (outcome.ok) return { outcome, file };
867
1062
  }
1063
+ return { outcome: { ok: false, error: "no heading" } };
868
1064
  }
869
- async function flushAndShutdownClient() {
870
- if (!client) return;
871
- const active = client;
872
- client = null;
873
- await Promise.race([
874
- (async () => {
875
- await active.flush();
876
- await active.shutdown();
877
- })(),
878
- new Promise((_, reject) => {
879
- setTimeout(
880
- () => reject(new Error("telemetry shutdown timed out")),
881
- SHUTDOWN_TIMEOUT_MS
882
- );
883
- })
884
- ]);
1065
+
1066
+ // src/plan.ts
1067
+ function createPlan(root, pm) {
1068
+ const pmRun = pm === "pnpm" ? "pnpm dev" : pm === "yarn" ? "yarn dev" : pm === "bun" ? "bun run dev" : "npm run dev";
1069
+ return {
1070
+ root,
1071
+ pm,
1072
+ pmRun,
1073
+ installCommand: "",
1074
+ modify: [],
1075
+ create: [],
1076
+ warnings: [],
1077
+ tier: "full",
1078
+ failedSteps: []
1079
+ };
885
1080
  }
886
- async function shutdownTelemetry() {
887
- if (shutdownDone) return;
888
- shutdownDone = true;
889
- try {
890
- await flushAndShutdownClient();
891
- telemetryDebug("flush + shutdown complete");
892
- } catch (error) {
893
- telemetryDebug("shutdown failed", error);
1081
+
1082
+ // src/write-nuvio-folder.ts
1083
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
1084
+ import { dirname, join as join7 } from "path";
1085
+ import { fileURLToPath } from "url";
1086
+ var CLI_ROOT = join7(dirname(fileURLToPath(import.meta.url)), "..");
1087
+ function loadTemplate(name) {
1088
+ return readFileSync10(join7(CLI_ROOT, "templates", name), "utf8");
1089
+ }
1090
+ function render(tpl, vars) {
1091
+ let out = tpl;
1092
+ for (const [key, value] of Object.entries(vars)) {
1093
+ out = out.replaceAll(`{{${key}}}`, value);
894
1094
  }
1095
+ return out;
895
1096
  }
896
- function registerTelemetrySignalHandlers() {
897
- if (signalHandlersRegistered) return;
898
- signalHandlersRegistered = true;
899
- const onSignal = (signal) => {
900
- void (async () => {
901
- await shutdownTelemetry();
902
- const code2 = signal === "SIGINT" ? 130 : 143;
903
- process.exit(code2);
904
- })();
1097
+ function writeNuvioFolder(opts) {
1098
+ const dir = join7(opts.root, "nuvio");
1099
+ const created = [];
1100
+ mkdirSync2(dir, { recursive: true });
1101
+ const vars = {
1102
+ NUVIO_VERSION: opts.version,
1103
+ PM_RUN: opts.pmRun,
1104
+ FAILED_STEPS: opts.failedSteps.join(", ") || "(none)"
905
1105
  };
906
- process.once("SIGINT", onSignal);
907
- process.once("SIGTERM", onSignal);
1106
+ const startHere = join7(dir, "START_HERE.md");
1107
+ writeFileSync6(
1108
+ startHere,
1109
+ render(loadTemplate("START_HERE.md.tpl"), vars),
1110
+ "utf8"
1111
+ );
1112
+ created.push("nuvio/START_HERE.md");
1113
+ const readme = join7(dir, "README.md");
1114
+ writeFileSync6(
1115
+ readme,
1116
+ render(loadTemplate("README.pointer.md.tpl"), vars),
1117
+ "utf8"
1118
+ );
1119
+ created.push("nuvio/README.md");
1120
+ const agent = join7(dir, "AGENT.md");
1121
+ if (!existsSync5(agent) || opts.forceAgent) {
1122
+ writeFileSync6(agent, render(loadTemplate("AGENT.md.tpl"), vars), "utf8");
1123
+ created.push("nuvio/AGENT.md");
1124
+ }
1125
+ if (opts.failedSteps.length > 0) {
1126
+ const todo = join7(dir, "SETUP_TODO.md");
1127
+ writeFileSync6(
1128
+ todo,
1129
+ render(loadTemplate("SETUP_TODO.md.tpl"), vars),
1130
+ "utf8"
1131
+ );
1132
+ created.push("nuvio/SETUP_TODO.md");
1133
+ }
1134
+ return created;
908
1135
  }
909
1136
 
910
1137
  // src/init.ts
@@ -1152,14 +1379,162 @@ async function runInit(opts) {
1152
1379
  return plan.tier === "partial" || plan.tier === "full" ? 0 : 1;
1153
1380
  }
1154
1381
 
1382
+ // src/scan-cmd.ts
1383
+ function runScan(opts) {
1384
+ let scan;
1385
+ try {
1386
+ scan = scanProject(opts.cwd);
1387
+ } catch (e) {
1388
+ if (e instanceof PreflightError) {
1389
+ console.error(e.message);
1390
+ return 1;
1391
+ }
1392
+ throw e;
1393
+ }
1394
+ const { ctx, detectedLibraries, index } = scan;
1395
+ const projectName = String(ctx.packageJson.name ?? "project");
1396
+ const hosts = index.entries.map((entry) => ({
1397
+ id: entry.id,
1398
+ file: relPath(ctx.root, entry.file),
1399
+ line: entry.line,
1400
+ column: entry.column,
1401
+ libraryHint: entry.libraryHint,
1402
+ classNameMode: entry.classNameMode
1403
+ }));
1404
+ const result = {
1405
+ projectName,
1406
+ hosts,
1407
+ hostCount: hosts.length,
1408
+ duplicateErrors: index.duplicateErrors.map((dup) => ({
1409
+ id: dup.id,
1410
+ occurrences: dup.occurrences.map((o) => ({
1411
+ file: relPath(ctx.root, o.file),
1412
+ line: o.line
1413
+ }))
1414
+ })),
1415
+ detectedLibraries,
1416
+ scannedFileCount: index.scannedFileCount
1417
+ };
1418
+ const pm = detectPackageManager(ctx.root);
1419
+ const telemetry = {
1420
+ ...buildCliTelemetryProps(pm, ctx),
1421
+ host_count: result.hostCount,
1422
+ duplicate_count: result.duplicateErrors.length,
1423
+ library_count: detectedLibraries.length
1424
+ };
1425
+ captureCliEvent("scan_run", telemetry);
1426
+ if (opts.json) {
1427
+ console.log(JSON.stringify(result, null, 2));
1428
+ return result.duplicateErrors.length > 0 ? 1 : 0;
1429
+ }
1430
+ console.log(`nuvio scan \u2014 ${result.hostCount} editable host(s)
1431
+ `);
1432
+ for (const host of hosts) {
1433
+ console.log(
1434
+ ` ${host.id.padEnd(28)} ${host.file}:${host.line}`
1435
+ );
1436
+ }
1437
+ if (result.duplicateErrors.length > 0) {
1438
+ console.log("");
1439
+ for (const dup of result.duplicateErrors) {
1440
+ const places = dup.occurrences.map((o) => `${o.file}:${o.line}`).join(", ");
1441
+ console.log(` \u274C duplicate id: ${dup.id} (${places}) \u2014 fix before apply`);
1442
+ }
1443
+ }
1444
+ if (detectedLibraries.length > 0) {
1445
+ console.log(`
1446
+ Libraries: ${detectedLibraries.join(", ")}`);
1447
+ }
1448
+ if (result.hostCount === 0) {
1449
+ console.log(
1450
+ "\n No hosts found \u2014 use Make Editable in the browser or add data-nuvio-id manually."
1451
+ );
1452
+ }
1453
+ return result.duplicateErrors.length > 0 ? 1 : 0;
1454
+ }
1455
+
1456
+ // src/stats.ts
1457
+ import { readRuntimeVersions } from "@nuvio/vite-plugin/scan";
1458
+ function runStats(opts) {
1459
+ let scan;
1460
+ try {
1461
+ scan = scanProject(opts.cwd);
1462
+ } catch (e) {
1463
+ if (e instanceof PreflightError) {
1464
+ console.error(e.message);
1465
+ return 1;
1466
+ }
1467
+ throw e;
1468
+ }
1469
+ const { ctx, detectedLibraries, index } = scan;
1470
+ const projectName = String(ctx.packageJson.name ?? "project");
1471
+ const taggedFiles = new Set(
1472
+ index.entries.map((e) => relPath(ctx.root, e.file))
1473
+ ).size;
1474
+ const classNameModes = aggregateClassNameModes(index.entries);
1475
+ const tableHosts = index.entries.filter(isTableHost).length;
1476
+ const versions = readRuntimeVersions(ctx.root);
1477
+ const result = {
1478
+ projectName,
1479
+ editableHosts: index.entries.length,
1480
+ taggedFiles,
1481
+ scannedFiles: index.scannedFileCount,
1482
+ duplicateIds: index.duplicateErrors.length,
1483
+ tableHosts,
1484
+ detectedLibraries,
1485
+ tailwindVersion: versions.tailwindVersion,
1486
+ classNameModes
1487
+ };
1488
+ const pm = detectPackageManager(ctx.root);
1489
+ const telemetry = {
1490
+ ...buildCliTelemetryProps(pm, ctx),
1491
+ editable_hosts: result.editableHosts,
1492
+ tagged_files: result.taggedFiles,
1493
+ duplicate_ids: result.duplicateIds,
1494
+ table_hosts: result.tableHosts,
1495
+ library_count: detectedLibraries.length
1496
+ };
1497
+ captureCliEvent("stats_run", telemetry);
1498
+ if (opts.json) {
1499
+ console.log(JSON.stringify(result, null, 2));
1500
+ return 0;
1501
+ }
1502
+ console.log("nuvio stats\n");
1503
+ console.log(` Editable hosts: ${result.editableHosts}`);
1504
+ console.log(` Tagged files: ${result.taggedFiles}`);
1505
+ console.log(` Files scanned: ${result.scannedFiles}`);
1506
+ console.log(
1507
+ ` Libraries detected: ${result.detectedLibraries.length > 0 ? result.detectedLibraries.join(", ") : "none"}`
1508
+ );
1509
+ console.log(` Table hosts: ${result.tableHosts}`);
1510
+ console.log(` Duplicate ids: ${result.duplicateIds}`);
1511
+ if (result.tailwindVersion) {
1512
+ console.log(` Tailwind version: ${result.tailwindVersion}`);
1513
+ }
1514
+ const modeParts = Object.entries(result.classNameModes).sort(([a], [b]) => a.localeCompare(b)).map(([mode, count]) => `${mode} ${count}`);
1515
+ if (modeParts.length > 0) {
1516
+ console.log(` Class modes: ${modeParts.join(", ")}`);
1517
+ }
1518
+ return 0;
1519
+ }
1520
+
1155
1521
  // src/cli.ts
1156
1522
  function printHelp() {
1157
1523
  console.log(`nuvio \u2014 CLI for React + Vite
1158
1524
 
1159
1525
  Usage:
1160
1526
  nuvio init [options]
1527
+ nuvio doctor [options]
1528
+ nuvio scan [options]
1529
+ nuvio stats [options]
1530
+
1531
+ Common options:
1532
+ --cwd <path> Project root (default: current directory)
1533
+ --json Machine-readable output (doctor, scan, stats)
1534
+ --verbose Show error stacks
1535
+ -h, --help Show help
1161
1536
 
1162
- Options:
1537
+ Init options:
1163
1538
  --yes Skip confirmation
1164
1539
  --no-install Patch files only; do not run package manager install
1165
1540
  --dry-run Show plan only (still prompts unless --yes / CI)
@@ -1167,16 +1542,18 @@ Options:
1167
1542
  --strict Fail if Tailwind is not detected
1168
1543
  --skip-tailwind-check Do not warn when Tailwind is missing
1169
1544
  --force-agent Overwrite nuvio/AGENT.md
1170
- --cwd <path> Project root (default: current directory)
1171
- --verbose Show error stacks
1172
- -h, --help Show help
1173
1545
 
1174
- Example:
1175
- pnpm dlx @nuvio/cli init
1546
+ Doctor options:
1547
+ --skip-dev-server Skip localhost dev-server health check
1548
+
1549
+ Examples:
1176
1550
  pnpm dlx @nuvio/cli init --yes
1551
+ pnpm dlx @nuvio/cli doctor
1552
+ pnpm dlx @nuvio/cli scan --json
1553
+ pnpm dlx @nuvio/cli stats
1177
1554
  `);
1178
1555
  }
1179
- function parseArgs(argv) {
1556
+ function parseInitArgs(argv) {
1180
1557
  const args = argv.slice(2);
1181
1558
  let command = null;
1182
1559
  const opts = { cwd: process.cwd() };
@@ -1209,10 +1586,63 @@ function parseArgs(argv) {
1209
1586
  }
1210
1587
  return { command, opts, help };
1211
1588
  }
1589
+ function parseProjectCommandArgs(argv, command) {
1590
+ const args = argv.slice(2);
1591
+ const common = { cwd: process.cwd() };
1592
+ const doctor = { ...common };
1593
+ let help = false;
1594
+ let i = args[0] === command ? 1 : 0;
1595
+ for (; i < args.length; i++) {
1596
+ const arg = args[i];
1597
+ if (arg === "-h" || arg === "--help") {
1598
+ help = true;
1599
+ continue;
1600
+ }
1601
+ if (arg === "--json") {
1602
+ common.json = true;
1603
+ doctor.json = true;
1604
+ } else if (arg === "--verbose") {
1605
+ common.verbose = true;
1606
+ doctor.verbose = true;
1607
+ } else if (arg === "--cwd") {
1608
+ const cwd = resolve(args[++i] ?? ".");
1609
+ common.cwd = cwd;
1610
+ doctor.cwd = cwd;
1611
+ } else if (arg === "--skip-dev-server") {
1612
+ doctor.skipDevServer = true;
1613
+ } else if (arg.startsWith("-")) {
1614
+ console.error(`Unknown option: ${arg}`);
1615
+ help = true;
1616
+ }
1617
+ }
1618
+ return { command, common, doctor, help };
1619
+ }
1212
1620
  async function runCli(argv) {
1213
1621
  registerTelemetrySignalHandlers();
1214
- const { command, opts, help } = parseArgs(argv);
1215
- captureCliInvoked(resolveCliInvokedCommand(help, command), opts.pm);
1622
+ const rawCommand = argv[2] ?? null;
1623
+ const isProjectCmd = rawCommand === "doctor" || rawCommand === "scan" || rawCommand === "stats";
1624
+ let help = false;
1625
+ let command = rawCommand;
1626
+ let initOpts = { cwd: process.cwd() };
1627
+ let commonOpts = { cwd: process.cwd() };
1628
+ let doctorOpts = { cwd: process.cwd() };
1629
+ if (isProjectCmd) {
1630
+ const parsed = parseProjectCommandArgs(argv, rawCommand);
1631
+ help = parsed.help;
1632
+ command = parsed.command;
1633
+ commonOpts = parsed.common;
1634
+ doctorOpts = parsed.doctor;
1635
+ } else {
1636
+ const parsed = parseInitArgs(argv);
1637
+ help = parsed.help;
1638
+ command = parsed.command;
1639
+ initOpts = parsed.opts;
1640
+ }
1641
+ const cwd = isProjectCmd ? commonOpts.cwd : initOpts.cwd;
1642
+ captureCliInvoked(
1643
+ resolveCliInvokedCommand(help, command),
1644
+ isProjectCmd ? void 0 : initOpts.pm
1645
+ );
1216
1646
  try {
1217
1647
  if (help) {
1218
1648
  printHelp();
@@ -1222,19 +1652,32 @@ async function runCli(argv) {
1222
1652
  printHelp();
1223
1653
  return 1;
1224
1654
  }
1225
- if (command !== "init") {
1226
- console.error(`Unknown command: ${command}`);
1227
- printHelp();
1228
- return 1;
1655
+ switch (command) {
1656
+ case "init":
1657
+ return await runInit(initOpts);
1658
+ case "doctor":
1659
+ return await runDoctor({
1660
+ cwd: doctorOpts.cwd,
1661
+ json: doctorOpts.json,
1662
+ checkDevServer: !doctorOpts.skipDevServer
1663
+ });
1664
+ case "scan":
1665
+ return runScan({ cwd: commonOpts.cwd, json: commonOpts.json });
1666
+ case "stats":
1667
+ return runStats({ cwd: commonOpts.cwd, json: commonOpts.json });
1668
+ default:
1669
+ console.error(`Unknown command: ${command}`);
1670
+ printHelp();
1671
+ return 1;
1229
1672
  }
1230
- return await runInit(opts);
1231
1673
  } catch (e) {
1232
- const pm = detectPackageManager(opts.cwd, opts.pm);
1674
+ const pm = detectPackageManager(cwd, initOpts.pm);
1233
1675
  captureCliEvent("nuvio_init_failed", {
1234
1676
  ...buildCliTelemetryProps(pm),
1235
1677
  error_code: "unexpected_error"
1236
1678
  });
1237
- if (opts.verbose) console.error(e);
1679
+ const verbose = isProjectCmd ? commonOpts.verbose : initOpts.verbose;
1680
+ if (verbose) console.error(e);
1238
1681
  else console.error("Something went wrong. Run with --verbose for details.");
1239
1682
  return 2;
1240
1683
  } finally {