@nuvio/cli 0.5.4 → 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,25 +3,48 @@
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 = {
15
38
  noPackageJson: "Run this from your app folder (the one with package.json).",
16
- noVite: "Nuvio works with React + Vite projects. I couldn't find a Vite config here.",
17
- noReact: "Nuvio needs React. Add react to this project first.",
18
- noViteDep: "Nuvio needs Vite. Add vite to this project first.",
19
- strictTailwind: "Nuvio expects Tailwind CSS for class edits. Install tailwindcss or pass --skip-tailwind-check.",
20
- monorepoRoot: "This looks like the Nuvio monorepo. Run init in your app folder, not the tooling repo.",
39
+ noVite: "nuvio works with React + Vite projects. I couldn't find a Vite config here.",
40
+ noReact: "nuvio needs React. Add react to this project first.",
41
+ noViteDep: "nuvio needs Vite. Add vite to this project first.",
42
+ strictTailwind: "nuvio expects Tailwind CSS for class edits. Install tailwindcss or pass --skip-tailwind-check.",
43
+ monorepoRoot: "This looks like the nuvio monorepo. Run init in your app folder, not the tooling repo.",
21
44
  cliPackage: "Cannot init inside @nuvio/cli package.",
22
- partialHelp: "Nuvio set up what it could safely. Finish the steps in nuvio/SETUP_TODO.md, then run your dev server.",
23
- noHeading: 'Nuvio is wired, but I could not find a heading to mark editable. Add data-nuvio-id="page.title" to one visible element (see nuvio/START_HERE.md).',
24
- telemetryNotice: `Nuvio collects anonymous usage metrics to improve onboarding and reliability.
45
+ partialHelp: "nuvio set up what it could safely. Finish the steps in nuvio/SETUP_TODO.md, then run your dev server.",
46
+ noHeading: 'nuvio is wired, but I could not find a heading to mark editable. Add data-nuvio-id="page.title" to one visible element (see nuvio/START_HERE.md).',
47
+ telemetryNotice: `nuvio collects anonymous usage metrics to improve onboarding and reliability.
25
48
  No source code, file contents, file paths, project names, emails, or personal data are sent.
26
49
 
27
50
  Disable anytime with:
@@ -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";
130
+ // src/nuvio-deps.ts
131
+ import { readFileSync as readFileSync2 } from "fs";
132
+ function readPackageJson(packageJsonPath) {
133
+ return JSON.parse(readFileSync2(packageJsonPath, "utf8"));
118
134
  }
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
- }
135
+ function getDependencyVersion(pkg, name) {
136
+ const deps = pkg.dependencies;
137
+ const devDeps = pkg.devDependencies;
138
+ return deps?.[name] ?? devDeps?.[name];
139
+ }
140
+ function hasNuvioDependency(pkg, name) {
141
+ return Boolean(getDependencyVersion(pkg, name));
142
+ }
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";
131
154
  }
132
155
 
133
- // src/install-packages.ts
134
- import { spawnSync } from "child_process";
135
- import { readFileSync as readFileSync2 } from "fs";
136
- function parseInstalledVersion(pkg, name) {
137
- const dev = pkg.devDependencies;
138
- const deps = pkg.dependencies;
139
- const raw = dev?.[name] ?? deps?.[name];
140
- if (!raw) return null;
141
- return raw.replace(/^[\^~]/, "");
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 };
142
169
  }
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;
170
+ function relPath(root, fileAbs) {
171
+ return relative(root, fileAbs).replace(/\\/g, "/");
150
172
  }
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
- };
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;
165
181
  }
166
- return { ok: true };
182
+ return counts;
167
183
  }
