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