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