168
184
 
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;
173
-
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;
201
+ // src/telemetry.ts
202
+ var POSTHOG_HOST = "https://us.i.posthog.com";
203
+ function telemetryFilePath() {
204
+ return join3(homedir(), ".nuvio", "telemetry.json");
210
205
  }
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;
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}`);
219
227
  }
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
- );
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;
226
233
  }
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;
234
+ function posthogToken() {
235
+ return process.env.NUVIO_POSTHOG_TOKEN ?? NUVIO_POSTHOG_TOKEN;
282
236
  }
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);
237
+ function tokenIsConfigured(token) {
238
+ return Boolean(token && token.startsWith("phc_"));
288
239
  }
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;
240
+ function readOrCreateAnonymousId() {
241
+ if (sessionAnonymousId) return sessionAnonymousId;
242
+ try {
243
+ const raw = readFileSync3(telemetryFilePath(), "utf8");
244
+ const parsed = JSON.parse(raw);
245
+ if (parsed.anonymousId) {
246
+ sessionAnonymousId = parsed.anonymousId;
247
+ return parsed.anonymousId;
297
248
  }
298
- });
299
- return patched;
300
- }
301
- function patchViteConfigFile(filePath) {
302
- const source = readFileSync3(filePath, "utf8");
303
- let ast;
249
+ } catch {
250
+ }
251
+ const id = randomUUID();
252
+ sessionAnonymousId = id;
304
253
  try {
305
- ast = parseTs(source, filePath);
254
+ mkdirSync(join3(homedir(), ".nuvio"), { recursive: true, mode: 448 });
255
+ writeFileSync(
256
+ telemetryFilePath(),
257
+ JSON.stringify({ anonymousId: id }, null, 2),
258
+ { mode: 384 }
259
+ );
306
260
  } catch {
307
- return { ok: false, error: "parse failed" };
308
261
  }
309
- const depsPatched = ensureOptimizeDepsExclude(ast);
310
- const alreadyPlugin = hasNuvioImport(ast) && hasNuvioPluginCall(ast);
311
- if (alreadyPlugin && !depsPatched) {
312
- return { ok: true, skipped: true };
262
+ return id;
263
+ }
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
+ });
313
278
  }
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
- )
320
- );
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;
321
289
  }
322
- if (!hasNuvioPluginCall(ast)) {
323
- if (!appendNuvioPlugin(ast)) {
324
- return { ok: false, error: "no static plugins array" };
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) {
341
+ try {
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;
325
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);
326
360
  }
327
- writeFileSync(filePath, printTs(ast, source), "utf8");
328
- return { ok: true, skipped: alreadyPlugin && depsPatched };
329
361
  }
330
- function viteConfigHasNuvio(filePath) {
331
- const source = readFileSync3(filePath, "utf8");
332
- try {
333
- const ast = parseTs(source, filePath);
334
- return hasNuvioImport(ast) && hasNuvioPluginCall(ast);
335
- } catch {
336
- return /nuvio\s*\(/.test(source);
337
- }
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);
387
+ }
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;
407
+
408
+ // src/patch-app-root.ts
409
+ import * as t from "@babel/types";
410
+ import fg from "fast-glob";
411
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
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
+ `;
338
435
  }
339
436
 
340
437
  // src/patch-app-root.ts
341
- import * as t2 from "@babel/types";
342
- import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
343
- import { join as join3 } from "path";
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,19 +594,156 @@ 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";
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;
605
+ }
606
+ });
607
+ return found;
608
+ }
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;
614
+ }
615
+ });
616
+ return found;
617
+ }
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) {
626
+ let patched = false;
627
+ babel_traverse_default(ast, {
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
+ }
639
+ }
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"), []));
694
+ patched = true;
695
+ }
696
+ });
697
+ return patched;
698
+ }
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" };
723
+ }
724
+ }
725
+ writeFileSync4(filePath, printTs(ast, source), "utf8");
726
+ return { ok: true, skipped: alreadyPlugin && depsPatched };
727
+ }
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
+ }
736
+ }
496
737
 
497
738
  // src/scan-ids.ts
498
- import { readFileSync as readFileSync6 } from "fs";
499
- import { join as join5 } from "path";
500
- import fg from "fast-glob";
739
+ import { readFileSync as readFileSync7 } from "fs";
740
+ import { join as join6 } from "path";
741
+ import fg2 from "fast-glob";
501
742
  var ID_GLOB = ["src/**/*.{tsx,jsx}"];
