@nuvio/cli 0.5.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli-entry.js CHANGED
@@ -1,10 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { resolve } from "path";
4
+ import { resolve as resolve4 } from "path";
5
5
 
6
- // src/init.ts
7
- import { createInterface } from "readline";
6
+ // src/brand-apply.ts
7
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
8
+ import { join as join2, resolve } from "path";
9
+ import { applyPatchToSource } from "@nuvio/ast-engine";
10
+ import {
11
+ buildBrandPatchOps,
12
+ brandFragmentHostHint,
13
+ DEFAULT_BRAND_CONFIG,
14
+ isHostPatchable,
15
+ isPccBrandableCategory,
16
+ normalizeBrandConfig
17
+ } from "@nuvio/shared";
18
+ import {
19
+ listPccManifestFiles,
20
+ loadPccManifestFromFile,
21
+ resolvePccManifestPath
22
+ } from "@nuvio/shared/load-pcc-manifest";
8
23
 
9
24
  // src/detect-project.ts
10
25
  import { existsSync, readFileSync } from "fs";
@@ -104,283 +119,957 @@ function detectProject(root) {
104
119
  };
105
120
  }
106
121
 
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}`;
122
+ // src/project-scan.ts
123
+ import { relative } from "path";
124
+ import {
125
+ buildSourceIndex,
126
+ detectProjectLibraries,
127
+ NUVIO_DEFAULT_SCAN_GLOBS
128
+ } from "@nuvio/vite-plugin/scan";
129
+ var SCAN_GLOBS = [...NUVIO_DEFAULT_SCAN_GLOBS, "app/**/*.{tsx,jsx}"];
130
+ function scanProject(root) {
131
+ const ctx = detectProject(root);
132
+ const detectedLibraries = detectProjectLibraries(root, ctx.packageJson);
133
+ const index = buildSourceIndex(root, SCAN_GLOBS, { detectedLibraries });
134
+ return { ctx, detectedLibraries, index };
135
+ }
136
+ function relPath(root, fileAbs) {
137
+ return relative(root, fileAbs).replace(/\\/g, "/");
138
+ }
139
+ function isTableHost(entry) {
140
+ return entry.hierarchyRole === "table" || entry.id.endsWith(".table") || entry.id.includes(".header.") || /\.row\./.test(entry.id);
141
+ }
142
+ function aggregateClassNameModes(entries) {
143
+ const counts = {};
144
+ for (const entry of entries) {
145
+ const mode = entry.classNameMode ?? "literal-only";
146
+ counts[mode] = (counts[mode] ?? 0) + 1;
130
147
  }
148
+ return counts;
131
149
  }
132
150
 
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(/^[\^~]/, "");
142
- }
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;
151
+ // src/brand-apply.ts
152
+ var BRAND_RELATIVE = "nuvio/brand.json";
153
+ function readProjectBrandConfig(cwd) {
154
+ const filePath = join2(resolve(cwd), BRAND_RELATIVE);
155
+ if (!existsSync2(filePath)) {
156
+ return { ...DEFAULT_BRAND_CONFIG };
148
157
  }
149
- return false;
150
- }
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
- };
158
+ try {
159
+ const raw = readFileSync2(filePath, "utf8");
160
+ return normalizeBrandConfig(JSON.parse(raw));
161
+ } catch {
162
+ return { ...DEFAULT_BRAND_CONFIG };
165
163
  }
166
- return { ok: true };
167
- }
168
-
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";
177
-
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;
182
-
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
164
  }
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
- `;
165
+ function duplicateIdSet(errors) {
166
+ return new Set(errors.map((error) => error.id));
199
167
  }
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;
168
+ function collectApplyTargets(manifest, entries, duplicateIds) {
169
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
170
+ const targets = [];
171
+ let skipped = 0;
172
+ for (const category of Object.keys(manifest.categories)) {
173
+ if (!isPccBrandableCategory(category)) {
174
+ continue;
207
175
  }
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;
176
+ const config = manifest.categories[category];
177
+ if (!config) {
178
+ continue;
216
179
  }
217
- });
218
- return found;
219
- }
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
- );
226
- }
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;
180
+ for (const hostId of config.hosts) {
181
+ const entry = byId.get(hostId);
182
+ if (!entry) {
183
+ skipped += 1;
184
+ continue;
256
185
  }
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
- }
186
+ const patch = isHostPatchable(entry, duplicateIds);
187
+ if (!patch.patchable) {
188
+ skipped += 1;
189
+ continue;
264
190
  }
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;
191
+ targets.push({
192
+ hostId,
193
+ category,
194
+ action: category,
195
+ entry
196
+ });
197
+ }
198
+ }
199
+ return { targets, skipped };
200
+ }
201
+ async function applyTargetsToProject(projectRoot, targets, brand, dryRun) {
202
+ const root = resolve(projectRoot);
203
+ const byFile = /* @__PURE__ */ new Map();
204
+ for (const target of targets) {
205
+ const filePath = resolve(root, target.entry.file);
206
+ const list = byFile.get(filePath) ?? [];
207
+ list.push(target);
208
+ byFile.set(filePath, list);
209
+ }
210
+ let applied = 0;
211
+ const failed = [];
212
+ for (const [filePath, fileTargets] of byFile) {
213
+ if (!existsSync2(filePath)) {
214
+ for (const target of fileTargets) {
215
+ failed.push({ hostId: target.hostId, reason: "file_missing" });
274
216
  }
275
- if (t.isArrayExpression(excludeProp.value) && !excludeListsOverlay(excludeProp.value)) {
276
- excludeProp.value.elements.push(t.stringLiteral(OVERLAY_DEP));
277
- patched = true;
217
+ continue;
218
+ }
219
+ if (dryRun) {
220
+ applied += fileTargets.length;
221
+ continue;
222
+ }
223
+ let source = readFileSync2(filePath, "utf8");
224
+ for (const target of fileTargets) {
225
+ const ops = buildBrandPatchOps(
226
+ target.action,
227
+ brand,
228
+ brandFragmentHostHint(target.entry)
229
+ );
230
+ const result = await applyPatchToSource(source, filePath, target.hostId, ops, {
231
+ classNameMode: target.entry.classNameMode
232
+ });
233
+ if (!result.ok) {
234
+ failed.push({ hostId: target.hostId, reason: result.message ?? result.code });
235
+ continue;
278
236
  }
237
+ source = result.source;
238
+ applied += 1;
279
239
  }
280
- });
281
- return patched;
282
- }
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);
288
- }
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
+ writeFileSync(filePath, source, "utf8");
241
+ }
242
+ return { applied, failed };
243
+ }
244
+ function printHumanReport(result) {
245
+ console.log(`Page: ${result.page}`);
246
+ console.log(`Route: ${result.route}`);
247
+ console.log(`Manifest: ${result.manifestPath}`);
248
+ console.log(`Applied: ${result.applied}`);
249
+ console.log(`Skipped: ${result.skipped}`);
250
+ if (result.failed.length > 0) {
251
+ console.log("Failed:");
252
+ for (const failure of result.failed) {
253
+ console.log(`- ${failure.hostId}: ${failure.reason}`);
297
254
  }
298
- });
299
- return patched;
255
+ }
300
256
  }
