@nuvio/cli 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Nuvio contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # @nuvio/cli
2
+
3
+ One-command onboarding for Nuvio in Vite + React + Tailwind projects.
4
+
5
+ ```bash
6
+ pnpm dlx @nuvio/cli init
7
+ pnpm dev
8
+ ```
9
+
10
+ See [Nuvio docs](https://github.com/ehah/Nuvio/blob/main/docs/nuvioUser.md).
@@ -0,0 +1,851 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { resolve } from "path";
5
+
6
+ // src/init.ts
7
+ import { createInterface } from "readline";
8
+
9
+ // src/detect-project.ts
10
+ import { existsSync, readFileSync } from "fs";
11
+ import { join } from "path";
12
+
13
+ // src/messages.ts
14
+ var MSG = {
15
+ noPackageJson: "Run this from your app folder (the one with package.json).",
16
+ noVite: "Nuvio works with React + Vite projects. I couldn't find a Vite config here.",
17
+ noReact: "Nuvio needs React. Add react to this project first.",
18
+ noViteDep: "Nuvio needs Vite. Add vite to this project first.",
19
+ strictTailwind: "Nuvio expects Tailwind CSS for class edits. Install tailwindcss or pass --skip-tailwind-check.",
20
+ monorepoRoot: "This looks like the Nuvio monorepo. Run init in your app folder, not the tooling repo.",
21
+ cliPackage: "Cannot init inside @nuvio/cli package.",
22
+ partialHelp: "Nuvio set up what it could safely. Finish the steps in nuvio/SETUP_TODO.md, then run your dev server.",
23
+ noHeading: 'Nuvio is wired, but I could not find a heading to mark editable. Add data-nuvio-id="page.title" to one visible element (see nuvio/START_HERE.md).'
24
+ };
25
+
26
+ // src/detect-project.ts
27
+ var VITE_CONFIGS = [
28
+ "vite.config.ts",
29
+ "vite.config.js",
30
+ "vite.config.mts",
31
+ "vite.config.mjs"
32
+ ];
33
+ var PreflightError = class extends Error {
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = "PreflightError";
37
+ }
38
+ };
39
+ function hasDep(pkg, name) {
40
+ const deps = pkg.dependencies;
41
+ const dev = pkg.devDependencies;
42
+ return Boolean(deps?.[name] ?? dev?.[name]);
43
+ }
44
+ function detectTailwind(root, pkg) {
45
+ if (hasDep(pkg, "tailwindcss")) return true;
46
+ const cssCandidates = ["src/index.css", "src/App.css"];
47
+ for (const rel of cssCandidates) {
48
+ const p = join(root, rel);
49
+ if (!existsSync(p)) continue;
50
+ const text = readFileSync(p, "utf8");
51
+ if (text.includes("@tailwind") || text.includes('@import "tailwindcss"') || text.includes("@import 'tailwindcss'")) {
52
+ return true;
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+ function detectProject(root) {
58
+ const packageJsonPath = join(root, "package.json");
59
+ if (!existsSync(packageJsonPath)) {
60
+ throw new PreflightError(MSG.noPackageJson);
61
+ }
62
+ const packageJson = JSON.parse(
63
+ readFileSync(packageJsonPath, "utf8")
64
+ );
65
+ if (packageJson.name === "@nuvio/cli") {
66
+ throw new PreflightError(MSG.cliPackage);
67
+ }
68
+ if (packageJson.name === "nuvio" && packageJson.private === true) {
69
+ throw new PreflightError(MSG.monorepoRoot);
70
+ }
71
+ let viteConfigPath = "";
72
+ let viteConfigName = "";
73
+ for (const name of VITE_CONFIGS) {
74
+ const p = join(root, name);
75
+ if (existsSync(p)) {
76
+ viteConfigPath = p;
77
+ viteConfigName = name;
78
+ break;
79
+ }
80
+ }
81
+ if (!viteConfigPath) {
82
+ throw new PreflightError(MSG.noVite);
83
+ }
84
+ if (!hasDep(packageJson, "react")) {
85
+ throw new PreflightError(MSG.noReact);
86
+ }
87
+ if (!hasDep(packageJson, "vite")) {
88
+ throw new PreflightError(MSG.noViteDep);
89
+ }
90
+ const tailwindOk = detectTailwind(root, packageJson);
91
+ return {
92
+ root,
93
+ packageJsonPath,
94
+ packageJson,
95
+ viteConfigPath,
96
+ viteConfigName,
97
+ tailwindOk,
98
+ tailwindWarn: !tailwindOk
99
+ };
100
+ }
101
+
102
+ // src/detect-pm.ts
103
+ import { existsSync as existsSync2 } from "fs";
104
+ import { join as join2 } from "path";
105
+ function detectPackageManager(root, override) {
106
+ if (override) return override;
107
+ if (existsSync2(join2(root, "pnpm-lock.yaml"))) return "pnpm";
108
+ if (existsSync2(join2(root, "package-lock.json"))) return "npm";
109
+ if (existsSync2(join2(root, "yarn.lock"))) return "yarn";
110
+ if (existsSync2(join2(root, "bun.lockb")) || existsSync2(join2(root, "bun.lock")))
111
+ return "bun";
112
+ return "npm";
113
+ }
114
+ function installCommand(pm, version) {
115
+ const pkgs = `@nuvio/vite-plugin@${version} @nuvio/overlay@${version}`;
116
+ switch (pm) {
117
+ case "pnpm":
118
+ return `pnpm add -D ${pkgs}`;
119
+ case "yarn":
120
+ return `yarn add -D ${pkgs}`;
121
+ case "bun":
122
+ return `bun add -d ${pkgs}`;
123
+ default:
124
+ return `npm install -D ${pkgs}`;
125
+ }
126
+ }
127
+
128
+ // src/install-packages.ts
129
+ import { spawnSync } from "child_process";
130
+ import { readFileSync as readFileSync2 } from "fs";
131
+ function parseInstalledVersion(pkg, name) {
132
+ const dev = pkg.devDependencies;
133
+ const deps = pkg.dependencies;
134
+ const raw = dev?.[name] ?? deps?.[name];
135
+ if (!raw) return null;
136
+ return raw.replace(/^[\^~]/, "");
137
+ }
138
+ function packagesNeedInstall(packageJsonPath, targetVersion) {
139
+ const pkg = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
140
+ for (const name of ["@nuvio/vite-plugin", "@nuvio/overlay"]) {
141
+ const v = parseInstalledVersion(pkg, name);
142
+ if (v !== targetVersion) return true;
143
+ }
144
+ return false;
145
+ }
146
+ function runInstall(root, pm, version) {
147
+ const cmd = installCommand(pm, version);
148
+ const result = spawnSync(cmd, {
149
+ cwd: root,
150
+ shell: true,
151
+ stdio: "inherit",
152
+ env: process.env
153
+ });
154
+ if (result.status !== 0) {
155
+ return {
156
+ ok: false,
157
+ message: `Install failed. Try manually:
158
+ ${cmd}`
159
+ };
160
+ }
161
+ return { ok: true };
162
+ }
163
+
164
+ // src/babel-traverse.ts
165
+ import traverseImport from "@babel/traverse";
166
+ var traverse = typeof traverseImport === "function" ? traverseImport : traverseImport.default;
167
+ var babel_traverse_default = traverse;
168
+
169
+ // src/patch-vite-config.ts
170
+ import * as t from "@babel/types";
171
+ import { readFileSync as readFileSync3, writeFileSync } from "fs";
172
+
173
+ // src/babel-generator.ts
174
+ import generateImport from "@babel/generator";
175
+ var generate = typeof generateImport === "function" ? generateImport : generateImport.default;
176
+ var babel_generator_default = generate;
177
+
178
+ // src/parse-ts.ts
179
+ import { parse } from "@babel/parser";
180
+ var PARSE_OPTS = {
181
+ sourceType: "module",
182
+ plugins: ["typescript", "jsx"]
183
+ };
184
+ function parseTs(source, filename = "file.tsx") {
185
+ return parse(source, {
186
+ ...PARSE_OPTS,
187
+ sourceFilename: filename
188
+ });
189
+ }
190
+ function printTs(ast, source) {
191
+ const out = babel_generator_default(ast, { retainLines: true }, source);
192
+ return out.code.endsWith("\n") ? out.code : `${out.code}
193
+ `;
194
+ }
195
+
196
+ // src/patch-vite-config.ts
197
+ function hasNuvioImport(ast) {
198
+ let found = false;
199
+ babel_traverse_default(ast, {
200
+ ImportDeclaration(path) {
201
+ if (path.node.source.value === "@nuvio/vite-plugin") found = true;
202
+ }
203
+ });
204
+ return found;
205
+ }
206
+ function hasNuvioPluginCall(ast) {
207
+ let found = false;
208
+ babel_traverse_default(ast, {
209
+ CallExpression(path) {
210
+ if (t.isIdentifier(path.node.callee, { name: "nuvio" })) found = true;
211
+ }
212
+ });
213
+ return found;
214
+ }
215
+ function appendNuvioPlugin(ast) {
216
+ let patched = false;
217
+ babel_traverse_default(ast, {
218
+ ObjectProperty(path) {
219
+ if (!t.isIdentifier(path.node.key, { name: "plugins" })) return;
220
+ if (!t.isArrayExpression(path.node.value)) return;
221
+ path.node.value.elements.push(t.callExpression(t.identifier("nuvio"), []));
222
+ patched = true;
223
+ }
224
+ });
225
+ return patched;
226
+ }
227
+ function patchViteConfigFile(filePath) {
228
+ const source = readFileSync3(filePath, "utf8");
229
+ let ast;
230
+ try {
231
+ ast = parseTs(source, filePath);
232
+ } catch {
233
+ return { ok: false, error: "parse failed" };
234
+ }
235
+ if (hasNuvioImport(ast) && hasNuvioPluginCall(ast)) {
236
+ return { ok: true, skipped: true };
237
+ }
238
+ if (!hasNuvioImport(ast)) {
239
+ ast.program.body.unshift(
240
+ t.importDeclaration(
241
+ [t.importSpecifier(t.identifier("nuvio"), t.identifier("nuvio"))],
242
+ t.stringLiteral("@nuvio/vite-plugin")
243
+ )
244
+ );
245
+ }
246
+ if (!hasNuvioPluginCall(ast)) {
247
+ if (!appendNuvioPlugin(ast)) {
248
+ return { ok: false, error: "no static plugins array" };
249
+ }
250
+ }
251
+ writeFileSync(filePath, printTs(ast, source), "utf8");
252
+ return { ok: true };
253
+ }
254
+ function viteConfigHasNuvio(filePath) {
255
+ const source = readFileSync3(filePath, "utf8");
256
+ try {
257
+ const ast = parseTs(source, filePath);
258
+ return hasNuvioImport(ast) && hasNuvioPluginCall(ast);
259
+ } catch {
260
+ return /nuvio\s*\(/.test(source);
261
+ }
262
+ }
263
+
264
+ // src/patch-app-root.ts
265
+ import * as t2 from "@babel/types";
266
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
267
+ import { join as join3 } from "path";
268
+ var APP_CANDIDATES = [
269
+ "src/App.tsx",
270
+ "src/App.jsx",
271
+ "src/main.tsx",
272
+ "src/main.jsx"
273
+ ];
274
+ function resolveAppFile(root) {
275
+ for (const rel of APP_CANDIDATES) {
276
+ const p = join3(root, rel);
277
+ if (existsSync3(p)) return p;
278
+ }
279
+ return null;
280
+ }
281
+ function hasOverlayImport(ast) {
282
+ let found = false;
283
+ babel_traverse_default(ast, {
284
+ ImportDeclaration(path) {
285
+ if (path.node.source.value === "@nuvio/overlay") found = true;
286
+ }
287
+ });
288
+ return found;
289
+ }
290
+ function hasDevShell(ast) {
291
+ let found = false;
292
+ babel_traverse_default(ast, {
293
+ JSXElement(path) {
294
+ const name = path.node.openingElement.name;
295
+ if (t2.isJSXIdentifier(name) && name.name === "NuvioDevShell") found = true;
296
+ }
297
+ });
298
+ return found;
299
+ }
300
+ function unwrapJsx(node) {
301
+ if (!node) return null;
302
+ if (t2.isJSXElement(node) || t2.isJSXFragment(node)) return node;
303
+ if (t2.isParenthesizedExpression(node)) return unwrapJsx(node.expression);
304
+ return null;
305
+ }
306
+ var devShellElement = t2.jsxElement(
307
+ t2.jsxOpeningElement(t2.jsxIdentifier("NuvioDevShell"), [], true),
308
+ null,
309
+ [],
310
+ true
311
+ );
312
+ function appendDevShell(ast) {
313
+ let patched = false;
314
+ babel_traverse_default(ast, {
315
+ ReturnStatement(path) {
316
+ const jsx = unwrapJsx(path.node.argument);
317
+ if (!jsx) return;
318
+ if (t2.isJSXFragment(jsx)) {
319
+ jsx.children.push(t2.jsxText("\n "));
320
+ jsx.children.push(devShellElement);
321
+ patched = true;
322
+ } else {
323
+ path.node.argument = t2.jsxFragment(
324
+ t2.jsxOpeningFragment(),
325
+ t2.jsxClosingFragment(),
326
+ [jsx, t2.jsxText("\n "), devShellElement]
327
+ );
328
+ patched = true;
329
+ }
330
+ }
331
+ });
332
+ return patched;
333
+ }
334
+ function patchAppRootFile(filePath) {
335
+ const source = readFileSync4(filePath, "utf8");
336
+ let ast;
337
+ try {
338
+ ast = parseTs(source, filePath);
339
+ } catch {
340
+ return { ok: false, error: "parse failed" };
341
+ }
342
+ if (hasOverlayImport(ast) && hasDevShell(ast)) {
343
+ return { ok: true, skipped: true };
344
+ }
345
+ if (!hasOverlayImport(ast)) {
346
+ ast.program.body.unshift(
347
+ t2.importDeclaration(
348
+ [
349
+ t2.importSpecifier(
350
+ t2.identifier("NuvioDevShell"),
351
+ t2.identifier("NuvioDevShell")
352
+ )
353
+ ],
354
+ t2.stringLiteral("@nuvio/overlay")
355
+ )
356
+ );
357
+ }
358
+ if (!hasDevShell(ast) && !appendDevShell(ast)) {
359
+ return { ok: false, error: "no JSX return to patch" };
360
+ }
361
+ writeFileSync2(filePath, printTs(ast, source), "utf8");
362
+ return { ok: true };
363
+ }
364
+ function appHasDevShell(filePath) {
365
+ if (!existsSync3(filePath)) return false;
366
+ const source = readFileSync4(filePath, "utf8");
367
+ try {
368
+ const ast = parseTs(source, filePath);
369
+ return hasOverlayImport(ast) && hasDevShell(ast);
370
+ } catch {
371
+ return /NuvioDevShell/.test(source);
372
+ }
373
+ }
374
+
375
+ // src/patch-starter-id.ts
376
+ import * as t3 from "@babel/types";
377
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
378
+
379
+ // src/scan-ids.ts
380
+ import { readFileSync as readFileSync5 } from "fs";
381
+ import { join as join4 } from "path";
382
+ import fg from "fast-glob";
383
+ var ID_GLOB = ["src/**/*.{tsx,jsx}"];
384
+ function projectHasPageTitleId(root) {
385
+ const files = fg.sync(ID_GLOB, { cwd: root, absolute: true });
386
+ for (const file of files) {
387
+ const text = readFileSync5(file, "utf8");
388
+ if (/data-nuvio-id=["']page\.title["']/.test(text)) {
389
+ return true;
390
+ }
391
+ }
392
+ return false;
393
+ }
394
+ function findHeadingFiles(root) {
395
+ const files = fg.sync(ID_GLOB, { cwd: root, absolute: true });
396
+ const ordered = [
397
+ join4(root, "src/App.tsx"),
398
+ join4(root, "src/App.jsx"),
399
+ ...files.filter(
400
+ (f) => !f.endsWith("App.tsx") && !f.endsWith("App.jsx")
401
+ )
402
+ ];
403
+ const seen = /* @__PURE__ */ new Set();
404
+ const out = [];
405
+ for (const f of ordered) {
406
+ if (!seen.has(f) && files.includes(f)) {
407
+ seen.add(f);
408
+ out.push(f);
409
+ }
410
+ }
411
+ for (const f of files) {
412
+ if (!seen.has(f)) out.push(f);
413
+ }
414
+ return out;
415
+ }
416
+
417
+ // src/patch-starter-id.ts
418
+ function patchFirstHeading(filePath) {
419
+ const source = readFileSync6(filePath, "utf8");
420
+ let ast;
421
+ try {
422
+ ast = parseTs(source, filePath);
423
+ } catch {
424
+ return { ok: false, error: "parse failed" };
425
+ }
426
+ let patched = false;
427
+ babel_traverse_default(ast, {
428
+ JSXOpeningElement(path) {
429
+ if (patched) return;
430
+ const name = path.node.name;
431
+ if (!t3.isJSXIdentifier(name)) return;
432
+ if (name.name !== "h1" && name.name !== "h2") return;
433
+ for (const attr of path.node.attributes) {
434
+ if (t3.isJSXAttribute(attr) && t3.isJSXIdentifier(attr.name, { name: "data-nuvio-id" })) {
435
+ return;
436
+ }
437
+ }
438
+ path.node.attributes.push(
439
+ t3.jsxAttribute(
440
+ t3.jsxIdentifier("data-nuvio-id"),
441
+ t3.stringLiteral("page.title")
442
+ )
443
+ );
444
+ patched = true;
445
+ }
446
+ });
447
+ if (!patched) return { ok: false, error: "no h1/h2" };
448
+ writeFileSync3(filePath, printTs(ast, source), "utf8");
449
+ return { ok: true };
450
+ }
451
+ function patchStarterId(root) {
452
+ const files = findHeadingFiles(root);
453
+ for (const file of files) {
454
+ const source = readFileSync6(file, "utf8");
455
+ if (!/<h[12][\s>]/.test(source) && !/<>[\s\S]*<h[12]/.test(source)) {
456
+ try {
457
+ const ast = parseTs(source, file);
458
+ let has = false;
459
+ babel_traverse_default(ast, {
460
+ JSXOpeningElement(path) {
461
+ const name = path.node.name;
462
+ if (t3.isJSXIdentifier(name) && (name.name === "h1" || name.name === "h2"))
463
+ has = true;
464
+ }
465
+ });
466
+ if (!has) continue;
467
+ } catch {
468
+ continue;
469
+ }
470
+ }
471
+ const outcome = patchFirstHeading(file);
472
+ if (outcome.ok) return { outcome, file };
473
+ }
474
+ return { outcome: { ok: false, error: "no heading" } };
475
+ }
476
+
477
+ // src/plan.ts
478
+ function createPlan(root, pm) {
479
+ const pmRun = pm === "pnpm" ? "pnpm dev" : pm === "yarn" ? "yarn dev" : pm === "bun" ? "bun run dev" : "npm run dev";
480
+ return {
481
+ root,
482
+ pm,
483
+ pmRun,
484
+ installCommand: "",
485
+ modify: [],
486
+ create: [],
487
+ warnings: [],
488
+ tier: "full",
489
+ failedSteps: []
490
+ };
491
+ }
492
+
493
+ // src/version.ts
494
+ import { createRequire } from "module";
495
+ var require2 = createRequire(import.meta.url);
496
+ var NUVIO_VERSION = require2("../package.json").version;
497
+
498
+ // src/write-nuvio-folder.ts
499
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
500
+ import { dirname, join as join5 } from "path";
501
+ import { fileURLToPath } from "url";
502
+ var CLI_ROOT = join5(dirname(fileURLToPath(import.meta.url)), "..");
503
+ function loadTemplate(name) {
504
+ return readFileSync7(join5(CLI_ROOT, "templates", name), "utf8");
505
+ }
506
+ function render(tpl, vars) {
507
+ let out = tpl;
508
+ for (const [key, value] of Object.entries(vars)) {
509
+ out = out.replaceAll(`{{${key}}}`, value);
510
+ }
511
+ return out;
512
+ }
513
+ function writeNuvioFolder(opts) {
514
+ const dir = join5(opts.root, "nuvio");
515
+ const created = [];
516
+ mkdirSync(dir, { recursive: true });
517
+ const vars = {
518
+ NUVIO_VERSION: opts.version,
519
+ PM_RUN: opts.pmRun,
520
+ FAILED_STEPS: opts.failedSteps.join(", ") || "(none)"
521
+ };
522
+ const startHere = join5(dir, "START_HERE.md");
523
+ writeFileSync4(
524
+ startHere,
525
+ render(loadTemplate("START_HERE.md.tpl"), vars),
526
+ "utf8"
527
+ );
528
+ created.push("nuvio/START_HERE.md");
529
+ const readme = join5(dir, "README.md");
530
+ writeFileSync4(
531
+ readme,
532
+ render(loadTemplate("README.pointer.md.tpl"), vars),
533
+ "utf8"
534
+ );
535
+ created.push("nuvio/README.md");
536
+ const agent = join5(dir, "AGENT.md");
537
+ if (!existsSync4(agent) || opts.forceAgent) {
538
+ writeFileSync4(agent, render(loadTemplate("AGENT.md.tpl"), vars), "utf8");
539
+ created.push("nuvio/AGENT.md");
540
+ }
541
+ if (opts.failedSteps.length > 0) {
542
+ const todo = join5(dir, "SETUP_TODO.md");
543
+ writeFileSync4(
544
+ todo,
545
+ render(loadTemplate("SETUP_TODO.md.tpl"), vars),
546
+ "utf8"
547
+ );
548
+ created.push("nuvio/SETUP_TODO.md");
549
+ }
550
+ return created;
551
+ }
552
+
553
+ // src/verify.ts
554
+ import { readFileSync as readFileSync8 } from "fs";
555
+ function verifyProject(root, packageJsonPath, viteConfigPath) {
556
+ const pkg = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
557
+ const dev = pkg.devDependencies;
558
+ const depsOk = Boolean(dev?.["@nuvio/vite-plugin"]) && Boolean(dev?.["@nuvio/overlay"]);
559
+ const appFile = resolveAppFile(root);
560
+ return {
561
+ deps: depsOk ? "OK" : "MISSING",
562
+ vite: viteConfigHasNuvio(viteConfigPath) ? "OK" : "TODO",
563
+ shell: appFile && appHasDevShell(appFile) ? "OK" : "TODO",
564
+ starterId: projectHasPageTitleId(root) ? "OK" : "MISSING"
565
+ };
566
+ }
567
+ function printVerification(v) {
568
+ console.log("Verification:");
569
+ console.log(
570
+ ` devDependencies: @nuvio/vite-plugin, @nuvio/overlay \u2014 ${v.deps}`
571
+ );
572
+ console.log(` vite.config: nuvio() \u2014 ${v.vite}`);
573
+ console.log(` App shell: NuvioDevShell \u2014 ${v.shell}`);
574
+ console.log(` Starter id page.title \u2014 ${v.starterId}`);
575
+ }
576
+
577
+ // src/init.ts
578
+ function isAutoYes(opts) {
579
+ if (opts.yes) return true;
580
+ if (process.env.CI === "true") return true;
581
+ return !process.stdin.isTTY;
582
+ }
583
+ async function confirm(plan) {
584
+ console.log("\nPlanned changes:");
585
+ for (const f of plan.modify) console.log(` modify: ${f}`);
586
+ for (const f of plan.create) console.log(` create: ${f}`);
587
+ if (plan.warnings.length) {
588
+ console.log("\nWarnings:");
589
+ for (const w of plan.warnings) console.log(` \u26A0 ${w}`);
590
+ }
591
+ console.log(`
592
+ Install: ${plan.installCommand || "(skip)"}`);
593
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
594
+ return new Promise((resolve2) => {
595
+ rl.question("\nProceed? [y/N] ", (answer) => {
596
+ rl.close();
597
+ resolve2(/^y(es)?$/i.test(answer.trim()));
598
+ });
599
+ });
600
+ }
601
+ function computeTier(installOk, viteOk, appOk, starterOk) {
602
+ if (!installOk) return "failed";
603
+ if (!appOk) return "failed";
604
+ if (!viteOk || !starterOk) return "partial";
605
+ return "full";
606
+ }
607
+ function printSuccess(plan, checks) {
608
+ if (checks.install) {
609
+ console.log(
610
+ `\u2705 Nuvio packages targeted (@nuvio/vite-plugin@${NUVIO_VERSION}, @nuvio/overlay@${NUVIO_VERSION})`
611
+ );
612
+ }
613
+ if (checks.vite) console.log("\u2705 Vite plugin added");
614
+ else if (plan.failedSteps.some((s) => s.includes("vite"))) {
615
+ console.log("\u26A0 Vite plugin \u2014 see nuvio/SETUP_TODO.md");
616
+ }
617
+ if (checks.app) console.log("\u2705 Nuvio editor mounted");
618
+ else console.log("\u26A0 App shell \u2014 see nuvio/SETUP_TODO.md");
619
+ if (checks.starter) {
620
+ console.log(
621
+ `\u2705 Starter editable area: page.title${checks.starterFile ? ` (${checks.starterFile})` : ""}`
622
+ );
623
+ } else {
624
+ console.log(`\u26A0 ${MSG.noHeading}`);
625
+ }
626
+ console.log("\u2705 Start here: nuvio/START_HERE.md");
627
+ console.log("\u2705 Agent guide: nuvio/AGENT.md");
628
+ console.log(`
629
+ Next:
630
+ ${plan.pmRun}
631
+ `);
632
+ console.log("Open localhost \u2192 Edit on \u2192 click the starter element.");
633
+ if (plan.tier === "partial" && plan.failedSteps.length > 0) {
634
+ console.log(`
635
+ ${MSG.partialHelp}`);
636
+ } else if (plan.tier === "partial") {
637
+ console.log(
638
+ "\nNuvio helped you as far as it safely could. See warnings above."
639
+ );
640
+ }
641
+ }
642
+ async function runInit(opts) {
643
+ const root = opts.cwd;
644
+ let project;
645
+ try {
646
+ project = detectProject(root);
647
+ } catch (e) {
648
+ if (e instanceof PreflightError) {
649
+ console.error(e.message);
650
+ return 1;
651
+ }
652
+ if (opts.verbose) console.error(e);
653
+ console.error("Something went wrong. Run with --verbose for details.");
654
+ return 2;
655
+ }
656
+ const pm = detectPackageManager(root, opts.pm);
657
+ const plan = createPlan(root, pm);
658
+ plan.installCommand = opts.noInstall ? "(skipped \u2014 --no-install)" : installCommand(pm, NUVIO_VERSION);
659
+ if (project.tailwindWarn && !opts.skipTailwindCheck) {
660
+ const msg = "Tailwind CSS not detected. Class/style edits may not work until Tailwind is installed.";
661
+ if (opts.strict) {
662
+ console.error(MSG.strictTailwind);
663
+ return 1;
664
+ }
665
+ plan.warnings.push(msg);
666
+ }
667
+ const appFile = resolveAppFile(root);
668
+ if (appFile) plan.modify.push(appFile.replace(`${root}/`, ""));
669
+ plan.modify.push(project.viteConfigName);
670
+ plan.create.push(
671
+ "nuvio/START_HERE.md",
672
+ "nuvio/README.md",
673
+ "nuvio/AGENT.md"
674
+ );
675
+ if (opts.dryRun) {
676
+ console.log("Dry run \u2014 no files changed.\n");
677
+ for (const f of plan.modify) console.log(` would modify: ${f}`);
678
+ for (const f of plan.create) console.log(` would create: ${f}`);
679
+ if (plan.warnings.length) {
680
+ for (const w of plan.warnings) console.log(` would warn: ${w}`);
681
+ }
682
+ console.log(` would run: ${plan.installCommand}`);
683
+ return 0;
684
+ }
685
+ if (!isAutoYes(opts)) {
686
+ const ok = await confirm(plan);
687
+ if (!ok) {
688
+ console.log("Cancelled.");
689
+ return 1;
690
+ }
691
+ }
692
+ let installOk = true;
693
+ if (!opts.noInstall) {
694
+ if (packagesNeedInstall(project.packageJsonPath, NUVIO_VERSION)) {
695
+ const result = runInstall(root, pm, NUVIO_VERSION);
696
+ if (!result.ok) {
697
+ console.error(result.message ?? "Install failed.");
698
+ return 1;
699
+ }
700
+ } else {
701
+ console.log("\u2705 Nuvio packages already installed");
702
+ }
703
+ } else {
704
+ console.log("(skipped install \u2014 --no-install)");
705
+ installOk = true;
706
+ }
707
+ let viteOk = false;
708
+ const viteResult = patchViteConfigFile(project.viteConfigPath);
709
+ if (viteResult.ok) {
710
+ viteOk = true;
711
+ } else {
712
+ plan.failedSteps.push(`vite (${viteResult.error ?? "unknown"})`);
713
+ plan.warnings.push(`Could not patch ${project.viteConfigName}`);
714
+ }
715
+ let appOk = false;
716
+ if (appFile) {
717
+ const appResult = patchAppRootFile(appFile);
718
+ if (appResult.ok) {
719
+ appOk = true;
720
+ } else {
721
+ plan.failedSteps.push(`app (${appResult.error ?? "unknown"})`);
722
+ plan.warnings.push(`Could not patch ${appFile}`);
723
+ }
724
+ } else {
725
+ plan.failedSteps.push("app (no App.tsx/main.tsx)");
726
+ }
727
+ let starterOk = false;
728
+ let starterFile;
729
+ if (!projectHasPageTitleId(root)) {
730
+ const { outcome, file } = patchStarterId(root);
731
+ if (outcome.ok) {
732
+ starterOk = true;
733
+ starterFile = file?.replace(`${root}/`, "");
734
+ if (file) plan.modify.push(starterFile);
735
+ } else {
736
+ plan.warnings.push(MSG.noHeading);
737
+ }
738
+ } else {
739
+ starterOk = true;
740
+ console.log("\u2705 Starter id page.title already present");
741
+ }
742
+ writeNuvioFolder({
743
+ root,
744
+ version: NUVIO_VERSION,
745
+ pmRun: plan.pmRun,
746
+ failedSteps: plan.failedSteps,
747
+ forceAgent: opts.forceAgent
748
+ });
749
+ plan.tier = computeTier(installOk, viteOk, appOk, starterOk);
750
+ console.log("");
751
+ printSuccess(plan, {
752
+ install: installOk,
753
+ vite: viteOk,
754
+ app: appOk,
755
+ starter: starterOk,
756
+ starterFile
757
+ });
758
+ const verification = verifyProject(
759
+ root,
760
+ project.packageJsonPath,
761
+ project.viteConfigPath
762
+ );
763
+ printVerification(verification);
764
+ if (plan.tier === "failed") return 1;
765
+ return plan.tier === "partial" || plan.tier === "full" ? 0 : 1;
766
+ }
767
+
768
+ // src/cli.ts
769
+ function printHelp() {
770
+ console.log(`nuvio \u2014 Nuvio CLI
771
+
772
+ Usage:
773
+ nuvio init [options]
774
+
775
+ Options:
776
+ --yes Skip confirmation
777
+ --no-install Patch files only; do not run package manager install
778
+ --dry-run Show plan only (still prompts unless --yes / CI)
779
+ --pm <pnpm|npm|yarn|bun> Force package manager
780
+ --strict Fail if Tailwind is not detected
781
+ --skip-tailwind-check Do not warn when Tailwind is missing
782
+ --force-agent Overwrite nuvio/AGENT.md
783
+ --cwd <path> Project root (default: current directory)
784
+ --verbose Show error stacks
785
+ -h, --help Show help
786
+
787
+ Example:
788
+ pnpm dlx @nuvio/cli init
789
+ pnpm dlx @nuvio/cli init --yes
790
+ `);
791
+ }
792
+ function parseArgs(argv) {
793
+ const args = argv.slice(2);
794
+ let command = null;
795
+ const opts = { cwd: process.cwd() };
796
+ let help = false;
797
+ for (let i = 0; i < args.length; i++) {
798
+ const arg = args[i];
799
+ if (arg === "-h" || arg === "--help") {
800
+ help = true;
801
+ continue;
802
+ }
803
+ if (!command && !arg.startsWith("-")) {
804
+ command = arg;
805
+ continue;
806
+ }
807
+ if (arg === "--yes") opts.yes = true;
808
+ else if (arg === "--no-install") opts.noInstall = true;
809
+ else if (arg === "--dry-run") opts.dryRun = true;
810
+ else if (arg === "--strict") opts.strict = true;
811
+ else if (arg === "--skip-tailwind-check") opts.skipTailwindCheck = true;
812
+ else if (arg === "--force-agent") opts.forceAgent = true;
813
+ else if (arg === "--verbose") opts.verbose = true;
814
+ else if (arg === "--pm") {
815
+ opts.pm = args[++i];
816
+ } else if (arg === "--cwd") {
817
+ opts.cwd = resolve(args[++i] ?? ".");
818
+ } else if (arg.startsWith("-")) {
819
+ console.error(`Unknown option: ${arg}`);
820
+ help = true;
821
+ }
822
+ }
823
+ return { command, opts, help };
824
+ }
825
+ async function runCli(argv) {
826
+ const { command, opts, help } = parseArgs(argv);
827
+ if (help) {
828
+ printHelp();
829
+ return 0;
830
+ }
831
+ if (!command) {
832
+ printHelp();
833
+ return 1;
834
+ }
835
+ if (command !== "init") {
836
+ console.error(`Unknown command: ${command}`);
837
+ printHelp();
838
+ return 1;
839
+ }
840
+ try {
841
+ return await runInit(opts);
842
+ } catch (e) {
843
+ if (opts.verbose) console.error(e);
844
+ else console.error("Something went wrong. Run with --verbose for details.");
845
+ return 2;
846
+ }
847
+ }
848
+
849
+ // src/cli-entry.ts
850
+ var code = await runCli(process.argv);
851
+ process.exit(code);
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@nuvio/cli",
3
+ "version": "0.5.1",
4
+ "description": "Nuvio CLI: one-command Vite + React onboarding (nuvio init).",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/ehah/Nuvio.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "keywords": [
12
+ "nuvio",
13
+ "vite",
14
+ "cli",
15
+ "onboarding",
16
+ "devtools"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "templates",
24
+ "package.json",
25
+ "README.md"
26
+ ],
27
+ "type": "module",
28
+ "bin": {
29
+ "nuvio": "./dist/cli-entry.js"
30
+ },
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "dependencies": {
35
+ "@babel/generator": "^7.26.9",
36
+ "@babel/parser": "^7.26.9",
37
+ "@babel/traverse": "^7.26.9",
38
+ "@babel/types": "^7.26.9",
39
+ "fast-glob": "^3.3.3"
40
+ },
41
+ "devDependencies": {
42
+ "@types/babel__generator": "^7.6.8",
43
+ "@types/babel__traverse": "^7.20.7",
44
+ "@types/node": "^22.13.5",
45
+ "tsup": "^8.4.0",
46
+ "typescript": "^5.7.3",
47
+ "vitest": "^3.0.6"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup src/cli-entry.ts --format esm --clean --external @babel/parser --external @babel/traverse --external @babel/generator --external @babel/types --external fast-glob",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit",
52
+ "test": "vitest run"
53
+ }
54
+ }
@@ -0,0 +1,27 @@
1
+ <!-- nuvio-cli-template: 1 -->
2
+ # Nuvio agent instructions
3
+
4
+ This project uses [Nuvio](https://www.npmjs.com/org/nuvio) (dev-only visual editor).
5
+
6
+ When the user asks to make UI editable or wire Nuvio:
7
+
8
+ 1. Do **not** change unrelated files.
9
+ 2. Add **string literal** `data-nuvio-id="region.name"` on JSX elements they should click in the browser.
10
+ 3. Keep `className="..."` as a **string literal** on that same tag when they need Tailwind class patches (avoid `cn(...)` on the patch target).
11
+ 4. Never use `{condition ? "id" : undefined}` for `data-nuvio-id` — use a string literal on the branch they edit.
12
+
13
+ **Card pattern:**
14
+ - `metric.orders.card` (container)
15
+ - `metric.orders.label`
16
+ - `metric.orders.value`
17
+
18
+ **Table pattern:**
19
+ - `orders.section`, `orders.title`
20
+ - `orders.header.products` (column headers)
21
+ - `orders.row.${id}.nameText` (row text — template literal id is OK for rows)
22
+
23
+ **After instrumentation:** user runs `{{PM_RUN}}`, Edit on, Preview Changes, Apply to Code.
24
+
25
+ If Vite or shell wiring is missing, see `nuvio/SETUP_TODO.md` or run `pnpm dlx @nuvio/cli@{{NUVIO_VERSION}} init`.
26
+
27
+ Human quick path: `nuvio/START_HERE.md`.
@@ -0,0 +1,6 @@
1
+ <!-- nuvio-cli-template: 1 -->
2
+ # Nuvio
3
+
4
+ **Start here:** [START_HERE.md](./START_HERE.md)
5
+
6
+ Agent instructions: [AGENT.md](./AGENT.md)
@@ -0,0 +1,27 @@
1
+ <!-- nuvio-cli-template: 1 -->
2
+ # Nuvio setup — manual steps
3
+
4
+ @nuvio/cli could not safely patch: {{FAILED_STEPS}}
5
+
6
+ ## Vite (if needed)
7
+
8
+ Add to `vite.config.ts`:
9
+
10
+ ```ts
11
+ import { nuvio } from "@nuvio/vite-plugin";
12
+ // inside defineConfig:
13
+ plugins: [react(), nuvio()],
14
+ resolve: { dedupe: ["react", "react-dom"] },
15
+ ```
16
+
17
+ ## App shell (if needed)
18
+
19
+ ```tsx
20
+ import { NuvioDevShell } from "@nuvio/overlay";
21
+ // inside root component return:
22
+ <NuvioDevShell />
23
+ ```
24
+
25
+ ## Starter id (if needed)
26
+
27
+ Add to one visible heading: `data-nuvio-id="page.title"`
@@ -0,0 +1,16 @@
1
+ <!-- nuvio-cli-template: 1 -->
2
+ # Start here — Nuvio in this project
3
+
4
+ Installed with @nuvio/cli@{{NUVIO_VERSION}}.
5
+
6
+ **Run:** {{PM_RUN}}
7
+
8
+ **Then:**
9
+ 1. Open the localhost URL from the terminal
10
+ 2. Turn **Edit** on (Nuvio chip)
11
+ 3. Click the starter element (usually the page title — id `page.title`)
12
+ 4. **Preview Changes** → **Apply to Code**
13
+
14
+ **More UI (cards, tables, nav):** ask your AI agent to read `nuvio/AGENT.md` and add `data-nuvio-id` attributes.
15
+
16
+ **Manual setup:** https://github.com/ehah/Nuvio/blob/v{{NUVIO_VERSION}}/docs/nuvioUser.md