502
743
  function projectHasPageTitleId(root) {
503
- const files = fg.sync(ID_GLOB, { cwd: root, absolute: true });
744
+ const files = fg2.sync(ID_GLOB, { cwd: root, absolute: true });
504
745
  for (const file of files) {
505
- const text = readFileSync6(file, "utf8");
746
+ const text = readFileSync7(file, "utf8");
506
747
  if (/data-nuvio-id=["']page\.title["']/.test(text)) {
507
748
  return true;
508
749
  }
@@ -510,10 +751,10 @@ function projectHasPageTitleId(root) {
510
751
  return false;
511
752
  }
512
753
  function findHeadingFiles(root) {
513
- const files = fg.sync(ID_GLOB, { cwd: root, absolute: true });
754
+ const files = fg2.sync(ID_GLOB, { cwd: root, absolute: true });
514
755
  const ordered = [
515
- join5(root, "src/App.tsx"),
516
- join5(root, "src/App.jsx"),
756
+ join6(root, "src/App.tsx"),
757
+ join6(root, "src/App.jsx"),
517
758
  ...files.filter(
518
759
  (f) => !f.endsWith("App.tsx") && !f.endsWith("App.jsx")
519
760
  )
@@ -532,9 +773,239 @@ function findHeadingFiles(root) {
532
773
  return out;
533
774
  }
534
775
 
776
+ // src/verify.ts
777
+ function optimizeDepsSatisfied(viteConfigPath, packageJsonPath) {
778
+ if (viteConfigHasOverlayOptimizeExclude(viteConfigPath)) return true;
779
+ const pkg = readPackageJson(packageJsonPath);
780
+ return nuvioOverlayLinkKind(pkg) === "workspace";
781
+ }
782
+ function verifyProject(root, packageJsonPath, viteConfigPath) {
783
+ const pkg = readPackageJson(packageJsonPath);
784
+ const depsOk = hasNuvioPackages(pkg);
785
+ const mainEntry = resolveMainEntry(root);
786
+ return {
787
+ deps: depsOk ? "OK" : "MISSING",
788
+ vite: viteConfigHasNuvio(viteConfigPath) ? "OK" : "TODO",
789
+ overlayCss: mainEntry && (mainHasOverlayStyles(mainEntry) || !overlayInstalledFromNpm(packageJsonPath)) ? "OK" : "TODO",
790
+ optimizeDeps: optimizeDepsSatisfied(viteConfigPath, packageJsonPath) ? "OK" : "TODO",
791
+ shell: projectHasDevShell(root) ? "OK" : "TODO",
792
+ starterId: projectHasPageTitleId(root) ? "OK" : "MISSING"
793
+ };
794
+ }
795
+ function printVerification(v) {
796
+ console.log("Verification:");
797
+ console.log(
798
+ ` dependencies: @nuvio/vite-plugin, @nuvio/overlay \u2014 ${v.deps}`
799
+ );
800
+ console.log(` vite.config: nuvio() \u2014 ${v.vite}`);
801
+ console.log(` main.tsx: @nuvio/overlay/style.css \u2014 ${v.overlayCss}`);
802
+ console.log(` vite.config: optimizeDeps exclude overlay \u2014 ${v.optimizeDeps}`);
803
+ console.log(` App shell: NuvioDevShell \u2014 ${v.shell}`);
804
+ console.log(` Starter id page.title \u2014 ${v.starterId}`);
805
+ }
806
+
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
+ };
832
+ }
833
+ }
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}`);
840
+ }
841
+ async function runDoctor(opts) {
842
+ let scan;
843
+ try {
844
+ scan = scanProject(opts.cwd);
845
+ } catch (e) {
846
+ if (e instanceof PreflightError) {
847
+ console.error(e.message);
848
+ return 1;
849
+ }
850
+ throw e;
851
+ }
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
+ });
913
+ }
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`
920
+ });
921
+ } else {
922
+ checks.push({
923
+ id: "duplicate_ids",
924
+ label: "No duplicate data-nuvio-id values",
925
+ status: "pass"
926
+ });
927
+ }
928
+ if (opts.checkDevServer !== false) {
929
+ checks.push(await checkDevServerReachable(opts.devServerPort ?? 5173));
930
+ }
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
940
+ };
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
948
+ };
949
+ captureCliEvent("doctor_run", telemetry);
950
+ if (opts.json) {
951
+ console.log(JSON.stringify(result, null, 2));
952
+ return failCount > 0 ? 1 : 0;
953
+ }
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;
963
+ }
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;
983
+ }
984
+ return false;
985
+ }
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 };
1002
+ }
1003
+
535
1004
  // src/patch-starter-id.ts