301
- function patchViteConfigFile(filePath) {
302
- const source = readFileSync3(filePath, "utf8");
303
- let ast;
304
- try {
305
- ast = parseTs(source, filePath);
306
- } catch {
307
- return { ok: false, error: "parse failed" };
257
+ async function applyLoadedManifest(manifestPath, manifest, entries, duplicateIds, brand, projectRoot, opts) {
258
+ const { targets, skipped } = collectApplyTargets(manifest, entries, duplicateIds);
259
+ const { applied, failed } = await applyTargetsToProject(
260
+ projectRoot,
261
+ targets,
262
+ brand,
263
+ opts.dryRun === true
264
+ );
265
+ return {
266
+ page: manifest.page,
267
+ route: manifest.route,
268
+ manifestPath,
269
+ applied,
270
+ skipped,
271
+ failed
272
+ };
273
+ }
274
+ async function runBrandApplyAll(opts) {
275
+ const manifestPaths = listPccManifestFiles(opts.cwd);
276
+ if (manifestPaths.length === 0) {
277
+ console.error(`No PCC manifests found under ${resolve(opts.cwd)}/nuvio/pages`);
278
+ return 2;
308
279
  }
309
- const depsPatched = ensureOptimizeDepsExclude(ast);
310
- const alreadyPlugin = hasNuvioImport(ast) && hasNuvioPluginCall(ast);
311
- if (alreadyPlugin && !depsPatched) {
312
- return { ok: true, skipped: true };
280
+ const brand = readProjectBrandConfig(opts.cwd);
281
+ let scan;
282
+ try {
283
+ scan = scanProject(opts.cwd);
284
+ } catch (e) {
285
+ if (e instanceof PreflightError) {
286
+ console.error(e.message);
287
+ return 3;
288
+ }
289
+ throw e;
313
290
  }
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")
291
+ const duplicateIds = duplicateIdSet(scan.index.duplicateErrors);
292
+ const pages = [];
293
+ for (const manifestPath of manifestPaths) {
294
+ const loaded = loadPccManifestFromFile(manifestPath);
295
+ if (!loaded.ok) {
296
+ console.error(`Invalid PCC manifest (${manifestPath}): ${loaded.error.message}`);
297
+ return 2;
298
+ }
299
+ pages.push(
300
+ await applyLoadedManifest(
301
+ manifestPath,
302
+ loaded.manifest,
303
+ scan.index.entries,
304
+ duplicateIds,
305
+ brand,
306
+ scan.ctx.root,
307
+ opts
319
308
  )
320
309
  );
321
310
  }
322
- if (!hasNuvioPluginCall(ast)) {
323
- if (!appendNuvioPlugin(ast)) {
324
- return { ok: false, error: "no static plugins array" };
325
- }
311
+ const pass = pages.every((page) => page.failed.length === 0);
312
+ if (opts.json) {
313
+ console.log(JSON.stringify({ pass, dryRun: opts.dryRun === true, pages }, null, 2));
314
+ return pass ? 0 : 1;
326
315
  }
327
- writeFileSync(filePath, printTs(ast, source), "utf8");
328
- return { ok: true, skipped: alreadyPlugin && depsPatched };
316
+ console.log(`Nuvio Brand Apply${opts.dryRun ? " (dry run)" : ""}
317
+ `);
318
+ for (const page of pages) {
319
+ printHumanReport(page);
320
+ console.log("");
321
+ }
322
+ console.log(`Result: ${pass ? "PASS" : "FAIL"}`);
323
+ return pass ? 0 : 1;
329
324
  }
330
- function viteConfigHasNuvio(filePath) {
331
- const source = readFileSync3(filePath, "utf8");
325
+ async function runBrandApply(opts) {
326
+ if (opts.all) {
327
+ return runBrandApplyAll(opts);
328
+ }
329
+ let manifestPath;
332
330
  try {
333
- const ast = parseTs(source, filePath);
334
- return hasNuvioImport(ast) && hasNuvioPluginCall(ast);
335
- } catch {
336
- return /nuvio\s*\(/.test(source);
331
+ manifestPath = resolve(
332
+ resolvePccManifestPath(opts.cwd, { page: opts.page, manifest: opts.manifest })
333
+ );
334
+ } catch (e) {
335
+ console.error(e instanceof Error ? e.message : String(e));
336
+ return 2;
337
337
  }
338
+ const loaded = loadPccManifestFromFile(manifestPath);
339
+ if (!loaded.ok) {
340
+ console.error(`Invalid PCC manifest (${manifestPath}): ${loaded.error.message}`);
341
+ return 2;
342
+ }
343
+ const brand = readProjectBrandConfig(opts.cwd);
344
+ let scan;
345
+ try {
346
+ scan = scanProject(opts.cwd);
347
+ } catch (e) {
348
+ if (e instanceof PreflightError) {
349
+ console.error(e.message);
350
+ return 3;
351
+ }
352
+ throw e;
353
+ }
354
+ const duplicateIds = duplicateIdSet(scan.index.duplicateErrors);
355
+ const result = await applyLoadedManifest(
356
+ manifestPath,
357
+ loaded.manifest,
358
+ scan.index.entries,
359
+ duplicateIds,
360
+ brand,
361
+ scan.ctx.root,
362
+ opts
363
+ );
364
+ const pass = result.failed.length === 0;
365
+ if (opts.json) {
366
+ console.log(JSON.stringify({ pass, dryRun: opts.dryRun === true, ...result }, null, 2));
367
+ return pass ? 0 : 1;
368
+ }
369
+ console.log(`Nuvio Brand Apply${opts.dryRun ? " (dry run)" : ""}
370
+ `);
371
+ printHumanReport(result);
372
+ console.log(`
373
+ Result: ${pass ? "PASS" : "FAIL"}`);
374
+ return pass ? 0 : 1;
338
375
  }
339
376
 
340
- // 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
- var APP_CANDIDATES = [
345
- "src/App.tsx",
346
- "src/App.jsx",
347
- "src/main.tsx",
348
- "src/main.jsx"
349
- ];
350
- function resolveAppFile(root) {
351
- for (const rel of APP_CANDIDATES) {
352
- const p = join3(root, rel);
353
- if (existsSync3(p)) return p;
377
+ // src/brand-scan.ts
378
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
379
+ import { join as join3, resolve as resolve2 } from "path";
380
+ import {
381
+ DEFAULT_BRAND_CONFIG as DEFAULT_BRAND_CONFIG2,
382
+ evaluateBrandPageScan,
383
+ getBrandColorLabel,
384
+ getBrandDensityLabel,
385
+ getBrandRadiusLabel,
386
+ getBrandTypographyLabel,
387
+ normalizeBrandConfig as normalizeBrandConfig2,
388
+ pccCategoryLabel
389
+ } from "@nuvio/shared";
390
+ import {
391
+ listPccManifestFiles as listPccManifestFiles2,
392
+ loadPccManifestFromFile as loadPccManifestFromFile2,
393
+ resolvePccManifestPath as resolvePccManifestPath2
394
+ } from "@nuvio/shared/load-pcc-manifest";
395
+ var BRAND_RELATIVE2 = "nuvio/brand.json";
396
+ function readProjectBrandConfig2(cwd) {
397
+ const filePath = join3(resolve2(cwd), BRAND_RELATIVE2);
398
+ if (!existsSync3(filePath)) {
399
+ return { ...DEFAULT_BRAND_CONFIG2 };
400
+ }
401
+ try {
402
+ const raw = readFileSync3(filePath, "utf8");
403
+ return normalizeBrandConfig2(JSON.parse(raw));
404
+ } catch {
405
+ return { ...DEFAULT_BRAND_CONFIG2 };
354
406
  }
355
- return null;
356
407
  }
357
- function hasOverlayImport(ast) {
358
- let found = false;
359
- babel_traverse_default(ast, {
360
- ImportDeclaration(path) {
361
- if (path.node.source.value === "@nuvio/overlay") found = true;
408
+ function formatBrandSummary(brand) {
409
+ return [
410
+ getBrandColorLabel(brand.color),
411
+ getBrandRadiusLabel(brand.radius),
412
+ getBrandDensityLabel(brand.density),
413
+ getBrandTypographyLabel(brand.typography)
414
+ ].join(" \xB7 ");
415
+ }
416
+ function printHumanReport2(result, manifestPath) {
417
+ console.log("Nuvio Brand Scan\n");
418
+ console.log(`Page: ${result.page}`);
419
+ console.log(`Route: ${result.route}`);
420
+ console.log(`Manifest: ${manifestPath}`);
421
+ console.log(`Saved brand: ${formatBrandSummary(result.brand)}
422
+ `);
423
+ console.log("Category summary:");
424
+ for (const category of result.categories) {
425
+ const label = pccCategoryLabel(category.category).padEnd(10);
426
+ const status = category.pass ? "PASS" : "FAIL";
427
+ console.log(
428
+ `${label} ${status} on-brand ${category.onBrand}/${category.expected} \xB7 off-brand ${category.offBrand} \xB7 no-traits ${category.noTraits}`
429
+ );
430
+ }
431
+ console.log("\nTotals:");
432
+ console.log(`On-brand ${result.onBrandCount}`);
433
+ console.log(`Off-brand ${result.offBrandCount}`);
434
+ console.log(`No traits ${result.noTraitsCount}`);
435
+ console.log(`Missing ${result.missingCount}`);
436
+ const offBrandHosts = result.hosts.filter((host) => host.status === "off_brand");
437
+ if (offBrandHosts.length > 0) {
438
+ console.log("\nOff-brand hosts:");
439
+ for (const host of offBrandHosts.slice(0, 12)) {
440
+ const headline = host.inspect?.headline ?? "Off-brand";
441
+ console.log(`- ${host.hostId} (${host.category}) \u2014 ${headline}`);
362
442
  }
363
- });
364
- return found;
365
- }
366
- function hasDevShell(ast) {
367
- let found = false;
368
- babel_traverse_default(ast, {
369
- JSXElement(path) {
370
- const name = path.node.openingElement.name;
371
- if (t2.isJSXIdentifier(name) && name.name === "NuvioDevShell") found = true;
443
+ if (offBrandHosts.length > 12) {
444
+ console.log(`\u2026and ${offBrandHosts.length - 12} more`);
372
445
  }
373
- });
374
- return found;
375
- }
376
- function unwrapJsx(node) {
377
- if (!node) return null;
378
- if (t2.isJSXElement(node) || t2.isJSXFragment(node)) return node;
379
- if (t2.isParenthesizedExpression(node)) return unwrapJsx(node.expression);
446
+ }
447
+ console.log(`
448
+ Result: ${result.pass ? "PASS" : "FAIL"}`);
449
+ }
450
+ function printAllHumanReport(pages) {
451
+ console.log("Nuvio Brand Scan (all pages)\n");
452
+ for (const page of pages) {
453
+ const status = page.result.pass ? "PASS" : "FAIL";
454
+ const summary = `${page.result.onBrandCount} on-brand \xB7 ${page.result.offBrandCount} off-brand`;
455
+ console.log(`${page.result.page.padEnd(16)} ${status} ${summary}`);
456
+ }
457
+ const pass = pages.every((page) => page.result.pass);
458
+ console.log(`
459
+ Result: ${pass ? "PASS" : "FAIL"}`);
460
+ }
461
+ function scanLoadedManifest(manifestPath, entries, brand) {
462
+ const loaded = loadPccManifestFromFile2(manifestPath);
463
+ if (!loaded.ok) {
464
+ return {
465
+ ok: false,
466
+ code: 2,
467
+ message: `Invalid PCC manifest (${manifestPath}): ${loaded.error.message}`
468
+ };
469
+ }
470
+ return {
471
+ ok: true,
472
+ result: evaluateBrandPageScan(loaded.manifest, entries, brand)
473
+ };
474
+ }
475
+ function runBrandScanAll(opts) {
476
+ const manifestPaths = listPccManifestFiles2(opts.cwd);
477
+ if (manifestPaths.length === 0) {
478
+ console.error(`No PCC manifests found under ${resolve2(opts.cwd)}/nuvio/pages`);
479
+ return 2;
480
+ }
481
+ const brand = readProjectBrandConfig2(opts.cwd);
482
+ let scan;
483
+ try {
484
+ scan = scanProject(opts.cwd);
485
+ } catch (e) {
486
+ if (e instanceof PreflightError) {
487
+ console.error(e.message);
488
+ return 3;
489
+ }
490
+ throw e;
491
+ }
492
+ const pages = [];
493
+ for (const manifestPath of manifestPaths) {
494
+ const scanned = scanLoadedManifest(manifestPath, scan.index.entries, brand);
495
+ if (!scanned.ok) {
496
+ console.error(scanned.message);
497
+ return scanned.code;
498
+ }
499
+ pages.push({ manifestPath, result: scanned.result });
500
+ }
501
+ const pass = pages.every((page) => page.result.pass);
502
+ if (opts.json) {
503
+ console.log(JSON.stringify({ pass, brand, pages }, null, 2));
504
+ return pass ? 0 : 1;
505
+ }
506
+ printAllHumanReport(pages);
507
+ return pass ? 0 : 1;
508
+ }
509
+ function runBrandScan(opts) {
510
+ if (opts.all) {
511
+ return runBrandScanAll(opts);
512
+ }
513
+ let manifestPath;
514
+ try {
515
+ manifestPath = resolve2(
516
+ resolvePccManifestPath2(opts.cwd, { page: opts.page, manifest: opts.manifest })
517
+ );
518
+ } catch (e) {
519
+ console.error(e instanceof Error ? e.message : String(e));
520
+ return 2;
521
+ }
522
+ const brand = readProjectBrandConfig2(opts.cwd);
523
+ let scan;
524
+ try {
525
+ scan = scanProject(opts.cwd);
526
+ } catch (e) {
527
+ if (e instanceof PreflightError) {
528
+ console.error(e.message);
529
+ return 3;
530
+ }
531
+ throw e;
532
+ }
533
+ const scanned = scanLoadedManifest(manifestPath, scan.index.entries, brand);
534
+ if (!scanned.ok) {
535
+ console.error(scanned.message);
536
+ return scanned.code;
537
+ }
538
+ if (opts.json) {
539
+ console.log(
540
+ JSON.stringify(
541
+ {
542
+ manifestPath,
543
+ ...scanned.result
544
+ },
545
+ null,
546
+ 2
547
+ )
548
+ );
549
+ return scanned.result.pass ? 0 : 1;
550
+ }
551
+ printHumanReport2(scanned.result, manifestPath);
552
+ return scanned.result.pass ? 0 : 1;
553
+ }
554
+
555
+ // src/coverage-verify.ts
556
+ import { resolve as resolve3 } from "path";
557
+ import {
558
+ evaluatePageCoverage,
559
+ pccCategoryLabel as pccCategoryLabel2
560
+ } from "@nuvio/shared";
561
+ import {
562
+ listPccManifestFiles as listPccManifestFiles3,
563
+ loadPccManifestFromFile as loadPccManifestFromFile3,
564
+ resolvePccManifestPath as resolvePccManifestPath3
565
+ } from "@nuvio/shared/load-pcc-manifest";
566
+ function formatCategoryLine(summary) {
567
+ const label = pccCategoryLabel2(summary.category).padEnd(10);
568
+ const status = summary.pass ? "PASS" : "FAIL";
569
+ return `${label} ${status} ${summary.indexed}/${summary.expected}`;
570
+ }
571
+ function printHumanReport3(result, manifestPath) {
572
+ console.log("Nuvio Coverage Report\n");
573
+ console.log(`Page: ${result.page}`);
574
+ console.log(`Route: ${result.route}`);
575
+ console.log(`Manifest: ${manifestPath}
576
+ `);
577
+ console.log("Category summary:");
578
+ for (const category of result.categories) {
579
+ console.log(formatCategoryLine(category));
580
+ }
581
+ console.log("\nCoverage gates:");
582
+ const g = result.gates;
583
+ console.log(`Indexed ${g.indexed}/${g.expected}`);
584
+ console.log(`Patchable ${g.patchable}/${g.expected}`);
585
+ console.log(`Categorized ${g.categorized}/${g.expected}`);
586
+ console.log(`Editable ${g.editable}/${g.expected}`);
587
+ console.log(`Brandable ${g.brandable}/${g.expected}`);
588
+ console.log("\nBrandability:");
589
+ console.log(`Brandable ${result.brandableCount}`);
590
+ console.log(`Editable-only ${result.editableOnlyCount}`);
591
+ const missing = result.issues.filter((i) => i.kind === "missing");
592
+ const unpatchable = result.issues.filter((i) => i.kind === "unpatchable");
593
+ const duplicates = result.issues.filter((i) => i.kind === "duplicate_id");
594
+ if (missing.length > 0) {
595
+ console.log("\nMissing hosts:");
596
+ for (const issue of missing) {
597
+ console.log(`- ${issue.hostId} (${issue.category})`);
598
+ }
599
+ }
600
+ if (unpatchable.length > 0) {
601
+ console.log("\nUnpatchable hosts:");
602
+ for (const issue of unpatchable) {
603
+ console.log(`- ${issue.hostId}`);
604
+ if (issue.reason) {
605
+ console.log(` reason: ${issue.reason}`);
606
+ }
607
+ }
608
+ }
609
+ if (duplicates.length > 0) {
610
+ console.log("\nDuplicate id hosts:");
611
+ for (const issue of duplicates) {
612
+ console.log(`- ${issue.hostId}`);
613
+ }
614
+ }
615
+ console.log(`
616
+ Result: ${result.pass ? "PASS" : "FAIL"}`);
617
+ }
618
+ function printAllHumanReport2(summary) {
619
+ console.log("Nuvio Coverage Report (all pages)\n");
620
+ for (const entry of summary.pages) {
621
+ const status = entry.result.pass ? "PASS" : "FAIL";
622
+ console.log(`${entry.result.page.padEnd(16)} ${status} ${entry.manifestPath}`);
623
+ }
624
+ console.log(`
625
+ Result: ${summary.pass ? "PASS" : "FAIL"}`);
626
+ }
627
+ function verifyLoadedManifest(manifestPath, entries, duplicateErrors) {
628
+ const loaded = loadPccManifestFromFile3(manifestPath);
629
+ if (!loaded.ok) {
630
+ return {
631
+ ok: false,
632
+ code: 2,
633
+ message: `Invalid PCC manifest (${manifestPath}): ${loaded.error.message}`
634
+ };
635
+ }
636
+ const result = evaluatePageCoverage(loaded.manifest, entries, duplicateErrors);
637
+ return { ok: true, result };
638
+ }
639
+ function runCoverageVerifyAll(opts) {
640
+ const manifestPaths = listPccManifestFiles3(opts.cwd);
641
+ if (manifestPaths.length === 0) {
642
+ console.error(`No PCC manifests found under ${resolve3(opts.cwd)}/nuvio/pages`);
643
+ return 2;
644
+ }
645
+ let scan;
646
+ try {
647
+ scan = scanProject(opts.cwd);
648
+ } catch (e) {
649
+ if (e instanceof PreflightError) {
650
+ console.error(e.message);
651
+ return 3;
652
+ }
653
+ throw e;
654
+ }
655
+ const pages = [];
656
+ for (const manifestPath of manifestPaths) {
657
+ const verified = verifyLoadedManifest(
658
+ manifestPath,
659
+ scan.index.entries,
660
+ scan.index.duplicateErrors
661
+ );
662
+ if (!verified.ok) {
663
+ console.error(verified.message);
664
+ return verified.code;
665
+ }
666
+ pages.push({ manifestPath, result: verified.result });
667
+ }
668
+ const summary = {
669
+ pass: pages.every((page) => page.result.pass),
670
+ pages
671
+ };
672
+ if (opts.json) {
673
+ console.log(JSON.stringify(summary, null, 2));
674
+ return summary.pass ? 0 : 1;
675
+ }
676
+ printAllHumanReport2(summary);
677
+ return summary.pass ? 0 : 1;
678
+ }
679
+ function runCoverageVerify(opts) {
680
+ if (opts.all) {
681
+ return runCoverageVerifyAll(opts);
682
+ }
683
+ let manifestPath;
684
+ try {
685
+ manifestPath = resolve3(
686
+ resolvePccManifestPath3(opts.cwd, { page: opts.page, manifest: opts.manifest })
687
+ );
688
+ } catch (e) {
689
+ console.error(e instanceof Error ? e.message : String(e));
690
+ return 2;
691
+ }
692
+ let scan;
693
+ try {
694
+ scan = scanProject(opts.cwd);
695
+ } catch (e) {
696
+ if (e instanceof PreflightError) {
697
+ console.error(e.message);
698
+ return 3;
699
+ }
700
+ throw e;
701
+ }
702
+ const verified = verifyLoadedManifest(
703
+ manifestPath,
704
+ scan.index.entries,
705
+ scan.index.duplicateErrors
706
+ );
707
+ if (!verified.ok) {
708
+ console.error(verified.message);
709
+ return verified.code;
710
+ }
711
+ if (opts.json) {
712
+ console.log(
713
+ JSON.stringify(
714
+ {
715
+ manifestPath,
716
+ ...verified.result
717
+ },
718
+ null,
719
+ 2
720
+ )
721
+ );
722
+ return verified.result.pass ? 0 : 1;
723
+ }
724
+ printHumanReport3(verified.result, manifestPath);
725
+ return verified.result.pass ? 0 : 1;
726
+ }
727
+
728
+ // src/detect-pm.ts
729
+ import { existsSync as existsSync4 } from "fs";
730
+ import { join as join4 } from "path";
731
+ function detectPackageManager(root, override) {
732
+ if (override) return override;
733
+ if (existsSync4(join4(root, "pnpm-lock.yaml"))) return "pnpm";
734
+ if (existsSync4(join4(root, "package-lock.json"))) return "npm";
735
+ if (existsSync4(join4(root, "yarn.lock"))) return "yarn";
736
+ if (existsSync4(join4(root, "bun.lockb")) || existsSync4(join4(root, "bun.lock")))
737
+ return "bun";
738
+ return "npm";
739
+ }
740
+ function installCommand(pm, version) {
741
+ const pkgs = `@nuvio/vite-plugin@${version} @nuvio/overlay@${version}`;
742
+ switch (pm) {
743
+ case "pnpm":
744
+ return `pnpm add -D ${pkgs}`;
745
+ case "yarn":
746
+ return `yarn add -D ${pkgs}`;
747
+ case "bun":
748
+ return `bun add -d ${pkgs}`;
749
+ default:
750
+ return `npm install -D ${pkgs}`;
751
+ }
752
+ }
753
+
754
+ // src/nuvio-deps.ts
755
+ import { readFileSync as readFileSync4 } from "fs";
756
+ function readPackageJson(packageJsonPath) {
757
+ return JSON.parse(readFileSync4(packageJsonPath, "utf8"));
758
+ }
759
+ function getDependencyVersion(pkg, name) {
760
+ const deps = pkg.dependencies;
761
+ const devDeps = pkg.devDependencies;
762
+ return deps?.[name] ?? devDeps?.[name];
763
+ }
764
+ function hasNuvioDependency(pkg, name) {
765
+ return Boolean(getDependencyVersion(pkg, name));
766
+ }
767
+ function hasNuvioPackages(pkg) {
768
+ return hasNuvioDependency(pkg, "@nuvio/vite-plugin") && hasNuvioDependency(pkg, "@nuvio/overlay");
769
+ }
770
+ function isWorkspaceLinkedVersion(version) {
771
+ if (!version) return false;
772
+ return version.startsWith("workspace:") || version.startsWith("link:") || version.startsWith("file:");
773
+ }
774
+ function nuvioOverlayLinkKind(pkg) {
775
+ const raw = getDependencyVersion(pkg, "@nuvio/overlay");
776
+ if (!raw) return "missing";
777
+ return isWorkspaceLinkedVersion(raw) ? "workspace" : "npm";
778
+ }
779
+
780
+ // src/telemetry.ts
781
+ import { mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
782
+ import { homedir } from "os";
783
+ import { join as join5 } from "path";
784
+ import { randomUUID } from "crypto";
785
+ import os from "os";
786
+ import { PostHog } from "posthog-node";
787
+
788
+ // src/nuvio-posthog-token.ts
789
+ var NUVIO_POSTHOG_TOKEN = "phc_CJnWrLU4hB4aA88DJrPnma2WBMQqVHxUMVvrsye3R6x2";
790
+
791
+ // src/version.ts
792
+ import { createRequire } from "module";
793
+ var require2 = createRequire(import.meta.url);
794
+ var NUVIO_VERSION = require2("../package.json").version;
795
+
796
+ // src/telemetry.ts
797
+ var POSTHOG_HOST = "https://us.i.posthog.com";
798
+ function telemetryFilePath() {
799
+ return join5(homedir(), ".nuvio", "telemetry.json");
800
+ }
801
+ var FORBIDDEN_PROP_KEYS = /* @__PURE__ */ new Set([
802
+ "cwd",
803
+ "root",
804
+ "file",
805
+ "path",
806
+ "name",
807
+ "message",
808
+ "stack"
809
+ ]);
810
+ var SHUTDOWN_TIMEOUT_MS = 3e3;
811
+ var client = null;
812
+ var sessionAnonymousId = null;
813
+ var shutdownDone = false;
814
+ var signalHandlersRegistered = false;
815
+ function telemetryDebug(message, detail) {
816
+ if (process.env.NUVIO_TELEMETRY_DEBUG !== "1") return;
817
+ if (detail !== void 0) {
818
+ console.error(`[nuvio telemetry] ${message}`, detail);
819
+ return;
820
+ }
821
+ console.error(`[nuvio telemetry] ${message}`);
822
+ }
823
+ function isTelemetryEnabled() {
824
+ const flag = process.env.NUVIO_TELEMETRY;
825
+ if (flag === "0") return false;
826
+ if (flag?.toLowerCase() === "false") return false;
827
+ return true;
828
+ }
829
+ function posthogToken() {
830
+ return process.env.NUVIO_POSTHOG_TOKEN ?? NUVIO_POSTHOG_TOKEN;
831
+ }
832
+ function tokenIsConfigured(token) {
833
+ return Boolean(token && token.startsWith("phc_"));
834
+ }
835
+ function readOrCreateAnonymousId() {
836
+ if (sessionAnonymousId) return sessionAnonymousId;
837
+ try {
838
+ const raw = readFileSync5(telemetryFilePath(), "utf8");
839
+ const parsed = JSON.parse(raw);
840
+ if (parsed.anonymousId) {
841
+ sessionAnonymousId = parsed.anonymousId;
842
+ return parsed.anonymousId;
843
+ }
844
+ } catch {
845
+ }
846
+ const id = randomUUID();
847
+ sessionAnonymousId = id;
848
+ try {
849
+ mkdirSync(join5(homedir(), ".nuvio"), { recursive: true, mode: 448 });
850
+ writeFileSync2(
851
+ telemetryFilePath(),
852
+ JSON.stringify({ anonymousId: id }, null, 2),
853
+ { mode: 384 }
854
+ );
855
+ } catch {
856
+ }
857
+ return id;
858
+ }
859
+ function getClient() {
860
+ if (!isTelemetryEnabled()) return null;
861
+ const token = posthogToken();
862
+ if (!tokenIsConfigured(token)) return null;
863
+ if (!client) {
864
+ client = new PostHog(token, {
865
+ host: POSTHOG_HOST,
866
+ flushAt: 1,
867
+ flushInterval: 0
868
+ });
869
+ telemetryDebug("PostHog client initialized", {
870
+ host: POSTHOG_HOST,
871
+ tokenPrefix: `${token.slice(0, 8)}\u2026`
872
+ });
873
+ }
874
+ return client;
875
+ }
876
+ function sanitizeProps(props) {
877
+ if (!props) return void 0;
878
+ const out = {};
879
+ for (const [key, value] of Object.entries(props)) {
880
+ if (FORBIDDEN_PROP_KEYS.has(key)) continue;
881
+ if (value === void 0) continue;
882
+ if (typeof value === "string" && /[/\\]/.test(value)) continue;
883
+ out[key] = value;
884
+ }
885
+ return Object.keys(out).length > 0 ? out : void 0;
886
+ }
887
+ function resolveCliInvokedCommand(help, command) {
888
+ if (help) return "help";
889
+ if (!command) return "none";
890
+ if (command === "init") return "init";
891
+ if (command === "doctor") return "doctor";
892
+ if (command === "scan") return "scan";
893
+ if (command === "stats") return "stats";
894
+ return "unknown";
895
+ }
896
+ function buildCliInvokedProps(command, pmOverride) {
897
+ const props = {
898
+ nuvio_version: NUVIO_VERSION,
899
+ os: process.platform,
900
+ arch: os.arch(),
901
+ node: process.version,
902
+ command
903
+ };
904
+ if (pmOverride) props.package_manager = pmOverride;
905
+ return props;
906
+ }
907
+ function buildCliTelemetryProps(pm, project) {
908
+ const props = {
909
+ nuvio_version: NUVIO_VERSION,
910
+ os: process.platform,
911
+ arch: os.arch(),
912
+ node: process.version
913
+ };
914
+ if (pm) props.package_manager = pm;
915
+ if (project) {
916
+ props.has_react = true;
917
+ props.has_vite = true;
918
+ props.has_tailwind = project.tailwindOk;
919
+ }
920
+ return props;
921
+ }
922
+ function preflightErrorCode(message) {
923
+ if (message === MSG.noPackageJson) return "preflight_no_package_json";
924
+ if (message === MSG.noVite) return "preflight_no_vite";
925
+ if (message === MSG.noReact) return "preflight_no_react";
926
+ if (message === MSG.noViteDep) return "preflight_no_vite_dep";
927
+ if (message === MSG.monorepoRoot || message === MSG.cliPackage) {
928
+ return "preflight_monorepo";
929
+ }
930
+ return "preflight_unknown";
931
+ }
932
+ function captureCliInvoked(command, pmOverride) {
933
+ captureCliEvent("nuvio_cli_invoked", buildCliInvokedProps(command, pmOverride));
934
+ }
935
+ function captureCliEvent(event, props) {
936
+ try {
937
+ if (!isTelemetryEnabled()) {
938
+ telemetryDebug(`skipped ${event} (telemetry disabled)`);
939
+ return;
940
+ }
941
+ const ph = getClient();
942
+ if (!ph) {
943
+ telemetryDebug(`skipped ${event} (no PostHog client \u2014 check token)`);
944
+ return;
945
+ }
946
+ const distinctId = readOrCreateAnonymousId();
947
+ ph.capture({
948
+ distinctId,
949
+ event,
950
+ properties: sanitizeProps(props)
951
+ });
952
+ telemetryDebug(`captured ${event}`, { distinctId });
953
+ } catch (error) {
954
+ telemetryDebug(`capture failed for ${event}`, error);
955
+ }
956
+ }
957
+ async function flushAndShutdownClient() {
958
+ if (!client) return;
959
+ const active = client;
960
+ client = null;
961
+ await Promise.race([
962
+ (async () => {
963
+ await active.flush();
964
+ await active.shutdown();
965
+ })(),
966
+ new Promise((_, reject) => {
967
+ setTimeout(
968
+ () => reject(new Error("telemetry shutdown timed out")),
969
+ SHUTDOWN_TIMEOUT_MS
970
+ );
971
+ })
972
+ ]);
973
+ }
974
+ async function shutdownTelemetry() {
975
+ if (shutdownDone) return;
976
+ shutdownDone = true;
977
+ try {
978
+ await flushAndShutdownClient();
979
+ telemetryDebug("flush + shutdown complete");
980
+ } catch (error) {
981
+ telemetryDebug("shutdown failed", error);
982
+ }
983
+ }
984
+ function registerTelemetrySignalHandlers() {
985
+ if (signalHandlersRegistered) return;
986
+ signalHandlersRegistered = true;
987
+ const onSignal = (signal) => {
988
+ void (async () => {
989
+ await shutdownTelemetry();
990
+ const code2 = signal === "SIGINT" ? 130 : 143;
991
+ process.exit(code2);
992
+ })();
993
+ };
994
+ process.once("SIGINT", onSignal);
995
+ process.once("SIGTERM", onSignal);
996
+ }
997
+
998
+ // src/babel-traverse.ts
999
+ import traverseImport from "@babel/traverse";
1000
+ var traverse = typeof traverseImport === "function" ? traverseImport : traverseImport.default;
1001
+ var babel_traverse_default = traverse;
1002
+
1003
+ // src/patch-app-root.ts
1004
+ import * as t from "@babel/types";
1005
+ import fg from "fast-glob";
1006
+ import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
1007
+ import { join as join6 } from "path";
1008
+
1009
+ // src/babel-generator.ts
1010
+ import generateImport from "@babel/generator";
1011
+ var generate = typeof generateImport === "function" ? generateImport : generateImport.default;
1012
+ var babel_generator_default = generate;
1013
+
1014
+ // src/parse-ts.ts
1015
+ import { parse } from "@babel/parser";
1016
+ var PARSE_OPTS = {
1017
+ sourceType: "module",
1018
+ plugins: ["typescript", "jsx"]
1019
+ };
1020
+ function parseTs(source, filename = "file.tsx") {
1021
+ return parse(source, {
1022
+ ...PARSE_OPTS,
1023
+ sourceFilename: filename
1024
+ });
1025
+ }
1026
+ function printTs(ast, source) {
1027
+ const out = babel_generator_default(ast, { retainLines: true }, source);
1028
+ return out.code.endsWith("\n") ? out.code : `${out.code}
1029
+ `;
1030
+ }
1031
+
1032
+ // src/patch-app-root.ts
1033
+ var APP_CANDIDATES = [
1034
+ "src/App.tsx",
1035
+ "src/App.jsx",
1036
+ "src/main.tsx",
1037
+ "src/main.jsx"
1038
+ ];
1039
+ function resolveAppFile(root) {
1040
+ for (const rel of APP_CANDIDATES) {
1041
+ const p = join6(root, rel);
1042
+ if (existsSync5(p)) return p;
1043
+ }
1044
+ return null;
1045
+ }
1046
+ function hasOverlayImport(ast) {
1047
+ let found = false;
1048
+ babel_traverse_default(ast, {
1049
+ ImportDeclaration(path) {
1050
+ if (path.node.source.value === "@nuvio/overlay") found = true;
1051
+ }
1052
+ });
1053
+ return found;
1054
+ }
1055
+ function hasDevShell(ast) {
1056
+ let found = false;
1057
+ babel_traverse_default(ast, {
1058
+ JSXElement(path) {
1059
+ const name = path.node.openingElement.name;
1060
+ if (t.isJSXIdentifier(name) && name.name === "NuvioDevShell") found = true;
1061
+ }
1062
+ });
1063
+ return found;
1064
+ }
1065
+ function unwrapJsx(node) {
1066
+ if (!node) return null;
1067
+ if (t.isJSXElement(node) || t.isJSXFragment(node)) return node;
1068
+ if (t.isParenthesizedExpression(node)) return unwrapJsx(node.expression);
380
1069
  return null;
381
1070
  }
382
- var devShellElement = t2.jsxElement(
383
- t2.jsxOpeningElement(t2.jsxIdentifier("NuvioDevShell"), [], true),
1071
+ var devShellElement = t.jsxElement(
1072
+ t.jsxOpeningElement(t.jsxIdentifier("NuvioDevShell"), [], true),
384
1073
  null,
385
1074
  [],
386
1075
  true
@@ -391,15 +1080,15 @@ function appendDevShell(ast) {
391
1080
  ReturnStatement(path) {
392
1081
  const jsx = unwrapJsx(path.node.argument);
393
1082
  if (!jsx) return;
394
- if (t2.isJSXFragment(jsx)) {
395
- jsx.children.push(t2.jsxText("\n "));
1083
+ if (t.isJSXFragment(jsx)) {
1084
+ jsx.children.push(t.jsxText("\n "));
396
1085
  jsx.children.push(devShellElement);
397
1086
  patched = true;
398
1087
  } else {
399
- path.node.argument = t2.jsxFragment(
400
- t2.jsxOpeningFragment(),
401
- t2.jsxClosingFragment(),
402
- [jsx, t2.jsxText("\n "), devShellElement]
1088
+ path.node.argument = t.jsxFragment(
1089
+ t.jsxOpeningFragment(),
1090
+ t.jsxClosingFragment(),
1091
+ [jsx, t.jsxText("\n "), devShellElement]
403
1092
  );
404
1093
  patched = true;
405
1094
  }
@@ -408,7 +1097,7 @@ function appendDevShell(ast) {
408
1097
  return patched;
409
1098
  }
410
1099
  function patchAppRootFile(filePath) {
411
- const source = readFileSync4(filePath, "utf8");
1100
+ const source = readFileSync6(filePath, "utf8");
412
1101
  let ast;
413
1102
  try {
414
1103
  ast = parseTs(source, filePath);
@@ -420,26 +1109,26 @@ function patchAppRootFile(filePath) {
420
1109
  }
421
1110
  if (!hasOverlayImport(ast)) {
422
1111
  ast.program.body.unshift(
423
- t2.importDeclaration(
1112
+ t.importDeclaration(
424
1113
  [
425
- t2.importSpecifier(
426
- t2.identifier("NuvioDevShell"),
427
- t2.identifier("NuvioDevShell")
1114
+ t.importSpecifier(
1115
+ t.identifier("NuvioDevShell"),
1116
+ t.identifier("NuvioDevShell")
428
1117
  )
429
1118
  ],
430
- t2.stringLiteral("@nuvio/overlay")
1119
+ t.stringLiteral("@nuvio/overlay")
431
1120
  )
432
1121
  );
433
1122
  }
434
1123
  if (!hasDevShell(ast) && !appendDevShell(ast)) {
435
1124
  return { ok: false, error: "no JSX return to patch" };
436
1125
  }
437
- writeFileSync2(filePath, printTs(ast, source), "utf8");
1126
+ writeFileSync3(filePath, printTs(ast, source), "utf8");
438
1127
  return { ok: true };
439
1128
  }
440
1129
  function appHasDevShell(filePath) {
441
- if (!existsSync3(filePath)) return false;
442
- const source = readFileSync4(filePath, "utf8");
1130
+ if (!existsSync5(filePath)) return false;
1131
+ const source = readFileSync6(filePath, "utf8");
443
1132
  try {
444
1133
  const ast = parseTs(source, filePath);
445
1134
  return hasOverlayImport(ast) && hasDevShell(ast);
@@ -447,32 +1136,42 @@ function appHasDevShell(filePath) {
447
1136
  return /NuvioDevShell/.test(source);
448
1137
  }
449
1138
  }
1139
+ function projectHasDevShell(root) {
1140
+ const appFile = resolveAppFile(root);
1141
+ if (appFile && appHasDevShell(appFile)) return true;
1142
+ const files = fg.sync(["src/**/*.{tsx,jsx}"], {
1143
+ cwd: root,
1144
+ absolute: true,
1145
+ onlyFiles: true
1146
+ });
1147
+ for (const file of files) {
1148
+ if (appHasDevShell(file)) return true;
1149
+ }
1150
+ return false;
1151
+ }
450
1152
 
451
1153
  // src/patch-main-styles.ts
452
- import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
453
- import { join as join4 } from "path";
1154
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
1155
+ import { join as join7 } from "path";
454
1156
  var MAIN_CANDIDATES = ["src/main.tsx", "src/main.jsx", "main.tsx", "main.jsx"];
455
1157
  var STYLE_IMPORT = 'import "@nuvio/overlay/style.css";';
456
1158
  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:");
1159
+ const pkg = readPackageJson(packageJsonPath);
1160
+ return nuvioOverlayLinkKind(pkg) === "npm";
462
1161
  }
463
1162
  function resolveMainEntry(root) {
464
1163
  for (const rel of MAIN_CANDIDATES) {
465
- const p = join4(root, rel);
466
- if (existsSync4(p)) return p;
1164
+ const p = join7(root, rel);
1165
+ if (existsSync6(p)) return p;
467
1166
  }
468
1167
  return null;
469
1168
  }
470
1169
  function mainHasOverlayStyles(mainPath) {
471
- const text = readFileSync5(mainPath, "utf8");
1170
+ const text = readFileSync7(mainPath, "utf8");
472
1171
  return text.includes("@nuvio/overlay/style.css") || text.includes("@nuvio/overlay/dist/style.css");
473
1172
  }
474
1173
  function patchMainOverlayStyles(mainPath) {
475
- const text = readFileSync5(mainPath, "utf8");
1174
+ const text = readFileSync7(mainPath, "utf8");
476
1175
  if (text.includes("@nuvio/overlay/style.css") || text.includes("@nuvio/overlay/dist/style.css")) {
477
1176
  return { ok: true, skipped: true };
478
1177
  }
@@ -486,209 +1185,212 @@ function patchMainOverlayStyles(mainPath) {
486
1185
  } else {
487
1186
  lines.unshift(STYLE_IMPORT, "");
488
1187
  }
489
- writeFileSync3(mainPath, lines.join("\n"));
1188
+ writeFileSync4(mainPath, lines.join("\n"));
490
1189
  return { ok: true };
491
1190
  }
492
1191
 
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;
1192
+ // src/patch-vite-config.ts
1193
+ import * as t2 from "@babel/types";
1194
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
1195
+ function hasNuvioImport(ast) {
1196
+ let found = false;
1197
+ babel_traverse_default(ast, {
1198
+ ImportDeclaration(path) {
1199
+ if (path.node.source.value === "@nuvio/vite-plugin") found = true;
508
1200
  }
509
- }
510
- return false;
1201
+ });
1202
+ return found;
511
1203
  }
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);
1204
+ function hasNuvioPluginCall(ast) {
1205
+ let found = false;
1206
+ babel_traverse_default(ast, {
1207
+ CallExpression(path) {
1208
+ if (t2.isIdentifier(path.node.callee, { name: "nuvio" })) found = true;
527
1209
  }
528
- }
529
- for (const f of files) {
530
- if (!seen.has(f)) out.push(f);
531
- }
532
- return out;
1210
+ });
1211
+ return found;
533
1212
  }
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
- }
1213
+ var OVERLAY_DEP = "@nuvio/overlay";
1214
+ function excludeListsOverlay(expr) {
1215
+ if (!expr || !t2.isArrayExpression(expr)) return false;
1216
+ return expr.elements.some(
1217
+ (el) => t2.isStringLiteral(el) && el.value === OVERLAY_DEP
1218
+ );
1219
+ }
1220
+ function ensureOptimizeDepsExclude(ast) {
544
1221
  let patched = false;
545
1222
  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;
1223
+ CallExpression(path) {
1224
+ const callee = path.node.callee;
1225
+ if (!t2.isIdentifier(callee, { name: "defineConfig" })) return;
1226
+ const arg = path.node.arguments[0];
1227
+ if (!t2.isObjectExpression(arg)) return;
1228
+ let optimizeDeps;
1229
+ for (const prop of arg.properties) {
1230
+ if (t2.isObjectProperty(prop) && t2.isIdentifier(prop.key, { name: "optimizeDeps" })) {
1231
+ optimizeDeps = prop;
1232
+ break;
554
1233
  }
555
1234
  }
556
- path.node.attributes.push(
557
- t3.jsxAttribute(
558
- t3.jsxIdentifier("data-nuvio-id"),
559
- t3.stringLiteral("page.title")
560
- )
561
- );
1235
+ if (!optimizeDeps) {
1236
+ arg.properties.push(
1237
+ t2.objectProperty(
1238
+ t2.identifier("optimizeDeps"),
1239
+ t2.objectExpression([
1240
+ t2.objectProperty(
1241
+ t2.identifier("exclude"),
1242
+ t2.arrayExpression([t2.stringLiteral(OVERLAY_DEP)])
1243
+ )
1244
+ ])
1245
+ )
1246
+ );
1247
+ patched = true;
1248
+ return;
1249
+ }
1250
+ if (!t2.isObjectExpression(optimizeDeps.value)) return;
1251
+ let excludeProp;
1252
+ for (const p of optimizeDeps.value.properties) {
1253
+ if (t2.isObjectProperty(p) && t2.isIdentifier(p.key, { name: "exclude" })) {
1254
+ excludeProp = p;
1255
+ break;
1256
+ }
1257
+ }
1258
+ if (!excludeProp) {
1259
+ optimizeDeps.value.properties.push(
1260
+ t2.objectProperty(
1261
+ t2.identifier("exclude"),
1262
+ t2.arrayExpression([t2.stringLiteral(OVERLAY_DEP)])
1263
+ )
1264
+ );
1265
+ patched = true;
1266
+ return;
1267
+ }
1268
+ if (t2.isArrayExpression(excludeProp.value) && !excludeListsOverlay(excludeProp.value)) {
1269
+ excludeProp.value.elements.push(t2.stringLiteral(OVERLAY_DEP));
1270
+ patched = true;
1271
+ }
1272
+ }
1273
+ });
1274
+ return patched;
1275
+ }
1276
+ function viteConfigHasOverlayOptimizeExclude(filePath) {
1277
+ const source = readFileSync8(filePath, "utf8");
1278
+ return /optimizeDeps\s*:\s*\{[^}]*exclude\s*:\s*\[[^\]]*@nuvio\/overlay/.test(
1279
+ source
1280
+ ) || /exclude\s*:\s*\[[^\]]*["']@nuvio\/overlay["']/.test(source);
1281
+ }
1282
+ function appendNuvioPlugin(ast) {
1283
+ let patched = false;
1284
+ babel_traverse_default(ast, {
1285
+ ObjectProperty(path) {
1286
+ if (!t2.isIdentifier(path.node.key, { name: "plugins" })) return;
1287
+ if (!t2.isArrayExpression(path.node.value)) return;
1288
+ path.node.value.elements.push(t2.callExpression(t2.identifier("nuvio"), []));
562
1289
  patched = true;
563
1290
  }
564
1291
  });
565
- if (!patched) return { ok: false, error: "no h1/h2" };
566
- writeFileSync4(filePath, printTs(ast, source), "utf8");
567
- return { ok: true };
1292
+ return patched;
568
1293
  }
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
- }
1294
+ function patchViteConfigFile(filePath) {
1295
+ const source = readFileSync8(filePath, "utf8");
1296
+ let ast;
1297
+ try {
1298
+ ast = parseTs(source, filePath);
1299
+ } catch {
1300
+ return { ok: false, error: "parse failed" };
1301
+ }
1302
+ const depsPatched = ensureOptimizeDepsExclude(ast);
1303
+ const alreadyPlugin = hasNuvioImport(ast) && hasNuvioPluginCall(ast);
1304
+ if (alreadyPlugin && !depsPatched) {
1305
+ return { ok: true, skipped: true };
1306
+ }
1307
+ if (!hasNuvioImport(ast)) {
1308
+ ast.program.body.unshift(
1309
+ t2.importDeclaration(
1310
+ [t2.importSpecifier(t2.identifier("nuvio"), t2.identifier("nuvio"))],
1311
+ t2.stringLiteral("@nuvio/vite-plugin")
1312
+ )
1313
+ );
1314
+ }
1315
+ if (!hasNuvioPluginCall(ast)) {
1316
+ if (!appendNuvioPlugin(ast)) {
1317
+ return { ok: false, error: "no static plugins array" };
588
1318
  }
589
- const outcome = patchFirstHeading(file);
590
- if (outcome.ok) return { outcome, file };
591
1319
  }
592
- return { outcome: { ok: false, error: "no heading" } };
593
- }
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
- };
1320
+ writeFileSync5(filePath, printTs(ast, source), "utf8");
1321
+ return { ok: true, skipped: alreadyPlugin && depsPatched };
609
1322
  }
610
-
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");
1323
+ function viteConfigHasNuvio(filePath) {
1324
+ const source = readFileSync8(filePath, "utf8");
1325
+ try {
1326
+ const ast = parseTs(source, filePath);
1327
+ return hasNuvioImport(ast) && hasNuvioPluginCall(ast);
1328
+ } catch {
1329
+ return /nuvio\s*\(/.test(source);
1330
+ }
623
1331
  }
624
- function render(tpl, vars) {
625
- let out = tpl;
626
- for (const [key, value] of Object.entries(vars)) {
627
- out = out.replaceAll(`{{${key}}}`, value);
1332
+
1333
+ // src/scan-ids.ts
1334
+ import { readFileSync as readFileSync9 } from "fs";
1335
+ import { join as join8 } from "path";
1336
+ import fg2 from "fast-glob";
1337
+ var ID_GLOB = ["src/**/*.{tsx,jsx}"];
1338
+ function projectHasPageTitleId(root) {
1339
+ const files = fg2.sync(ID_GLOB, { cwd: root, absolute: true });
1340
+ for (const file of files) {
1341
+ const text = readFileSync9(file, "utf8");
1342
+ if (/data-nuvio-id=["']page\.title["']/.test(text)) {
1343
+ return true;
1344
+ }
628
1345
  }
629
- return out;
1346
+ return false;
630
1347
  }
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");
1348
+ function findHeadingFiles(root) {
1349
+ const files = fg2.sync(ID_GLOB, { cwd: root, absolute: true });
1350
+ const ordered = [
1351
+ join8(root, "src/App.tsx"),
1352
+ join8(root, "src/App.jsx"),
1353
+ ...files.filter(
1354
+ (f) => !f.endsWith("App.tsx") && !f.endsWith("App.jsx")
1355
+ )
1356
+ ];
1357
+ const seen = /* @__PURE__ */ new Set();
1358
+ const out = [];
1359
+ for (const f of ordered) {
1360
+ if (!seen.has(f) && files.includes(f)) {
1361
+ seen.add(f);
1362
+ out.push(f);
1363
+ }
658
1364
  }
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");
1365
+ for (const f of files) {
1366
+ if (!seen.has(f)) out.push(f);
667
1367
  }
668
- return created;
1368
+ return out;
669
1369
  }
670
1370
 
671
1371
  // src/verify.ts
672
- import { readFileSync as readFileSync9 } from "fs";
1372
+ function optimizeDepsSatisfied(viteConfigPath, packageJsonPath) {
1373
+ if (viteConfigHasOverlayOptimizeExclude(viteConfigPath)) return true;
1374
+ const pkg = readPackageJson(packageJsonPath);
1375
+ return nuvioOverlayLinkKind(pkg) === "workspace";
1376
+ }
673
1377
  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);
1378
+ const pkg = readPackageJson(packageJsonPath);
1379
+ const depsOk = hasNuvioPackages(pkg);
678
1380
  const mainEntry = resolveMainEntry(root);
679
1381
  return {
680
1382
  deps: depsOk ? "OK" : "MISSING",
681
1383
  vite: viteConfigHasNuvio(viteConfigPath) ? "OK" : "TODO",
682
1384
  overlayCss: mainEntry && (mainHasOverlayStyles(mainEntry) || !overlayInstalledFromNpm(packageJsonPath)) ? "OK" : "TODO",
683
- optimizeDeps: viteConfigHasOverlayOptimizeExclude(viteConfigPath) ? "OK" : "TODO",
684
- shell: appFile && appHasDevShell(appFile) ? "OK" : "TODO",
1385
+ optimizeDeps: optimizeDepsSatisfied(viteConfigPath, packageJsonPath) ? "OK" : "TODO",
1386
+ shell: projectHasDevShell(root) ? "OK" : "TODO",
685
1387
  starterId: projectHasPageTitleId(root) ? "OK" : "MISSING"
686
1388
  };
687
1389
  }
688
1390
  function printVerification(v) {
689
1391
  console.log("Verification:");
690
1392
  console.log(
691
- ` devDependencies: @nuvio/vite-plugin, @nuvio/overlay \u2014 ${v.deps}`
1393
+ ` dependencies: @nuvio/vite-plugin, @nuvio/overlay \u2014 ${v.deps}`
692
1394
  );
693
1395
  console.log(` vite.config: nuvio() \u2014 ${v.vite}`);
694
1396
  console.log(` main.tsx: @nuvio/overlay/style.css \u2014 ${v.overlayCss}`);
@@ -697,214 +1399,341 @@ function printVerification(v) {
697
1399
  console.log(` Starter id page.title \u2014 ${v.starterId}`);
698
1400
  }
699
1401
 
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;
1402
+ // src/doctor.ts
1403
+ async function checkDevServerReachable(port) {
1404
+ const url = `http://127.0.0.1:${port}/`;
1405
+ try {
1406
+ const res = await fetch(url, { signal: AbortSignal.timeout(1500) });
1407
+ if (res.ok) {
1408
+ return {
1409
+ id: "dev_server",
1410
+ label: `Dev server reachable (${url})`,
1411
+ status: "pass"
1412
+ };
1413
+ }
1414
+ return {
1415
+ id: "dev_server",
1416
+ label: "Dev server reachable",
1417
+ status: "warn",
1418
+ detail: `HTTP ${res.status} from ${url}`
1419
+ };
1420
+ } catch {
1421
+ return {
1422
+ id: "dev_server",
1423
+ label: "Dev server reachable",
1424
+ status: "warn",
1425
+ detail: `Start pnpm dev \u2014 could not reach ${url}`
1426
+ };
735
1427
  }
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
- }
744
- function posthogToken() {
745
- return process.env.NUVIO_POSTHOG_TOKEN ?? NUVIO_POSTHOG_TOKEN;
746
1428
  }
747
- function tokenIsConfigured(token) {
748
- return Boolean(token && token.startsWith("phc_"));
1429
+ function summarize(result) {
1430
+ const total = result.checks.length;
1431
+ const passed = result.passCount;
1432
+ const label = result.failCount > 0 ? "nuvio not ready" : result.warnCount > 0 ? "nuvio partially ready" : "nuvio ready";
1433
+ console.log(`
1434
+ Result: ${passed}/${total} passed \u2014 ${label}`);
749
1435
  }
750
- function readOrCreateAnonymousId() {
751
- if (sessionAnonymousId) return sessionAnonymousId;
1436
+ async function runDoctor(opts) {
1437
+ let scan;
752
1438
  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;
1439
+ scan = scanProject(opts.cwd);
1440
+ } catch (e) {
1441
+ if (e instanceof PreflightError) {
1442
+ console.error(e.message);
1443
+ return 1;
758
1444
  }
759
- } catch {
1445
+ throw e;
760
1446
  }
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 {
1447
+ const { ctx, detectedLibraries, index } = scan;
1448
+ const pkg = ctx.packageJson;
1449
+ const projectName = String(pkg.name ?? "project");
1450
+ const verification = verifyProject(
1451
+ ctx.root,
1452
+ ctx.packageJsonPath,
1453
+ ctx.viteConfigPath
1454
+ );
1455
+ const checks = [
1456
+ {
1457
+ id: "deps_plugin",
1458
+ label: "@nuvio/vite-plugin installed",
1459
+ status: verification.deps === "OK" ? "pass" : "fail"
1460
+ },
1461
+ {
1462
+ id: "deps_overlay",
1463
+ label: "@nuvio/overlay installed",
1464
+ status: verification.deps === "OK" ? "pass" : "fail"
1465
+ },
1466
+ {
1467
+ id: "vite_plugin",
1468
+ label: "vite.config contains nuvio()",
1469
+ status: verification.vite === "OK" ? "pass" : "fail"
1470
+ },
1471
+ {
1472
+ id: "optimize_deps",
1473
+ label: "optimizeDeps.exclude includes @nuvio/overlay",
1474
+ status: verification.optimizeDeps === "OK" ? "pass" : "fail",
1475
+ detail: verification.optimizeDeps === "OK" && nuvioOverlayLinkKind(pkg) === "workspace" ? "workspace install \u2014 optional" : void 0
1476
+ },
1477
+ {
1478
+ id: "overlay_css",
1479
+ label: "main entry imports @nuvio/overlay/style.css",
1480
+ status: verification.overlayCss === "OK" ? "pass" : "fail",
1481
+ detail: verification.overlayCss === "OK" && nuvioOverlayLinkKind(pkg) === "workspace" ? "workspace install \u2014 optional" : void 0
1482
+ },
1483
+ {
1484
+ id: "dev_shell",
1485
+ label: "NuvioDevShell mounted in app",
1486
+ status: verification.shell === "OK" ? "pass" : "fail"
1487
+ },
1488
+ {
1489
+ id: "tailwind",
1490
+ label: "Tailwind detected",
1491
+ status: ctx.tailwindOk ? "pass" : "warn",
1492
+ detail: ctx.tailwindOk ? void 0 : "Style edits may not apply visually without Tailwind"
1493
+ },
1494
+ {
1495
+ id: "editable_hosts",
1496
+ label: "At least one data-nuvio-id indexed",
1497
+ status: index.entries.length > 0 ? "pass" : "fail",
1498
+ detail: index.entries.length > 0 ? `${index.entries.length} host(s)` : "Run dev \u2192 Make Editable, or add ids manually"
1499
+ }
1500
+ ];
1501
+ if (detectedLibraries.length > 0) {
1502
+ checks.push({
1503
+ id: "libraries",
1504
+ label: "Component libraries detected",
1505
+ status: "pass",
1506
+ detail: detectedLibraries.join(", ")
1507
+ });
771
1508
  }
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
1509
+ if (index.duplicateErrors.length > 0) {
1510
+ checks.push({
1511
+ id: "duplicate_ids",
1512
+ label: "No duplicate data-nuvio-id values",
1513
+ status: "fail",
1514
+ detail: `${index.duplicateErrors.length} duplicate id(s) \u2014 run nuvio scan`
783
1515
  });
784
- telemetryDebug("PostHog client initialized", {
785
- host: POSTHOG_HOST,
786
- tokenPrefix: `${token.slice(0, 8)}\u2026`
1516
+ } else {
1517
+ checks.push({
1518
+ id: "duplicate_ids",
1519
+ label: "No duplicate data-nuvio-id values",
1520
+ status: "pass"
787
1521
  });
788
1522
  }
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;
1523
+ if (opts.checkDevServer !== false) {
1524
+ checks.push(await checkDevServerReachable(opts.devServerPort ?? 5173));
799
1525
  }
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
1526
+ const passCount = checks.filter((c) => c.status === "pass").length;
1527
+ const warnCount = checks.filter((c) => c.status === "warn").length;
1528
+ const failCount = checks.filter((c) => c.status === "fail").length;
1529
+ const result = {
1530
+ projectName,
1531
+ checks,
1532
+ passCount,
1533
+ warnCount,
1534
+ failCount
815
1535
  };
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
1536
+ const pm = detectPackageManager(ctx.root);
1537
+ const telemetry = {
1538
+ ...buildCliTelemetryProps(pm, ctx),
1539
+ pass_count: passCount,
1540
+ warn_count: warnCount,
1541
+ fail_count: failCount,
1542
+ ready: failCount === 0
825
1543
  };
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;
1544
+ captureCliEvent("doctor_run", telemetry);
1545
+ if (opts.json) {
1546
+ console.log(JSON.stringify(result, null, 2));
1547
+ return failCount > 0 ? 1 : 0;
831
1548
  }
832
- return props;
1549
+ console.log(`nuvio doctor \u2014 ${projectName}
1550
+ `);
1551
+ for (const check of checks) {
1552
+ const icon = check.status === "pass" ? "\u2705" : check.status === "warn" ? "\u26A0" : "\u274C";
1553
+ const suffix = check.detail ? ` \u2014 ${check.detail}` : "";
1554
+ console.log(` ${icon} ${check.label}${suffix}`);
1555
+ }
1556
+ summarize(result);
1557
+ return failCount > 0 ? 1 : 0;
833
1558
  }
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";
1559
+
1560
+ // src/init.ts
1561
+ import { createInterface } from "readline";
1562
+
1563
+ // src/install-packages.ts
1564
+ import { spawnSync } from "child_process";
1565
+ import { readFileSync as readFileSync10 } from "fs";
1566
+ function parseInstalledVersion(pkg, name) {
1567
+ const dev = pkg.devDependencies;
1568
+ const deps = pkg.dependencies;
1569
+ const raw = dev?.[name] ?? deps?.[name];
1570
+ if (!raw) return null;
1571
+ return raw.replace(/^[\^~]/, "");
1572
+ }
1573
+ function packagesNeedInstall(packageJsonPath, targetVersion) {
1574
+ const pkg = JSON.parse(readFileSync10(packageJsonPath, "utf8"));
1575
+ for (const name of ["@nuvio/vite-plugin", "@nuvio/overlay"]) {
1576
+ const v = parseInstalledVersion(pkg, name);
1577
+ if (v !== targetVersion) return true;
841
1578
  }
842
- return "preflight_unknown";
1579
+ return false;
843
1580
  }
844
- function captureCliInvoked(command, pmOverride) {
845
- captureCliEvent("nuvio_cli_invoked", buildCliInvokedProps(command, pmOverride));
1581
+ function runInstall(root, pm, version) {
1582
+ const cmd = installCommand(pm, version);
1583
+ const result = spawnSync(cmd, {
1584
+ cwd: root,
1585
+ shell: true,
1586
+ stdio: "inherit",
1587
+ env: process.env
1588
+ });
1589
+ if (result.status !== 0) {
1590
+ return {
1591
+ ok: false,
1592
+ message: `Install failed. Try manually:
1593
+ ${cmd}`
1594
+ };
1595
+ }
1596
+ return { ok: true };
846
1597
  }
847
- function captureCliEvent(event, props) {
1598
+
1599
+ // src/patch-starter-id.ts
1600
+ import * as t3 from "@babel/types";
1601
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
1602
+ function patchFirstHeading(filePath) {
1603
+ const source = readFileSync11(filePath, "utf8");
1604
+ let ast;
848
1605
  try {
849
- if (!isTelemetryEnabled()) {
850
- telemetryDebug(`skipped ${event} (telemetry disabled)`);
851
- return;
1606
+ ast = parseTs(source, filePath);
1607
+ } catch {
1608
+ return { ok: false, error: "parse failed" };
1609
+ }
1610
+ let patched = false;
1611
+ babel_traverse_default(ast, {
1612
+ JSXOpeningElement(path) {
1613
+ if (patched) return;
1614
+ const name = path.node.name;
1615
+ if (!t3.isJSXIdentifier(name)) return;
1616
+ if (name.name !== "h1" && name.name !== "h2") return;
1617
+ for (const attr of path.node.attributes) {
1618
+ if (t3.isJSXAttribute(attr) && t3.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
1619
+ return;
1620
+ }
1621
+ }
1622
+ path.node.attributes.push(
1623
+ t3.jsxAttribute(
1624
+ t3.jsxIdentifier("data-nuvio-id"),
1625
+ t3.stringLiteral("page.title")
1626
+ )
1627
+ );
1628
+ patched = true;
852
1629
  }
853
- const ph = getClient();
854
- if (!ph) {
855
- telemetryDebug(`skipped ${event} (no PostHog client \u2014 check token)`);
856
- return;
1630
+ });
1631
+ if (!patched) return { ok: false, error: "no h1/h2" };
1632
+ writeFileSync6(filePath, printTs(ast, source), "utf8");
1633
+ return { ok: true };
1634
+ }
1635
+ function patchStarterId(root) {
1636
+ const files = findHeadingFiles(root);
1637
+ for (const file of files) {
1638
+ const source = readFileSync11(file, "utf8");
1639
+ if (!/<h[12][\s>]/.test(source) && !/<>[\s\S]*<h[12]/.test(source)) {
1640
+ try {
1641
+ const ast = parseTs(source, file);
1642
+ let has = false;
1643
+ babel_traverse_default(ast, {
1644
+ JSXOpeningElement(path) {
1645
+ const name = path.node.name;
1646
+ if (t3.isJSXIdentifier(name) && (name.name === "h1" || name.name === "h2"))
1647
+ has = true;
1648
+ }
1649
+ });
1650
+ if (!has) continue;
1651
+ } catch {
1652
+ continue;
1653
+ }
857
1654
  }
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);
1655
+ const outcome = patchFirstHeading(file);
1656
+ if (outcome.ok) return { outcome, file };
867
1657
  }
1658
+ return { outcome: { ok: false, error: "no heading" } };
868
1659
  }
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
- ]);
1660
+
1661
+ // src/plan.ts
1662
+ function createPlan(root, pm) {
1663
+ const pmRun = pm === "pnpm" ? "pnpm dev" : pm === "yarn" ? "yarn dev" : pm === "bun" ? "bun run dev" : "npm run dev";
1664
+ return {
1665
+ root,
1666
+ pm,
1667
+ pmRun,
1668
+ installCommand: "",
1669
+ modify: [],
1670
+ create: [],
1671
+ warnings: [],
1672
+ tier: "full",
1673
+ failedSteps: []
1674
+ };
885
1675
  }
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);
1676
+
1677
+ // src/write-nuvio-folder.ts
1678
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
1679
+ import { dirname, join as join9 } from "path";
1680
+ import { fileURLToPath } from "url";
1681
+ import { DEFAULT_BRAND_CONFIG as DEFAULT_BRAND_CONFIG3, serializeBrandConfig } from "@nuvio/shared";
1682
+ var CLI_ROOT = join9(dirname(fileURLToPath(import.meta.url)), "..");
1683
+ function loadTemplate(name) {
1684
+ return readFileSync12(join9(CLI_ROOT, "templates", name), "utf8");
1685
+ }
1686
+ function render(tpl, vars) {
1687
+ let out = tpl;
1688
+ for (const [key, value] of Object.entries(vars)) {
1689
+ out = out.replaceAll(`{{${key}}}`, value);
894
1690
  }
1691
+ return out;
895
1692
  }
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
- })();
1693
+ function writeNuvioFolder(opts) {
1694
+ const dir = join9(opts.root, "nuvio");
1695
+ const created = [];
1696
+ mkdirSync2(dir, { recursive: true });
1697
+ const vars = {
1698
+ NUVIO_VERSION: opts.version,
1699
+ PM_RUN: opts.pmRun,
1700
+ FAILED_STEPS: opts.failedSteps.join(", ") || "(none)"
905
1701
  };
906
- process.once("SIGINT", onSignal);
907
- process.once("SIGTERM", onSignal);
1702
+ const startHere = join9(dir, "START_HERE.md");
1703
+ writeFileSync7(
1704
+ startHere,
1705
+ render(loadTemplate("START_HERE.md.tpl"), vars),
1706
+ "utf8"
1707
+ );
1708
+ created.push("nuvio/START_HERE.md");
1709
+ const readme = join9(dir, "README.md");
1710
+ writeFileSync7(
1711
+ readme,
1712
+ render(loadTemplate("README.pointer.md.tpl"), vars),
1713
+ "utf8"
1714
+ );
1715
+ created.push("nuvio/README.md");
1716
+ const agent = join9(dir, "AGENT.md");
1717
+ if (!existsSync7(agent) || opts.forceAgent) {
1718
+ writeFileSync7(agent, render(loadTemplate("AGENT.md.tpl"), vars), "utf8");
1719
+ created.push("nuvio/AGENT.md");
1720
+ }
1721
+ if (opts.failedSteps.length > 0) {
1722
+ const todo = join9(dir, "SETUP_TODO.md");
1723
+ writeFileSync7(
1724
+ todo,
1725
+ render(loadTemplate("SETUP_TODO.md.tpl"), vars),
1726
+ "utf8"
1727
+ );
1728
+ created.push("nuvio/SETUP_TODO.md");
1729
+ }
1730
+ const brand = join9(dir, "brand.json");
1731
+ if (!existsSync7(brand)) {
1732
+ writeFileSync7(brand, `${JSON.stringify(serializeBrandConfig(DEFAULT_BRAND_CONFIG3), null, 2)}
1733
+ `, "utf8");
1734
+ created.push("nuvio/brand.json");
1735
+ }
1736
+ return created;
908
1737
  }
909
1738
 
910
1739
  // src/init.ts
@@ -924,10 +1753,10 @@ async function confirm(plan) {
924
1753
  console.log(`
925
1754
  Install: ${plan.installCommand || "(skip)"}`);
926
1755
  const rl = createInterface({ input: process.stdin, output: process.stdout });
927
- return new Promise((resolve2) => {
1756
+ return new Promise((resolve5) => {
928
1757
  rl.question("\nProceed? [y/N] ", (answer) => {
929
1758
  rl.close();
930
- resolve2(/^y(es)?$/i.test(answer.trim()));
1759
+ resolve5(/^y(es)?$/i.test(answer.trim()));
931
1760
  });
932
1761
  });
933
1762
  }
@@ -1152,14 +1981,165 @@ async function runInit(opts) {
1152
1981
  return plan.tier === "partial" || plan.tier === "full" ? 0 : 1;
1153
1982
  }
1154
1983
 
1984
+ // src/scan-cmd.ts
1985
+ function runScan(opts) {
1986
+ let scan;
1987
+ try {
1988
+ scan = scanProject(opts.cwd);
1989
+ } catch (e) {
1990
+ if (e instanceof PreflightError) {
1991
+ console.error(e.message);
1992
+ return 1;
1993
+ }
1994
+ throw e;
1995
+ }
1996
+ const { ctx, detectedLibraries, index } = scan;
1997
+ const projectName = String(ctx.packageJson.name ?? "project");
1998
+ const hosts = index.entries.map((entry) => ({
1999
+ id: entry.id,
2000
+ file: relPath(ctx.root, entry.file),
2001
+ line: entry.line,
2002
+ column: entry.column,
2003
+ libraryHint: entry.libraryHint,
2004
+ classNameMode: entry.classNameMode
2005
+ }));
2006
+ const result = {
2007
+ projectName,
2008
+ hosts,
2009
+ hostCount: hosts.length,
2010
+ duplicateErrors: index.duplicateErrors.map((dup) => ({
2011
+ id: dup.id,
2012
+ occurrences: dup.occurrences.map((o) => ({
2013
+ file: relPath(ctx.root, o.file),
2014
+ line: o.line
2015
+ }))
2016
+ })),
2017
+ detectedLibraries,
2018
+ scannedFileCount: index.scannedFileCount
2019
+ };
2020
+ const pm = detectPackageManager(ctx.root);
2021
+ const telemetry = {
2022
+ ...buildCliTelemetryProps(pm, ctx),
2023
+ host_count: result.hostCount,
2024
+ duplicate_count: result.duplicateErrors.length,
2025
+ library_count: detectedLibraries.length
2026
+ };
2027
+ captureCliEvent("scan_run", telemetry);
2028
+ if (opts.json) {
2029
+ console.log(JSON.stringify(result, null, 2));
2030
+ return result.duplicateErrors.length > 0 ? 1 : 0;
2031
+ }
2032
+ console.log(`nuvio scan \u2014 ${result.hostCount} editable host(s)
2033
+ `);
2034
+ for (const host of hosts) {
2035
+ console.log(
2036
+ ` ${host.id.padEnd(28)} ${host.file}:${host.line}`
2037
+ );
2038
+ }
2039
+ if (result.duplicateErrors.length > 0) {
2040
+ console.log("");
2041
+ for (const dup of result.duplicateErrors) {
2042
+ const places = dup.occurrences.map((o) => `${o.file}:${o.line}`).join(", ");
2043
+ console.log(` \u274C duplicate id: ${dup.id} (${places}) \u2014 fix before apply`);
2044
+ }
2045
+ }
2046
+ if (detectedLibraries.length > 0) {
2047
+ console.log(`
2048
+ Libraries: ${detectedLibraries.join(", ")}`);
2049
+ }
2050
+ if (result.hostCount === 0) {
2051
+ console.log(
2052
+ "\n No hosts found \u2014 use Make Editable in the browser or add data-nuvio-id manually."
2053
+ );
2054
+ }
2055
+ return result.duplicateErrors.length > 0 ? 1 : 0;
2056
+ }
2057
+
2058
+ // src/stats.ts
2059
+ import { readRuntimeVersions } from "@nuvio/vite-plugin/scan";
2060
+ function runStats(opts) {
2061
+ let scan;
2062
+ try {
2063
+ scan = scanProject(opts.cwd);
2064
+ } catch (e) {
2065
+ if (e instanceof PreflightError) {
2066
+ console.error(e.message);
2067
+ return 1;
2068
+ }
2069
+ throw e;
2070
+ }
2071
+ const { ctx, detectedLibraries, index } = scan;
2072
+ const projectName = String(ctx.packageJson.name ?? "project");
2073
+ const taggedFiles = new Set(
2074
+ index.entries.map((e) => relPath(ctx.root, e.file))
2075
+ ).size;
2076
+ const classNameModes = aggregateClassNameModes(index.entries);
2077
+ const tableHosts = index.entries.filter(isTableHost).length;
2078
+ const versions = readRuntimeVersions(ctx.root);
2079
+ const result = {
2080
+ projectName,
2081
+ editableHosts: index.entries.length,
2082
+ taggedFiles,
2083
+ scannedFiles: index.scannedFileCount,
2084
+ duplicateIds: index.duplicateErrors.length,
2085
+ tableHosts,
2086
+ detectedLibraries,
2087
+ tailwindVersion: versions.tailwindVersion,
2088
+ classNameModes
2089
+ };
2090
+ const pm = detectPackageManager(ctx.root);
2091
+ const telemetry = {
2092
+ ...buildCliTelemetryProps(pm, ctx),
2093
+ editable_hosts: result.editableHosts,
2094
+ tagged_files: result.taggedFiles,
2095
+ duplicate_ids: result.duplicateIds,
2096
+ table_hosts: result.tableHosts,
2097
+ library_count: detectedLibraries.length
2098
+ };
2099
+ captureCliEvent("stats_run", telemetry);
2100
+ if (opts.json) {
2101
+ console.log(JSON.stringify(result, null, 2));
2102
+ return 0;
2103
+ }
2104
+ console.log("nuvio stats\n");
2105
+ console.log(` Editable hosts: ${result.editableHosts}`);
2106
+ console.log(` Tagged files: ${result.taggedFiles}`);
2107
+ console.log(` Files scanned: ${result.scannedFiles}`);
2108
+ console.log(
2109
+ ` Libraries detected: ${result.detectedLibraries.length > 0 ? result.detectedLibraries.join(", ") : "none"}`
2110
+ );
2111
+ console.log(` Table hosts: ${result.tableHosts}`);
2112
+ console.log(` Duplicate ids: ${result.duplicateIds}`);
2113
+ if (result.tailwindVersion) {
2114
+ console.log(` Tailwind version: ${result.tailwindVersion}`);
2115
+ }
2116
+ const modeParts = Object.entries(result.classNameModes).sort(([a], [b]) => a.localeCompare(b)).map(([mode, count]) => `${mode} ${count}`);
2117
+ if (modeParts.length > 0) {
2118
+ console.log(` Class modes: ${modeParts.join(", ")}`);
2119
+ }
2120
+ return 0;
2121
+ }
2122
+
1155
2123
  // src/cli.ts
1156
2124
  function printHelp() {
1157
2125
  console.log(`nuvio \u2014 CLI for React + Vite
1158
2126
 
1159
2127
  Usage:
1160
2128
  nuvio init [options]
2129
+ nuvio doctor [options]
2130
+ nuvio scan [options]
2131
+ nuvio stats [options]
2132
+ nuvio coverage verify [options]
2133
+ nuvio brand scan [options]
2134
+ nuvio brand apply [options]
2135
+
2136
+ Common options:
2137
+ --cwd <path> Project root (default: current directory)
2138
+ --json Machine-readable output (doctor, scan, stats)
2139
+ --verbose Show error stacks
2140
+ -h, --help Show help
1161
2141
 
1162
- Options:
2142
+ Init options:
1163
2143
  --yes Skip confirmation
1164
2144
  --no-install Patch files only; do not run package manager install
1165
2145
  --dry-run Show plan only (still prompts unless --yes / CI)
@@ -1167,16 +2147,39 @@ Options:
1167
2147
  --strict Fail if Tailwind is not detected
1168
2148
  --skip-tailwind-check Do not warn when Tailwind is missing
1169
2149
  --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
2150
 
1174
- Example:
1175
- pnpm dlx @nuvio/cli init
2151
+ Doctor options:
2152
+ --skip-dev-server Skip localhost dev-server health check
2153
+
2154
+ Coverage verify options:
2155
+ --page <slug> Page slug (loads nuvio/pages/<slug>.pcc.yaml)
2156
+ --manifest <path> Explicit PCC manifest path (overrides --page)
2157
+ --all Verify every manifest in nuvio/pages/
2158
+
2159
+ Brand scan options:
2160
+ --page <slug> Page slug (loads nuvio/pages/<slug>.pcc.yaml)
2161
+ --manifest <path> Explicit PCC manifest path (overrides --page)
2162
+ --all Scan every manifest in nuvio/pages/
2163
+
2164
+ Brand apply options:
2165
+ --page <slug> Page slug (loads nuvio/pages/<slug>.pcc.yaml)
2166
+ --manifest <path> Explicit PCC manifest path (overrides --page)
2167
+ --all Apply to every manifest in nuvio/pages/
2168
+ --dry-run Report targets without writing source files
2169
+
2170
+ Examples:
1176
2171
  pnpm dlx @nuvio/cli init --yes
2172
+ pnpm dlx @nuvio/cli doctor
2173
+ pnpm dlx @nuvio/cli scan --json
2174
+ pnpm dlx @nuvio/cli stats
2175
+ pnpm dlx @nuvio/cli coverage verify --page dashboard --cwd apps/tailadmin-dogfood
2176
+ pnpm dlx @nuvio/cli coverage verify --all --cwd apps/tailadmin-dogfood
2177
+ pnpm dlx @nuvio/cli brand scan --page dashboard --cwd apps/tailadmin-dogfood
2178
+ pnpm dlx @nuvio/cli brand scan --all --cwd apps/tailadmin-dogfood
2179
+ pnpm dlx @nuvio/cli brand apply --all --cwd apps/tailadmin-dogfood
1177
2180
  `);
1178
2181
  }
1179
- function parseArgs(argv) {
2182
+ function parseInitArgs(argv) {
1180
2183
  const args = argv.slice(2);
1181
2184
  let command = null;
1182
2185
  const opts = { cwd: process.cwd() };
@@ -1201,7 +2204,7 @@ function parseArgs(argv) {
1201
2204
  else if (arg === "--pm") {
1202
2205
  opts.pm = args[++i];
1203
2206
  } else if (arg === "--cwd") {
1204
- opts.cwd = resolve(args[++i] ?? ".");
2207
+ opts.cwd = resolve4(args[++i] ?? ".");
1205
2208
  } else if (arg.startsWith("-")) {
1206
2209
  console.error(`Unknown option: ${arg}`);
1207
2210
  help = true;
@@ -1209,10 +2212,161 @@ function parseArgs(argv) {
1209
2212
  }
1210
2213
  return { command, opts, help };
1211
2214
  }
2215
+ function parseProjectCommandArgs(argv, command) {
2216
+ const args = argv.slice(2);
2217
+ const common = { cwd: process.cwd() };
2218
+ const doctor = { ...common };
2219
+ let help = false;
2220
+ let i = args[0] === command ? 1 : 0;
2221
+ for (; i < args.length; i++) {
2222
+ const arg = args[i];
2223
+ if (arg === "-h" || arg === "--help") {
2224
+ help = true;
2225
+ continue;
2226
+ }
2227
+ if (arg === "--json") {
2228
+ common.json = true;
2229
+ doctor.json = true;
2230
+ } else if (arg === "--verbose") {
2231
+ common.verbose = true;
2232
+ doctor.verbose = true;
2233
+ } else if (arg === "--cwd") {
2234
+ const cwd = resolve4(args[++i] ?? ".");
2235
+ common.cwd = cwd;
2236
+ doctor.cwd = cwd;
2237
+ } else if (arg === "--skip-dev-server") {
2238
+ doctor.skipDevServer = true;
2239
+ } else if (arg.startsWith("-")) {
2240
+ console.error(`Unknown option: ${arg}`);
2241
+ help = true;
2242
+ }
2243
+ }
2244
+ return { command, common, doctor, help };
2245
+ }
2246
+ function parseCoverageVerifyArgs(argv) {
2247
+ const args = argv.slice(2);
2248
+ const common = { cwd: process.cwd() };
2249
+ let help = false;
2250
+ let page;
2251
+ let manifest;
2252
+ let all = false;
2253
+ let i = 0;
2254
+ if (args[0] === "coverage") {
2255
+ i = 1;
2256
+ }
2257
+ const subcommand = args[i] === "verify" ? "verify" : "";
2258
+ if (subcommand) {
2259
+ i += 1;
2260
+ }
2261
+ for (; i < args.length; i++) {
2262
+ const arg = args[i];
2263
+ if (arg === "-h" || arg === "--help") {
2264
+ help = true;
2265
+ continue;
2266
+ }
2267
+ if (arg === "--json") {
2268
+ common.json = true;
2269
+ } else if (arg === "--verbose") {
2270
+ common.verbose = true;
2271
+ } else if (arg === "--cwd") {
2272
+ common.cwd = resolve4(args[++i] ?? ".");
2273
+ } else if (arg === "--page") {
2274
+ page = args[++i];
2275
+ } else if (arg === "--manifest") {
2276
+ manifest = resolve4(args[++i] ?? "");
2277
+ } else if (arg === "--all") {
2278
+ all = true;
2279
+ } else if (arg.startsWith("-")) {
2280
+ console.error(`Unknown option: ${arg}`);
2281
+ help = true;
2282
+ }
2283
+ }
2284
+ return { command: "coverage", subcommand, common, page, manifest, all, help };
2285
+ }
2286
+ function parseBrandArgs(argv) {
2287
+ const args = argv.slice(2);
2288
+ const common = { cwd: process.cwd() };
2289
+ let help = false;
2290
+ let page;
2291
+ let manifest;
2292
+ let all = false;
2293
+ let dryRun = false;
2294
+ let i = 0;
2295
+ if (args[0] === "brand") {
2296
+ i = 1;
2297
+ }
2298
+ const subArg = args[i];
2299
+ const subcommand = subArg === "scan" || subArg === "apply" ? subArg : "";
2300
+ if (subcommand) {
2301
+ i += 1;
2302
+ }
2303
+ for (; i < args.length; i++) {
2304
+ const arg = args[i];
2305
+ if (arg === "-h" || arg === "--help") {
2306
+ help = true;
2307
+ continue;
2308
+ }
2309
+ if (arg === "--json") {
2310
+ common.json = true;
2311
+ } else if (arg === "--verbose") {
2312
+ common.verbose = true;
2313
+ } else if (arg === "--cwd") {
2314
+ common.cwd = resolve4(args[++i] ?? ".");
2315
+ } else if (arg === "--page") {
2316
+ page = args[++i];
2317
+ } else if (arg === "--manifest") {
2318
+ manifest = resolve4(args[++i] ?? "");
2319
+ } else if (arg === "--all") {
2320
+ all = true;
2321
+ } else if (arg === "--dry-run") {
2322
+ dryRun = true;
2323
+ } else if (arg.startsWith("-")) {
2324
+ console.error(`Unknown option: ${arg}`);
2325
+ help = true;
2326
+ }
2327
+ }
2328
+ return { command: "brand", subcommand, common, page, manifest, all, dryRun, help };
2329
+ }
1212
2330
  async function runCli(argv) {
1213
2331
  registerTelemetrySignalHandlers();
1214
- const { command, opts, help } = parseArgs(argv);
1215
- captureCliInvoked(resolveCliInvokedCommand(help, command), opts.pm);
2332
+ const rawCommand = argv[2] ?? null;
2333
+ const isCoverageCmd = rawCommand === "coverage";
2334
+ const isBrandCmd = rawCommand === "brand";
2335
+ const isProjectCmd = rawCommand === "doctor" || rawCommand === "scan" || rawCommand === "stats" || isCoverageCmd || isBrandCmd;
2336
+ let help = false;
2337
+ let command = rawCommand;
2338
+ let initOpts = { cwd: process.cwd() };
2339
+ let commonOpts = { cwd: process.cwd() };
2340
+ let doctorOpts = { cwd: process.cwd() };
2341
+ let coverageOpts = null;
2342
+ let brandOpts = null;
2343
+ if (isBrandCmd) {
2344
+ brandOpts = parseBrandArgs(argv);
2345
+ help = brandOpts.help;
2346
+ command = brandOpts.command;
2347
+ commonOpts = brandOpts.common;
2348
+ } else if (isCoverageCmd) {
2349
+ coverageOpts = parseCoverageVerifyArgs(argv);
2350
+ help = coverageOpts.help;
2351
+ command = coverageOpts.command;
2352
+ commonOpts = coverageOpts.common;
2353
+ } else if (isProjectCmd) {
2354
+ const parsed = parseProjectCommandArgs(argv, rawCommand);
2355
+ help = parsed.help;
2356
+ command = parsed.command;
2357
+ commonOpts = parsed.common;
2358
+ doctorOpts = parsed.doctor;
2359
+ } else {
2360
+ const parsed = parseInitArgs(argv);
2361
+ help = parsed.help;
2362
+ command = parsed.command;
2363
+ initOpts = parsed.opts;
2364
+ }
2365
+ const cwd = isProjectCmd ? commonOpts.cwd : initOpts.cwd;
2366
+ captureCliInvoked(
2367
+ resolveCliInvokedCommand(help, command),
2368
+ isProjectCmd ? void 0 : initOpts.pm
2369
+ );
1216
2370
  try {
1217
2371
  if (help) {
1218
2372
  printHelp();
@@ -1222,19 +2376,78 @@ async function runCli(argv) {
1222
2376
  printHelp();
1223
2377
  return 1;
1224
2378
  }
1225
- if (command !== "init") {
1226
- console.error(`Unknown command: ${command}`);
1227
- printHelp();
1228
- return 1;
2379
+ switch (command) {
2380
+ case "init":
2381
+ return await runInit(initOpts);
2382
+ case "doctor":
2383
+ return await runDoctor({
2384
+ cwd: doctorOpts.cwd,
2385
+ json: doctorOpts.json,
2386
+ checkDevServer: !doctorOpts.skipDevServer
2387
+ });
2388
+ case "scan":
2389
+ return runScan({ cwd: commonOpts.cwd, json: commonOpts.json });
2390
+ case "stats":
2391
+ return runStats({ cwd: commonOpts.cwd, json: commonOpts.json });
2392
+ case "coverage": {
2393
+ if (!coverageOpts || coverageOpts.subcommand !== "verify") {
2394
+ console.error("Usage: nuvio coverage verify --page <slug>");
2395
+ printHelp();
2396
+ return 1;
2397
+ }
2398
+ if (!coverageOpts.page && !coverageOpts.manifest && !coverageOpts.all) {
2399
+ console.error("Either --page, --manifest, or --all is required");
2400
+ return 2;
2401
+ }
2402
+ return runCoverageVerify({
2403
+ cwd: coverageOpts.common.cwd,
2404
+ page: coverageOpts.page,
2405
+ manifest: coverageOpts.manifest,
2406
+ all: coverageOpts.all,
2407
+ json: coverageOpts.common.json
2408
+ });
2409
+ }
2410
+ case "brand": {
2411
+ if (!brandOpts || brandOpts.subcommand !== "scan" && brandOpts.subcommand !== "apply") {
2412
+ console.error("Usage: nuvio brand scan|apply --page <slug>");
2413
+ printHelp();
2414
+ return 1;
2415
+ }
2416
+ if (!brandOpts.page && !brandOpts.manifest && !brandOpts.all) {
2417
+ console.error("Either --page, --manifest, or --all is required");
2418
+ return 2;
2419
+ }
2420
+ if (brandOpts.subcommand === "scan") {
2421
+ return runBrandScan({
2422
+ cwd: brandOpts.common.cwd,
2423
+ page: brandOpts.page,
2424
+ manifest: brandOpts.manifest,
2425
+ all: brandOpts.all,
2426
+ json: brandOpts.common.json
2427
+ });
2428
+ }
2429
+ return runBrandApply({
2430
+ cwd: brandOpts.common.cwd,
2431
+ page: brandOpts.page,
2432
+ manifest: brandOpts.manifest,
2433
+ all: brandOpts.all,
2434
+ dryRun: brandOpts.dryRun,
2435
+ json: brandOpts.common.json
2436
+ });
2437
+ }
2438
+ default:
2439
+ console.error(`Unknown command: ${command}`);
2440
+ printHelp();
2441
+ return 1;
1229
2442
  }
1230
- return await runInit(opts);
1231
2443
  } catch (e) {
1232
- const pm = detectPackageManager(opts.cwd, opts.pm);
2444
+ const pm = detectPackageManager(cwd, initOpts.pm);
1233
2445
  captureCliEvent("nuvio_init_failed", {
1234
2446
  ...buildCliTelemetryProps(pm),
1235
2447
  error_code: "unexpected_error"
1236
2448
  });
1237
- if (opts.verbose) console.error(e);
2449
+ const verbose = isProjectCmd ? commonOpts.verbose : initOpts.verbose;
2450
+ if (verbose) console.error(e);
1238
2451
  else console.error("Something went wrong. Run with --verbose for details.");
1239
2452
  return 2;
1240
2453
  } finally {