1005
+ import * as t3 from "@babel/types";
1006
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
536
1007
  function patchFirstHeading(filePath) {
537
- const source = readFileSync7(filePath, "utf8");
1008
+ const source = readFileSync9(filePath, "utf8");
538
1009
  let ast;
539
1010
  try {
540
1011
  ast = parseTs(source, filePath);
@@ -563,13 +1034,13 @@ function patchFirstHeading(filePath) {
563
1034
  }
564
1035
  });
565
1036
  if (!patched) return { ok: false, error: "no h1/h2" };
566
- writeFileSync4(filePath, printTs(ast, source), "utf8");
1037
+ writeFileSync5(filePath, printTs(ast, source), "utf8");
567
1038
  return { ok: true };
568
1039
  }
569
1040
  function patchStarterId(root) {
570
1041
  const files = findHeadingFiles(root);
571
1042
  for (const file of files) {
572
- const source = readFileSync7(file, "utf8");
1043
+ const source = readFileSync9(file, "utf8");
573
1044
  if (!/<h[12][\s>]/.test(source) && !/<>[\s\S]*<h[12]/.test(source)) {
574
1045
  try {
575
1046
  const ast = parseTs(source, file);
@@ -608,18 +1079,13 @@ function createPlan(root, pm) {
608
1079
  };
609
1080
  }
610
1081
 
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
1082
  // 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";
1083
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
1084
+ import { dirname, join as join7 } from "path";
619
1085
  import { fileURLToPath } from "url";
620
- var CLI_ROOT = join6(dirname(fileURLToPath(import.meta.url)), "..");
1086
+ var CLI_ROOT = join7(dirname(fileURLToPath(import.meta.url)), "..");
621
1087
  function loadTemplate(name) {
622
- return readFileSync8(join6(CLI_ROOT, "templates", name), "utf8");
1088
+ return readFileSync10(join7(CLI_ROOT, "templates", name), "utf8");
623
1089
  }
624
1090
  function render(tpl, vars) {
625
1091
  let out = tpl;
@@ -629,36 +1095,36 @@ function render(tpl, vars) {
629
1095
  return out;
630
1096
  }
631
1097
  function writeNuvioFolder(opts) {
632
- const dir = join6(opts.root, "nuvio");
1098
+ const dir = join7(opts.root, "nuvio");
633
1099
  const created = [];
634
- mkdirSync(dir, { recursive: true });
1100
+ mkdirSync2(dir, { recursive: true });
635
1101
  const vars = {
636
1102
  NUVIO_VERSION: opts.version,
637
1103
  PM_RUN: opts.pmRun,
638
1104
  FAILED_STEPS: opts.failedSteps.join(", ") || "(none)"
639
1105
  };
640
- const startHere = join6(dir, "START_HERE.md");
641
- writeFileSync5(
1106
+ const startHere = join7(dir, "START_HERE.md");
1107
+ writeFileSync6(
642
1108
  startHere,
643
1109
  render(loadTemplate("START_HERE.md.tpl"), vars),
644
1110
  "utf8"
645
1111
  );
646
1112
  created.push("nuvio/START_HERE.md");
647
- const readme = join6(dir, "README.md");
648
- writeFileSync5(
1113
+ const readme = join7(dir, "README.md");
1114
+ writeFileSync6(
649
1115
  readme,
650
1116
  render(loadTemplate("README.pointer.md.tpl"), vars),
651
1117
  "utf8"
652
1118
  );
653
1119
  created.push("nuvio/README.md");
654
- const agent = join6(dir, "AGENT.md");
1120
+ const agent = join7(dir, "AGENT.md");
655
1121
  if (!existsSync5(agent) || opts.forceAgent) {
656
- writeFileSync5(agent, render(loadTemplate("AGENT.md.tpl"), vars), "utf8");
1122
+ writeFileSync6(agent, render(loadTemplate("AGENT.md.tpl"), vars), "utf8");
657
1123
  created.push("nuvio/AGENT.md");
658
1124
  }
659
1125
  if (opts.failedSteps.length > 0) {
660
- const todo = join6(dir, "SETUP_TODO.md");
661
- writeFileSync5(
1126
+ const todo = join7(dir, "SETUP_TODO.md");
1127
+ writeFileSync6(
662
1128
  todo,
663
1129
  render(loadTemplate("SETUP_TODO.md.tpl"), vars),
664
1130
  "utf8"
@@ -668,194 +1134,6 @@ function writeNuvioFolder(opts) {
668
1134
  return created;
669
1135
  }
670
1136
 
671
- // src/verify.ts
672
- import { readFileSync as readFileSync9 } from "fs";
673
- 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);
678
- const mainEntry = resolveMainEntry(root);
679
- return {
680
- deps: depsOk ? "OK" : "MISSING",
681
- vite: viteConfigHasNuvio(viteConfigPath) ? "OK" : "TODO",
682
- overlayCss: mainEntry && (mainHasOverlayStyles(mainEntry) || !overlayInstalledFromNpm(packageJsonPath)) ? "OK" : "TODO",
683
- optimizeDeps: viteConfigHasOverlayOptimizeExclude(viteConfigPath) ? "OK" : "TODO",
684
- shell: appFile && appHasDevShell(appFile) ? "OK" : "TODO",
685
- starterId: projectHasPageTitleId(root) ? "OK" : "MISSING"
686
- };
687
- }
688
- function printVerification(v) {
689
- console.log("Verification:");
690
- console.log(
691
- ` devDependencies: @nuvio/vite-plugin, @nuvio/overlay \u2014 ${v.deps}`
692
- );
693
- console.log(` vite.config: nuvio() \u2014 ${v.vite}`);
694
- console.log(` main.tsx: @nuvio/overlay/style.css \u2014 ${v.overlayCss}`);
695
- console.log(` vite.config: optimizeDeps exclude overlay \u2014 ${v.optimizeDeps}`);
696
- console.log(` App shell: NuvioDevShell \u2014 ${v.shell}`);
697
- console.log(` Starter id page.title \u2014 ${v.starterId}`);
698
- }
699
-
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 client = null;
726
- var sessionAnonymousId = null;
727
- function telemetryDebug(message, detail) {
728
- if (process.env.NUVIO_TELEMETRY_DEBUG !== "1") return;
729
- if (detail !== void 0) {
730
- console.error(`[nuvio telemetry] ${message}`, detail);
731
- return;
732
- }
733
- console.error(`[nuvio telemetry] ${message}`);
734
- }
735
- function isTelemetryEnabled() {
736
- const flag = process.env.NUVIO_TELEMETRY;
737
- if (flag === "0") return false;
738
- if (flag?.toLowerCase() === "false") return false;
739
- return true;
740
- }
741
- function posthogToken() {
742
- return process.env.NUVIO_POSTHOG_TOKEN ?? NUVIO_POSTHOG_TOKEN;
743
- }
744
- function tokenIsConfigured(token) {
745
- return Boolean(token && token.startsWith("phc_"));
746
- }
747
- function readOrCreateAnonymousId() {
748
- if (sessionAnonymousId) return sessionAnonymousId;
749
- try {
750
- const raw = readFileSync10(telemetryFilePath(), "utf8");
751
- const parsed = JSON.parse(raw);
752
- if (parsed.anonymousId) {
753
- sessionAnonymousId = parsed.anonymousId;
754
- return parsed.anonymousId;
755
- }
756
- } catch {
757
- }
758
- const id = randomUUID();
759
- sessionAnonymousId = id;
760
- try {
761
- mkdirSync2(join7(homedir(), ".nuvio"), { recursive: true, mode: 448 });
762
- writeFileSync6(
763
- telemetryFilePath(),
764
- JSON.stringify({ anonymousId: id }, null, 2),
765
- { mode: 384 }
766
- );
767
- } catch {
768
- }
769
- return id;
770
- }
771
- function getClient() {
772
- if (!isTelemetryEnabled()) return null;
773
- const token = posthogToken();
774
- if (!tokenIsConfigured(token)) return null;
775
- if (!client) {
776
- client = new PostHog(token, {
777
- host: POSTHOG_HOST,
778
- flushAt: 1,
779
- flushInterval: 0
780
- });
781
- telemetryDebug("PostHog client initialized", {
782
- host: POSTHOG_HOST,
783
- tokenPrefix: `${token.slice(0, 8)}\u2026`
784
- });
785
- }
786
- return client;
787
- }
788
- function sanitizeProps(props) {
789
- if (!props) return void 0;
790
- const out = {};
791
- for (const [key, value] of Object.entries(props)) {
792
- if (FORBIDDEN_PROP_KEYS.has(key)) continue;
793
- if (value === void 0) continue;
794
- if (typeof value === "string" && /[/\\]/.test(value)) continue;
795
- out[key] = value;
796
- }
797
- return Object.keys(out).length > 0 ? out : void 0;
798
- }
799
- function buildCliTelemetryProps(pm, project) {
800
- const props = {
801
- nuvio_version: NUVIO_VERSION,
802
- os: process.platform,
803
- arch: os.arch(),
804
- node: process.version
805
- };
806
- if (pm) props.package_manager = pm;
807
- if (project) {
808
- props.has_react = true;
809
- props.has_vite = true;
810
- props.has_tailwind = project.tailwindOk;
811
- }
812
- return props;
813
- }
814
- function preflightErrorCode(message) {
815
- if (message === MSG.noPackageJson) return "preflight_no_package_json";
816
- if (message === MSG.noVite) return "preflight_no_vite";
817
- if (message === MSG.noReact) return "preflight_no_react";
818
- if (message === MSG.noViteDep) return "preflight_no_vite_dep";
819
- if (message === MSG.monorepoRoot || message === MSG.cliPackage) {
820
- return "preflight_monorepo";
821
- }
822
- return "preflight_unknown";
823
- }
824
- function captureCliEvent(event, props) {
825
- try {
826
- if (!isTelemetryEnabled()) {
827
- telemetryDebug(`skipped ${event} (telemetry disabled)`);
828
- return;
829
- }
830
- const ph = getClient();
831
- if (!ph) {
832
- telemetryDebug(`skipped ${event} (no PostHog client \u2014 check token)`);
833
- return;
834
- }
835
- const distinctId = readOrCreateAnonymousId();
836
- ph.capture({
837
- distinctId,
838
- event,
839
- properties: sanitizeProps(props)
840
- });
841
- telemetryDebug(`captured ${event}`, { distinctId });
842
- } catch (error) {
843
- telemetryDebug(`capture failed for ${event}`, error);
844
- }
845
- }
846
- async function shutdownTelemetry() {
847
- try {
848
- if (client) {
849
- await client.flush();
850
- await client.shutdown();
851
- client = null;
852
- telemetryDebug("flush + shutdown complete");
853
- }
854
- } catch (error) {
855
- telemetryDebug("shutdown failed", error);
856
- }
857
- }
858
-
859
1137
  // src/init.ts
860
1138
  function isAutoYes(opts) {
861
1139
  if (opts.yes) return true;
@@ -889,14 +1167,14 @@ function computeTier(installOk, viteOk, appOk, starterOk) {
889
1167
  function printSuccess(plan, checks) {
890
1168
  if (checks.install) {
891
1169
  console.log(
892
- `\u2705 Nuvio packages targeted (@nuvio/vite-plugin@${NUVIO_VERSION}, @nuvio/overlay@${NUVIO_VERSION})`
1170
+ `\u2705 nuvio packages targeted (@nuvio/vite-plugin@${NUVIO_VERSION}, @nuvio/overlay@${NUVIO_VERSION})`
893
1171
  );
894
1172
  }
895
1173
  if (checks.vite) console.log("\u2705 Vite plugin added");
896
1174
  else if (plan.failedSteps.some((s) => s.includes("vite"))) {
897
1175
  console.log("\u26A0 Vite plugin \u2014 see nuvio/SETUP_TODO.md");
898
1176
  }
899
- if (checks.app) console.log("\u2705 Nuvio editor mounted");
1177
+ if (checks.app) console.log("\u2705 nuvio editor mounted");
900
1178
  else console.log("\u26A0 App shell \u2014 see nuvio/SETUP_TODO.md");
901
1179
  if (checks.starter) {
902
1180
  console.log(
@@ -917,7 +1195,7 @@ Next:
917
1195
  ${MSG.partialHelp}`);
918
1196
  } else if (plan.tier === "partial") {
919
1197
  console.log(
920
- "\nNuvio helped you as far as it safely could. See warnings above."
1198
+ "\nnuvio helped you as far as it safely could. See warnings above."
921
1199
  );
922
1200
  }
923
1201
  console.log(`
@@ -1017,7 +1295,7 @@ async function runInit(opts) {
1017
1295
  return 1;
1018
1296
  }
1019
1297
  } else {
1020
- console.log("\u2705 Nuvio packages already installed");
1298
+ console.log("\u2705 nuvio packages already installed");
1021
1299
  }
1022
1300
  } else {
1023
1301
  console.log("(skipped install \u2014 --no-install)");
@@ -1101,14 +1379,162 @@ async function runInit(opts) {
1101
1379
  return plan.tier === "partial" || plan.tier === "full" ? 0 : 1;
1102
1380
  }
1103
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
+
1104
1521
  // src/cli.ts
1105
1522
  function printHelp() {
1106
- console.log(`nuvio \u2014 Nuvio CLI
1523
+ console.log(`nuvio \u2014 CLI for React + Vite
1107
1524
 
1108
1525
  Usage:
1109
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
1110
1536
 
1111
- Options:
1537
+ Init options:
1112
1538
  --yes Skip confirmation
1113
1539
  --no-install Patch files only; do not run package manager install
1114
1540
  --dry-run Show plan only (still prompts unless --yes / CI)
@@ -1116,16 +1542,18 @@ Options:
1116
1542
  --strict Fail if Tailwind is not detected
1117
1543
  --skip-tailwind-check Do not warn when Tailwind is missing
1118
1544
  --force-agent Overwrite nuvio/AGENT.md
1119
- --cwd <path> Project root (default: current directory)
1120
- --verbose Show error stacks
1121
- -h, --help Show help
1122
1545
 
1123
- Example:
1124
- pnpm dlx @nuvio/cli init
1546
+ Doctor options:
1547
+ --skip-dev-server Skip localhost dev-server health check
1548
+
1549
+ Examples:
1125
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
1126
1554
  `);
1127
1555
  }
1128
- function parseArgs(argv) {
1556
+ function parseInitArgs(argv) {
1129
1557
  const args = argv.slice(2);
1130
1558
  let command = null;
1131
1559
  const opts = { cwd: process.cwd() };
@@ -1158,30 +1586,98 @@ function parseArgs(argv) {
1158
1586
  }
1159
1587
  return { command, opts, help };
1160
1588
  }
1161
- async function runCli(argv) {
1162
- const { command, opts, help } = parseArgs(argv);
1163
- if (help) {
1164
- printHelp();
1165
- return 0;
1166
- }
1167
- if (!command) {
1168
- printHelp();
1169
- return 1;
1170
- }
1171
- if (command !== "init") {
1172
- console.error(`Unknown command: ${command}`);
1173
- printHelp();
1174
- return 1;
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
+ }
1175
1617
  }
1618
+ return { command, common, doctor, help };
1619
+ }
1620
+ async function runCli(argv) {
1621
+ registerTelemetrySignalHandlers();
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
+ );
1176
1646
  try {
1177
- return await runInit(opts);
1647
+ if (help) {
1648
+ printHelp();
1649
+ return 0;
1650
+ }
1651
+ if (!command) {
1652
+ printHelp();
1653
+ return 1;
1654
+ }
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;
1672
+ }
1178
1673
  } catch (e) {
1179
- const pm = detectPackageManager(opts.cwd, opts.pm);
1674
+ const pm = detectPackageManager(cwd, initOpts.pm);
1180
1675
  captureCliEvent("nuvio_init_failed", {
1181
1676
  ...buildCliTelemetryProps(pm),
1182
1677
  error_code: "unexpected_error"
1183
1678
  });
1184
- if (opts.verbose) console.error(e);
1679
+ const verbose = isProjectCmd ? commonOpts.verbose : initOpts.verbose;
1680
+ if (verbose) console.error(e);
1185
1681
  else console.error("Something went wrong. Run with --verbose for details.");
1186
1682
  return 2;
1187
1683
  } finally {