@inspecto-dev/cli 0.2.0-alpha.5 → 0.3.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +19 -20
- package/CHANGELOG.md +22 -0
- package/README.md +93 -11
- package/bin/inspecto.js +5 -1
- package/dist/bin.d.ts +5 -1
- package/dist/bin.js +530 -49
- package/dist/chunk-FZS2TLXQ.js +3140 -0
- package/dist/index.d.ts +233 -2
- package/dist/index.js +17 -3
- package/package.json +3 -2
- package/src/bin.ts +286 -66
- package/src/commands/apply.ts +118 -0
- package/src/commands/detect.ts +59 -0
- package/src/commands/doctor.ts +225 -72
- package/src/commands/init.ts +143 -183
- package/src/commands/integration-install.ts +452 -0
- package/src/commands/onboard.ts +50 -0
- package/src/commands/plan.ts +41 -0
- package/src/detect/build-tool.ts +107 -3
- package/src/index.ts +17 -2
- package/src/inject/ast-injector.ts +17 -6
- package/src/inject/extension.ts +40 -22
- package/src/inject/gitignore.ts +10 -3
- package/src/instructions.ts +60 -46
- package/src/onboarding/apply.ts +364 -0
- package/src/onboarding/context.ts +36 -0
- package/src/onboarding/planner.ts +284 -0
- package/src/onboarding/session.ts +434 -0
- package/src/onboarding/target-resolution.ts +116 -0
- package/src/prompts.ts +54 -11
- package/src/types.ts +184 -0
- package/src/utils/fs.ts +2 -1
- package/src/utils/logger.ts +9 -0
- package/src/utils/output.ts +40 -0
- package/tests/apply.test.ts +583 -0
- package/tests/ast-injector.test.ts +50 -0
- package/tests/build-tool.test.ts +3 -5
- package/tests/detect.test.ts +94 -0
- package/tests/doctor.test.ts +224 -0
- package/tests/init.test.ts +364 -0
- package/tests/install-wrapper.test.ts +76 -0
- package/tests/instructions.test.ts +61 -0
- package/tests/integration-install.test.ts +294 -0
- package/tests/logger.test.ts +100 -0
- package/tests/onboard.test.ts +258 -0
- package/tests/plan.test.ts +713 -0
- package/tests/workspace-build-tool.test.ts +75 -0
- package/.turbo/turbo-test.log +0 -16
- package/dist/chunk-MIHQGC3L.js +0 -1720
|
@@ -0,0 +1,3140 @@
|
|
|
1
|
+
// src/utils/logger.ts
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
var log = {
|
|
4
|
+
/** Section header */
|
|
5
|
+
header(text) {
|
|
6
|
+
console.log();
|
|
7
|
+
console.log(` ${pc.bold(pc.cyan("\u2726"))} ${pc.bold(text)}`);
|
|
8
|
+
console.log();
|
|
9
|
+
},
|
|
10
|
+
/** Info item (used for actionable but not fully successful states) */
|
|
11
|
+
info(text) {
|
|
12
|
+
console.log(` ${pc.blue("\u2139")} ${text}`);
|
|
13
|
+
},
|
|
14
|
+
success(text) {
|
|
15
|
+
console.log(` ${pc.green("\u2714")} ${text}`);
|
|
16
|
+
},
|
|
17
|
+
/** Warning item */
|
|
18
|
+
warn(text) {
|
|
19
|
+
console.log(` ${pc.yellow("\u26A0")} ${text}`);
|
|
20
|
+
},
|
|
21
|
+
/** Error item */
|
|
22
|
+
error(text) {
|
|
23
|
+
console.log(` ${pc.red("\u2718")} ${text}`);
|
|
24
|
+
},
|
|
25
|
+
/** Indented hint line */
|
|
26
|
+
hint(text) {
|
|
27
|
+
console.log(` ${pc.dim("\u2192")} ${text}`);
|
|
28
|
+
},
|
|
29
|
+
/** Blank line */
|
|
30
|
+
blank() {
|
|
31
|
+
console.log();
|
|
32
|
+
},
|
|
33
|
+
/** Final ready message */
|
|
34
|
+
ready(text) {
|
|
35
|
+
console.log();
|
|
36
|
+
console.log(` ${pc.bold(pc.green("\u26A1"))} ${pc.bold(text)}`);
|
|
37
|
+
console.log();
|
|
38
|
+
},
|
|
39
|
+
/** Code block for manual instructions */
|
|
40
|
+
codeBlock(lines) {
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(` ${pc.dim("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510")}`);
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
console.log(` ${pc.dim("\u2502")} ${line.padEnd(48)}${pc.dim("\u2502")}`);
|
|
45
|
+
}
|
|
46
|
+
console.log(` ${pc.dim("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518")}`);
|
|
47
|
+
console.log();
|
|
48
|
+
},
|
|
49
|
+
/** Copy-friendly code block without box characters */
|
|
50
|
+
copyableCodeBlock(lines) {
|
|
51
|
+
console.log();
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
console.log(` ${line}`);
|
|
54
|
+
}
|
|
55
|
+
console.log();
|
|
56
|
+
},
|
|
57
|
+
/** Dry-run prefix */
|
|
58
|
+
dryRun(text) {
|
|
59
|
+
console.log(` ${pc.blue("[dry-run]")} ${text}`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/utils/output.ts
|
|
64
|
+
function writeCommandOutput(result, json, renderText) {
|
|
65
|
+
if (json) {
|
|
66
|
+
console.log(JSON.stringify(result, null, 2));
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
renderText(result);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
function reportCommandError(error, options = {}) {
|
|
73
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
74
|
+
const stack = error instanceof Error ? error.stack : void 0;
|
|
75
|
+
if (options.json) {
|
|
76
|
+
const payload = {
|
|
77
|
+
status: "error",
|
|
78
|
+
error: {
|
|
79
|
+
message: message2,
|
|
80
|
+
...options.debug && stack ? { stack } : {}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
console.error(JSON.stringify(payload, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
log.error(message2);
|
|
87
|
+
if (options.debug && stack) {
|
|
88
|
+
console.error(stack);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/onboarding/apply.ts
|
|
93
|
+
import path5 from "path";
|
|
94
|
+
import ora from "ora";
|
|
95
|
+
|
|
96
|
+
// src/detect/package-manager.ts
|
|
97
|
+
import path2 from "path";
|
|
98
|
+
|
|
99
|
+
// src/utils/fs.ts
|
|
100
|
+
import fs from "fs/promises";
|
|
101
|
+
import path from "path";
|
|
102
|
+
async function exists(filePath) {
|
|
103
|
+
try {
|
|
104
|
+
await fs.access(filePath);
|
|
105
|
+
return true;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function readFile(filePath) {
|
|
111
|
+
try {
|
|
112
|
+
return await fs.readFile(filePath, "utf-8");
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function writeFile(filePath, content) {
|
|
118
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
119
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
120
|
+
}
|
|
121
|
+
async function removeDir(dirPath) {
|
|
122
|
+
try {
|
|
123
|
+
await fs.rm(dirPath, { recursive: true, force: true });
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function readJSON(filePath) {
|
|
128
|
+
const text = await readFile(filePath);
|
|
129
|
+
if (text === null) return null;
|
|
130
|
+
if (text.trim() === "") return {};
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(text);
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function writeJSON(filePath, data) {
|
|
138
|
+
await writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/detect/package-manager.ts
|
|
142
|
+
async function detectPackageManager(root) {
|
|
143
|
+
const checks = [
|
|
144
|
+
["bun.lockb", "bun"],
|
|
145
|
+
["bun.lock", "bun"],
|
|
146
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
147
|
+
["yarn.lock", "yarn"],
|
|
148
|
+
["package-lock.json", "npm"]
|
|
149
|
+
];
|
|
150
|
+
const results = await Promise.all(
|
|
151
|
+
checks.map(async ([file, pm]) => {
|
|
152
|
+
const isExist = await exists(path2.join(root, file));
|
|
153
|
+
return { isExist, pm };
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
for (const result of results) {
|
|
157
|
+
if (result.isExist) {
|
|
158
|
+
return result.pm;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return "npm";
|
|
162
|
+
}
|
|
163
|
+
function getInstallCommand(pm, pkg) {
|
|
164
|
+
switch (pm) {
|
|
165
|
+
case "bun":
|
|
166
|
+
return `bun add -D ${pkg}`;
|
|
167
|
+
case "pnpm":
|
|
168
|
+
return `pnpm add -D ${pkg}`;
|
|
169
|
+
case "yarn":
|
|
170
|
+
return `yarn add -D ${pkg}`;
|
|
171
|
+
case "npm":
|
|
172
|
+
return `npm install -D ${pkg}`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function getUninstallCommand(pm, pkg) {
|
|
176
|
+
switch (pm) {
|
|
177
|
+
case "bun":
|
|
178
|
+
return `bun remove ${pkg}`;
|
|
179
|
+
case "pnpm":
|
|
180
|
+
return `pnpm remove ${pkg}`;
|
|
181
|
+
case "yarn":
|
|
182
|
+
return `yarn remove ${pkg}`;
|
|
183
|
+
case "npm":
|
|
184
|
+
return `npm uninstall ${pkg}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/inject/ast-injector.ts
|
|
189
|
+
import path3 from "path";
|
|
190
|
+
import { loadFile, writeFile as writeAstFile } from "magicast";
|
|
191
|
+
|
|
192
|
+
// src/inject/strategies/vite.ts
|
|
193
|
+
import { addVitePlugin } from "magicast/helpers";
|
|
194
|
+
var ViteStrategy = class {
|
|
195
|
+
name = "Vite";
|
|
196
|
+
supports(tool) {
|
|
197
|
+
return tool === "vite";
|
|
198
|
+
}
|
|
199
|
+
inject({ mod, detection }) {
|
|
200
|
+
addVitePlugin(mod, {
|
|
201
|
+
from: "@inspecto-dev/plugin",
|
|
202
|
+
constructor: "inspecto",
|
|
203
|
+
imported: "vitePlugin"
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
getManualInstructions(detection, reason) {
|
|
207
|
+
return [
|
|
208
|
+
`import { vitePlugin as inspecto } from '@inspecto-dev/plugin'`,
|
|
209
|
+
"",
|
|
210
|
+
"// Add to your plugins array:",
|
|
211
|
+
`plugins: [`,
|
|
212
|
+
` process.env.NODE_ENV !== 'production' && inspecto(),`,
|
|
213
|
+
` ...otherPlugins`,
|
|
214
|
+
`].filter(Boolean)`
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/inject/strategies/webpack.ts
|
|
220
|
+
var WebpackStrategy = class {
|
|
221
|
+
name = "Webpack";
|
|
222
|
+
supports(tool) {
|
|
223
|
+
return tool === "webpack";
|
|
224
|
+
}
|
|
225
|
+
inject(options) {
|
|
226
|
+
throw new Error("Webpack requires manual plugin configuration");
|
|
227
|
+
}
|
|
228
|
+
getManualInstructions(detection, reason) {
|
|
229
|
+
const importPkg = detection.isLegacyWebpack ? "@inspecto-dev/plugin/legacy/webpack4" : "@inspecto-dev/plugin";
|
|
230
|
+
const pluginName = detection.isLegacyWebpack ? "webpack4Plugin" : "webpackPlugin";
|
|
231
|
+
const pluginCall = detection.isLegacyWebpack ? `process.env.NODE_ENV !== 'production' && inspecto({
|
|
232
|
+
pathType: 'absolute',
|
|
233
|
+
escapeTags: ['Transition', 'AnimatePresence'],
|
|
234
|
+
})` : `process.env.NODE_ENV !== 'production' && inspecto()`;
|
|
235
|
+
return [
|
|
236
|
+
`import { ${pluginName} as inspecto } from '${importPkg}'`,
|
|
237
|
+
"",
|
|
238
|
+
"// Add to your plugins array:",
|
|
239
|
+
`plugins: [`,
|
|
240
|
+
` ${pluginCall},`,
|
|
241
|
+
` ...otherPlugins`,
|
|
242
|
+
`].filter(Boolean)`
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/inject/strategies/rspack.ts
|
|
248
|
+
var RspackStrategy = class {
|
|
249
|
+
name = "Rspack";
|
|
250
|
+
supports(tool) {
|
|
251
|
+
return tool === "rspack";
|
|
252
|
+
}
|
|
253
|
+
inject(options) {
|
|
254
|
+
throw new Error("Rspack requires manual plugin configuration");
|
|
255
|
+
}
|
|
256
|
+
getManualInstructions(detection, reason) {
|
|
257
|
+
const importPkg = detection.isLegacyRspack ? "@inspecto-dev/plugin/legacy/rspack" : "@inspecto-dev/plugin";
|
|
258
|
+
const pluginCall = detection.isLegacyRspack ? `process.env.NODE_ENV !== 'production' && inspecto({
|
|
259
|
+
pathType: 'absolute',
|
|
260
|
+
escapeTags: ['Transition', 'AnimatePresence'],
|
|
261
|
+
})` : `process.env.NODE_ENV !== 'production' && inspecto()`;
|
|
262
|
+
return [
|
|
263
|
+
`import { rspackPlugin as inspecto } from '${importPkg}'`,
|
|
264
|
+
"",
|
|
265
|
+
"// Add to your plugins array:",
|
|
266
|
+
`plugins: [`,
|
|
267
|
+
` ${pluginCall},`,
|
|
268
|
+
` ...otherPlugins`,
|
|
269
|
+
`].filter(Boolean)`
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/inject/strategies/rsbuild.ts
|
|
275
|
+
var RsbuildStrategy = class {
|
|
276
|
+
name = "Rsbuild";
|
|
277
|
+
supports(tool) {
|
|
278
|
+
return tool === "rsbuild";
|
|
279
|
+
}
|
|
280
|
+
inject(options) {
|
|
281
|
+
throw new Error("Rsbuild requires manual plugin configuration due to nested structure");
|
|
282
|
+
}
|
|
283
|
+
getManualInstructions(detection, reason) {
|
|
284
|
+
return [
|
|
285
|
+
`import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
|
|
286
|
+
"",
|
|
287
|
+
"// Add to tools.rspack:",
|
|
288
|
+
`tools: {`,
|
|
289
|
+
` rspack: {`,
|
|
290
|
+
` plugins: [`,
|
|
291
|
+
` process.env.NODE_ENV !== 'production' && inspecto(),`,
|
|
292
|
+
` ]`,
|
|
293
|
+
` }`,
|
|
294
|
+
`}`
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// src/inject/strategies/esbuild.ts
|
|
300
|
+
var EsbuildStrategy = class {
|
|
301
|
+
name = "esbuild";
|
|
302
|
+
supports(tool) {
|
|
303
|
+
return tool === "esbuild";
|
|
304
|
+
}
|
|
305
|
+
inject(options) {
|
|
306
|
+
throw new Error("Esbuild requires manual plugin configuration");
|
|
307
|
+
}
|
|
308
|
+
getManualInstructions(detection, reason) {
|
|
309
|
+
return [
|
|
310
|
+
`1. Update your esbuild config (${detection.configPath}):`,
|
|
311
|
+
`import { esbuildPlugin as inspecto } from '@inspecto-dev/plugin'`,
|
|
312
|
+
"",
|
|
313
|
+
"// Add to your plugins array:",
|
|
314
|
+
`plugins: [`,
|
|
315
|
+
` process.env.NODE_ENV !== 'production' && inspecto(),`,
|
|
316
|
+
` ...otherPlugins`,
|
|
317
|
+
`].filter(Boolean)`,
|
|
318
|
+
"",
|
|
319
|
+
"2. Initialize the client in your app entry (e.g., main.js / index.js):",
|
|
320
|
+
`import { mountInspector } from '@inspecto-dev/core'`,
|
|
321
|
+
"",
|
|
322
|
+
"// Call this before your app renders",
|
|
323
|
+
`if (process.env.NODE_ENV !== 'production') {`,
|
|
324
|
+
` mountInspector()`,
|
|
325
|
+
`}`
|
|
326
|
+
];
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// src/inject/strategies/rollup.ts
|
|
331
|
+
var RollupStrategy = class {
|
|
332
|
+
name = "Rollup";
|
|
333
|
+
supports(tool) {
|
|
334
|
+
return tool === "rollup";
|
|
335
|
+
}
|
|
336
|
+
inject(options) {
|
|
337
|
+
throw new Error("Rollup requires manual plugin configuration");
|
|
338
|
+
}
|
|
339
|
+
getManualInstructions(detection, reason) {
|
|
340
|
+
return [
|
|
341
|
+
`1. Update your rollup config (${detection.configPath}):`,
|
|
342
|
+
`import { rollupPlugin as inspecto } from '@inspecto-dev/plugin'`,
|
|
343
|
+
"",
|
|
344
|
+
"// Add to your plugins array:",
|
|
345
|
+
`plugins: [`,
|
|
346
|
+
` process.env.NODE_ENV !== 'production' && inspecto(),`,
|
|
347
|
+
` ...otherPlugins`,
|
|
348
|
+
`].filter(Boolean)`,
|
|
349
|
+
"",
|
|
350
|
+
"2. Initialize the client in your app entry (e.g., main.js / index.js):",
|
|
351
|
+
`import { mountInspector } from '@inspecto-dev/core'`,
|
|
352
|
+
"",
|
|
353
|
+
"// Call this before your app renders",
|
|
354
|
+
`if (process.env.NODE_ENV !== 'production') {`,
|
|
355
|
+
` mountInspector()`,
|
|
356
|
+
`}`
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// src/inject/strategies/index.ts
|
|
362
|
+
var STRATEGIES = [
|
|
363
|
+
new ViteStrategy(),
|
|
364
|
+
new WebpackStrategy(),
|
|
365
|
+
new RspackStrategy(),
|
|
366
|
+
new RsbuildStrategy(),
|
|
367
|
+
new EsbuildStrategy(),
|
|
368
|
+
new RollupStrategy()
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
// src/inject/ast-injector.ts
|
|
372
|
+
function printManualInstructions(strategy, detection, reason, quiet = false) {
|
|
373
|
+
if (quiet) return;
|
|
374
|
+
log.warn(`Could not automatically configure ${detection.configPath}`);
|
|
375
|
+
log.hint(`(reason: ${reason})`);
|
|
376
|
+
log.blank();
|
|
377
|
+
log.hint("Please add the following manually:");
|
|
378
|
+
if (strategy) {
|
|
379
|
+
const instructions = strategy.getManualInstructions(detection, reason);
|
|
380
|
+
log.copyableCodeBlock(instructions);
|
|
381
|
+
} else {
|
|
382
|
+
log.error(`Unsupported build tool: ${detection.tool}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function isAlreadyInjected(content) {
|
|
386
|
+
const normalized = content.replace(/\s+/g, " ");
|
|
387
|
+
const importPlugin = /import\s+(.+?)\s+from\s+['"]@inspecto-dev\/plugin['"]/g;
|
|
388
|
+
const requirePlugin = /require\(['"]@inspecto-dev\/plugin['"]\)/;
|
|
389
|
+
const legacyImport = /import\s+.*ai-dev-inspector/.test(normalized);
|
|
390
|
+
const legacyRequire = /require\(['"]ai-dev-inspector['"]\)/.test(normalized);
|
|
391
|
+
if (legacyImport || legacyRequire || requirePlugin.test(normalized)) return true;
|
|
392
|
+
let match;
|
|
393
|
+
importPlugin.lastIndex = 0;
|
|
394
|
+
while (match = importPlugin.exec(normalized)) {
|
|
395
|
+
const importClause = match[1] || "";
|
|
396
|
+
if (/inspecto/.test(importClause) || /vitePlugin/.test(importClause)) {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
async function injectPlugin(root, detection, dryRun, quiet = false) {
|
|
403
|
+
const configPath = path3.join(root, detection.configPath);
|
|
404
|
+
const mutations = [];
|
|
405
|
+
const strategy = STRATEGIES.find((s) => s.supports(detection.tool));
|
|
406
|
+
const content = await readFile(configPath);
|
|
407
|
+
if (!content) {
|
|
408
|
+
printManualInstructions(strategy, detection, "config file not readable", quiet);
|
|
409
|
+
return { success: false, mutations, failureReason: "config file not readable" };
|
|
410
|
+
}
|
|
411
|
+
if (isAlreadyInjected(content)) {
|
|
412
|
+
if (!quiet) {
|
|
413
|
+
log.success(`Plugin already configured in ${detection.configPath} (skipped)`);
|
|
414
|
+
}
|
|
415
|
+
mutations.push({
|
|
416
|
+
type: "file_modified",
|
|
417
|
+
path: detection.configPath,
|
|
418
|
+
description: "Previously configured inspecto() plugin"
|
|
419
|
+
});
|
|
420
|
+
return { success: true, mutations };
|
|
421
|
+
}
|
|
422
|
+
if (!strategy) {
|
|
423
|
+
printManualInstructions(
|
|
424
|
+
strategy,
|
|
425
|
+
detection,
|
|
426
|
+
`No injection strategy found for ${detection.tool}`,
|
|
427
|
+
quiet
|
|
428
|
+
);
|
|
429
|
+
return { success: false, mutations, failureReason: "No strategy found" };
|
|
430
|
+
}
|
|
431
|
+
if (dryRun) {
|
|
432
|
+
if (!quiet) {
|
|
433
|
+
log.dryRun(`Would automatically configure plugin in ${detection.configPath}`);
|
|
434
|
+
}
|
|
435
|
+
return { success: true, mutations: [] };
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
const mod = await loadFile(configPath);
|
|
439
|
+
strategy.inject({
|
|
440
|
+
mod,
|
|
441
|
+
detection
|
|
442
|
+
});
|
|
443
|
+
await writeAstFile(mod, configPath);
|
|
444
|
+
mutations.push({
|
|
445
|
+
type: "file_modified",
|
|
446
|
+
path: detection.configPath,
|
|
447
|
+
description: "Automatically configured inspecto() plugin"
|
|
448
|
+
});
|
|
449
|
+
if (!quiet) {
|
|
450
|
+
log.success(`Configured plugin in ${detection.configPath}`);
|
|
451
|
+
}
|
|
452
|
+
return { success: true, mutations };
|
|
453
|
+
} catch (err) {
|
|
454
|
+
printManualInstructions(
|
|
455
|
+
strategy,
|
|
456
|
+
detection,
|
|
457
|
+
`Automatic configuration unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
458
|
+
quiet
|
|
459
|
+
);
|
|
460
|
+
return {
|
|
461
|
+
success: false,
|
|
462
|
+
mutations,
|
|
463
|
+
failureReason: "Automatic configuration unavailable"
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/utils/exec.ts
|
|
469
|
+
import { execFile, exec as execCb } from "child_process";
|
|
470
|
+
import { promisify } from "util";
|
|
471
|
+
var execFileAsync = promisify(execFile);
|
|
472
|
+
var execAsync = promisify(execCb);
|
|
473
|
+
async function run(command, args, cwd) {
|
|
474
|
+
const result = await execFileAsync(command, args, {
|
|
475
|
+
cwd,
|
|
476
|
+
timeout: 6e4,
|
|
477
|
+
env: { ...process.env }
|
|
478
|
+
});
|
|
479
|
+
return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
480
|
+
}
|
|
481
|
+
async function shell(command, cwd) {
|
|
482
|
+
const result = await execAsync(command, {
|
|
483
|
+
cwd,
|
|
484
|
+
timeout: 6e4,
|
|
485
|
+
env: { ...process.env }
|
|
486
|
+
});
|
|
487
|
+
return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
488
|
+
}
|
|
489
|
+
async function which(bin) {
|
|
490
|
+
try {
|
|
491
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
492
|
+
await run(cmd, [bin]);
|
|
493
|
+
return true;
|
|
494
|
+
} catch {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/inject/extension.ts
|
|
500
|
+
var EXTENSION_ID = "inspecto.inspecto";
|
|
501
|
+
var VSCODE_PATHS = {
|
|
502
|
+
darwin: [
|
|
503
|
+
"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
|
|
504
|
+
"/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders",
|
|
505
|
+
`${process.env.HOME}/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code`
|
|
506
|
+
],
|
|
507
|
+
linux: ["/usr/bin/code", "/usr/share/code/bin/code", "/snap/bin/code", "/usr/bin/code-insiders"],
|
|
508
|
+
win32: [
|
|
509
|
+
`${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code\\bin\\code.cmd`,
|
|
510
|
+
`${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code Insiders\\bin\\code-insiders.cmd`,
|
|
511
|
+
`${process.env.PROGRAMFILES}\\Microsoft VS Code\\bin\\code.cmd`
|
|
512
|
+
]
|
|
513
|
+
};
|
|
514
|
+
async function findVSCodeBinary() {
|
|
515
|
+
const platform = process.platform;
|
|
516
|
+
const candidates = VSCODE_PATHS[platform] || [];
|
|
517
|
+
for (const candidate of candidates) {
|
|
518
|
+
if (await exists(candidate)) {
|
|
519
|
+
return candidate;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (await which("code-insiders")) {
|
|
523
|
+
return "code-insiders";
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
async function tryOpenURI(uri) {
|
|
528
|
+
try {
|
|
529
|
+
const platform = process.platform;
|
|
530
|
+
if (platform === "win32") {
|
|
531
|
+
await shell(`cmd /c start "" "${uri}"`);
|
|
532
|
+
} else {
|
|
533
|
+
const openCmd = platform === "darwin" ? "open" : "xdg-open";
|
|
534
|
+
await shell(`${openCmd} "${uri}"`);
|
|
535
|
+
}
|
|
536
|
+
return true;
|
|
537
|
+
} catch {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async function installExtension(dryRun, ide, quiet = false) {
|
|
542
|
+
if (dryRun) {
|
|
543
|
+
if (!quiet) {
|
|
544
|
+
log.dryRun("Would attempt to install VS Code extension");
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
const isVSCode = !ide || ide === "vscode";
|
|
549
|
+
if (isVSCode) {
|
|
550
|
+
if (await which("code")) {
|
|
551
|
+
try {
|
|
552
|
+
await run("code", ["--install-extension", EXTENSION_ID]);
|
|
553
|
+
if (!quiet) {
|
|
554
|
+
log.success("VS Code extension installed via CLI");
|
|
555
|
+
}
|
|
556
|
+
return { type: "extension_installed", id: EXTENSION_ID };
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const codePath = await findVSCodeBinary();
|
|
561
|
+
if (codePath) {
|
|
562
|
+
try {
|
|
563
|
+
await run(codePath, ["--install-extension", EXTENSION_ID]);
|
|
564
|
+
if (!quiet) {
|
|
565
|
+
log.success("VS Code extension installed via binary path");
|
|
566
|
+
log.info(
|
|
567
|
+
'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future'
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return { type: "extension_installed", id: EXTENSION_ID };
|
|
571
|
+
} catch {
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const uri = `vscode:extension/${EXTENSION_ID}`;
|
|
575
|
+
if (await tryOpenURI(uri)) {
|
|
576
|
+
if (!quiet) {
|
|
577
|
+
log.warn("Opened extension page in VS Code");
|
|
578
|
+
log.hint('Please click "Install" in the opened VS Code window to complete setup.');
|
|
579
|
+
}
|
|
580
|
+
return { type: "extension_installed", id: EXTENSION_ID, manual_action_required: true };
|
|
581
|
+
}
|
|
582
|
+
if (!quiet) {
|
|
583
|
+
log.warn("Could not auto-install VS Code extension");
|
|
584
|
+
log.hint("Please install it manually to enable Inspector features:");
|
|
585
|
+
log.hint(" 1. Open VS Code");
|
|
586
|
+
log.hint(" 2. Press Ctrl+Shift+X (or Cmd+Shift+X)");
|
|
587
|
+
log.hint(' 3. Search for "Inspecto"');
|
|
588
|
+
log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`);
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
if (!quiet) {
|
|
593
|
+
log.warn(`Could not auto-install extension for ${ide}`);
|
|
594
|
+
log.hint("Please install it manually to enable Inspector features:");
|
|
595
|
+
log.hint(
|
|
596
|
+
" 1. Download the latest .vsix file (Open VSX: https://open-vsx.org/extension/inspecto/inspecto)"
|
|
597
|
+
);
|
|
598
|
+
log.hint(` 2. Open ${ide}`);
|
|
599
|
+
log.hint(" 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)");
|
|
600
|
+
log.hint(' 4. Type and select "Extensions: Install from VSIX..."');
|
|
601
|
+
log.hint(" 5. Select the downloaded .vsix file");
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
async function isExtensionInstalled() {
|
|
606
|
+
try {
|
|
607
|
+
if (await which("code")) {
|
|
608
|
+
const { stdout } = await run("code", ["--list-extensions"]);
|
|
609
|
+
return stdout.toLowerCase().includes(EXTENSION_ID);
|
|
610
|
+
}
|
|
611
|
+
const codePath = await findVSCodeBinary();
|
|
612
|
+
if (codePath) {
|
|
613
|
+
const { stdout } = await run(codePath, ["--list-extensions"]);
|
|
614
|
+
return stdout.toLowerCase().includes(EXTENSION_ID);
|
|
615
|
+
}
|
|
616
|
+
return false;
|
|
617
|
+
} catch {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/inject/gitignore.ts
|
|
623
|
+
import path4 from "path";
|
|
624
|
+
var DEFAULT_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
|
|
625
|
+
var SHARED_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
|
|
626
|
+
async function updateGitignore(root, shared, dryRun, quiet = false) {
|
|
627
|
+
const gitignorePath = path4.join(root, ".gitignore");
|
|
628
|
+
let content = await readFile(gitignorePath) ?? "";
|
|
629
|
+
const desiredRules = shared ? SHARED_RULES : DEFAULT_RULES;
|
|
630
|
+
const hasGlobalRule = content.match(/^\.inspecto\/\s*$/m) !== null;
|
|
631
|
+
if (hasGlobalRule) {
|
|
632
|
+
content = content.replace(/^\.inspecto\/\s*$/gm, SHARED_RULES.join("\n"));
|
|
633
|
+
if (!dryRun) {
|
|
634
|
+
await writeFile(gitignorePath, content);
|
|
635
|
+
}
|
|
636
|
+
if (!quiet) {
|
|
637
|
+
log.success("Updated .gitignore: .inspecto/ is no longer fully ignored");
|
|
638
|
+
}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const missingRules = desiredRules.filter((rule) => !content.includes(rule));
|
|
642
|
+
if (missingRules.length === 0) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const section = "\n# Inspecto\n" + missingRules.join("\n") + "\n";
|
|
646
|
+
content = content.trimEnd() + "\n" + section;
|
|
647
|
+
if (dryRun) {
|
|
648
|
+
if (!quiet) {
|
|
649
|
+
log.dryRun(`Would update .gitignore with: ${missingRules.join(", ")}`);
|
|
650
|
+
}
|
|
651
|
+
} else {
|
|
652
|
+
await writeFile(gitignorePath, content);
|
|
653
|
+
if (!quiet) {
|
|
654
|
+
log.success("Updated .gitignore");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function cleanGitignore(root) {
|
|
659
|
+
const gitignorePath = path4.join(root, ".gitignore");
|
|
660
|
+
const content = await readFile(gitignorePath);
|
|
661
|
+
if (!content) return;
|
|
662
|
+
const cleaned = content.replace(/^# Inspecto\s*$/m, "").replace(/^\.inspecto\/?\s*$/gm, "").replace(/^\.inspecto\/install\.lock\s*$/gm, "").replace(/^\.inspecto\/cache\.json\s*$/gm, "").replace(/^\.inspecto\/\*\.local\.json\s*$/gm, "").replace(/\n{3,}/g, "\n\n");
|
|
663
|
+
await writeFile(gitignorePath, cleaned);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/onboarding/apply.ts
|
|
667
|
+
function shellQuote(value) {
|
|
668
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
669
|
+
}
|
|
670
|
+
function resolveRuntimePackages() {
|
|
671
|
+
const devRepo = process.env.INSPECTO_DEV_REPO;
|
|
672
|
+
if (!devRepo) {
|
|
673
|
+
return {
|
|
674
|
+
installSpec: "@inspecto-dev/plugin @inspecto-dev/core",
|
|
675
|
+
installedDependencyNames: ["@inspecto-dev/plugin", "@inspecto-dev/core"]
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const normalizedRepo = path5.resolve(devRepo);
|
|
679
|
+
return {
|
|
680
|
+
installSpec: [
|
|
681
|
+
shellQuote(path5.join(normalizedRepo, "packages/plugin")),
|
|
682
|
+
shellQuote(path5.join(normalizedRepo, "packages/core"))
|
|
683
|
+
].join(" "),
|
|
684
|
+
installedDependencyNames: ["@inspecto-dev/plugin", "@inspecto-dev/core"]
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function resultStatus(nextSteps) {
|
|
688
|
+
return nextSteps.length > 0 ? "warning" : "ok";
|
|
689
|
+
}
|
|
690
|
+
function manualPlanSteps(plan2) {
|
|
691
|
+
return [
|
|
692
|
+
...plan2.blockers.map((blocker) => blocker.message),
|
|
693
|
+
...plan2.actions.filter((action) => action.type === "manual_step").map((action) => action.description)
|
|
694
|
+
];
|
|
695
|
+
}
|
|
696
|
+
async function applyOnboardingPlan(input) {
|
|
697
|
+
return applyOnboardingPlanInternal(input);
|
|
698
|
+
}
|
|
699
|
+
function createReporter(quiet = false) {
|
|
700
|
+
if (quiet) {
|
|
701
|
+
return {
|
|
702
|
+
warn() {
|
|
703
|
+
},
|
|
704
|
+
success() {
|
|
705
|
+
},
|
|
706
|
+
error() {
|
|
707
|
+
},
|
|
708
|
+
hint() {
|
|
709
|
+
},
|
|
710
|
+
dryRun() {
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
warn(text) {
|
|
716
|
+
log.warn(text);
|
|
717
|
+
},
|
|
718
|
+
success(text) {
|
|
719
|
+
log.success(text);
|
|
720
|
+
},
|
|
721
|
+
error(text) {
|
|
722
|
+
log.error(text);
|
|
723
|
+
},
|
|
724
|
+
hint(text) {
|
|
725
|
+
log.hint(text);
|
|
726
|
+
},
|
|
727
|
+
dryRun(text) {
|
|
728
|
+
log.dryRun(text);
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function createSpinner(text, quiet = false) {
|
|
733
|
+
if (quiet) {
|
|
734
|
+
return {
|
|
735
|
+
start() {
|
|
736
|
+
},
|
|
737
|
+
succeed() {
|
|
738
|
+
},
|
|
739
|
+
fail() {
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
const spinner = ora(text);
|
|
744
|
+
return {
|
|
745
|
+
start() {
|
|
746
|
+
spinner.start();
|
|
747
|
+
},
|
|
748
|
+
succeed(successText) {
|
|
749
|
+
spinner.succeed(successText);
|
|
750
|
+
},
|
|
751
|
+
fail(failureText) {
|
|
752
|
+
spinner.fail(failureText);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
async function applyOnboardingPlanInternal(input) {
|
|
757
|
+
const reporter = createReporter(input.options.quiet);
|
|
758
|
+
if (input.plan && input.plan.strategy !== "supported" && !input.allowManualPlanApply) {
|
|
759
|
+
return {
|
|
760
|
+
status: input.plan.status,
|
|
761
|
+
mutations: [],
|
|
762
|
+
postInstall: {
|
|
763
|
+
installFailed: false,
|
|
764
|
+
injectionFailed: false,
|
|
765
|
+
manualExtensionInstallNeeded: false,
|
|
766
|
+
nextSteps: manualPlanSteps(input.plan)
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const mutations = [];
|
|
771
|
+
const settingsDir = path5.join(input.projectRoot, ".inspecto");
|
|
772
|
+
const settingsFileName = input.options.shared ? "settings.json" : "settings.local.json";
|
|
773
|
+
const promptsFileName = input.options.shared ? "prompts.json" : "prompts.local.json";
|
|
774
|
+
const settingsPath = path5.join(settingsDir, settingsFileName);
|
|
775
|
+
const promptsPath = path5.join(settingsDir, promptsFileName);
|
|
776
|
+
const runtimePackages = resolveRuntimePackages();
|
|
777
|
+
const installCmd = getInstallCommand(input.packageManager, runtimePackages.installSpec);
|
|
778
|
+
const nextSteps = [];
|
|
779
|
+
let installFailed = false;
|
|
780
|
+
if (input.options.skipInstall) {
|
|
781
|
+
reporter.warn("Skipping dependency installation (--skip-install)");
|
|
782
|
+
} else if (input.options.dryRun) {
|
|
783
|
+
reporter.dryRun(`Would run: ${installCmd}`);
|
|
784
|
+
} else {
|
|
785
|
+
const spinner = createSpinner(
|
|
786
|
+
`Installing devDependencies via: ${installCmd}`,
|
|
787
|
+
input.options.quiet
|
|
788
|
+
);
|
|
789
|
+
try {
|
|
790
|
+
spinner.start();
|
|
791
|
+
await shell(installCmd, input.projectRoot);
|
|
792
|
+
spinner.succeed("Dependencies installed successfully");
|
|
793
|
+
reporter.success("Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies");
|
|
794
|
+
for (const name of runtimePackages.installedDependencyNames) {
|
|
795
|
+
mutations.push({ type: "dependency_added", name, dev: true });
|
|
796
|
+
}
|
|
797
|
+
} catch (error) {
|
|
798
|
+
spinner.fail("Dependency installation failed");
|
|
799
|
+
installFailed = true;
|
|
800
|
+
reporter.error(`Failed to install dependency: ${error?.message || "Unknown error"}`);
|
|
801
|
+
reporter.hint(`Run manually in ${input.projectRoot}: ${installCmd}`);
|
|
802
|
+
reporter.hint(
|
|
803
|
+
"Setup will continue without dependencies, but Inspecto may not run until installation succeeds."
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
let injectionFailed = Boolean(input.injectionSkippedRequiresManualConfig);
|
|
808
|
+
for (const target of input.supportedBuildTargets) {
|
|
809
|
+
const result = await injectPlugin(
|
|
810
|
+
input.repoRoot,
|
|
811
|
+
target,
|
|
812
|
+
input.options.dryRun,
|
|
813
|
+
input.options.quiet ?? false
|
|
814
|
+
);
|
|
815
|
+
if (result.success) {
|
|
816
|
+
mutations.push(...result.mutations);
|
|
817
|
+
} else {
|
|
818
|
+
injectionFailed = true;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (await exists(settingsPath)) {
|
|
822
|
+
const existingSettings = await readJSON(settingsPath);
|
|
823
|
+
if (existingSettings === null) {
|
|
824
|
+
reporter.warn(`.inspecto/${settingsFileName} exists but contains invalid JSON`);
|
|
825
|
+
reporter.hint("Please fix the syntax errors manually, or delete it and re-run init");
|
|
826
|
+
nextSteps.push(`Fix .inspecto/${settingsFileName} or delete it and rerun Inspecto setup.`);
|
|
827
|
+
} else {
|
|
828
|
+
reporter.success(`.inspecto/${settingsFileName} already exists (skipped)`);
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
const defaultSettings = {};
|
|
832
|
+
if (input.selectedIDE?.supported) {
|
|
833
|
+
defaultSettings.ide = input.selectedIDE.ide.toLowerCase() === "vscode" ? "vscode" : input.selectedIDE.ide.toLowerCase();
|
|
834
|
+
}
|
|
835
|
+
if (input.providerDefault) {
|
|
836
|
+
defaultSettings["provider.default"] = input.providerDefault;
|
|
837
|
+
}
|
|
838
|
+
if (input.options.dryRun) {
|
|
839
|
+
reporter.dryRun(`Would create .inspecto/${settingsFileName}`);
|
|
840
|
+
} else {
|
|
841
|
+
await writeJSON(settingsPath, defaultSettings);
|
|
842
|
+
reporter.success(`Created .inspecto/${settingsFileName}`);
|
|
843
|
+
mutations.push({ type: "file_created", path: `.inspecto/${settingsFileName}` });
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (await exists(promptsPath)) {
|
|
847
|
+
reporter.success(`.inspecto/${promptsFileName} already exists (skipped)`);
|
|
848
|
+
} else if (input.options.dryRun) {
|
|
849
|
+
reporter.dryRun(`Would create .inspecto/${promptsFileName}`);
|
|
850
|
+
} else {
|
|
851
|
+
await writeJSON(promptsPath, []);
|
|
852
|
+
reporter.success(`Created .inspecto/${promptsFileName}`);
|
|
853
|
+
mutations.push({ type: "file_created", path: `.inspecto/${promptsFileName}` });
|
|
854
|
+
}
|
|
855
|
+
if (!input.options.dryRun) {
|
|
856
|
+
await updateGitignore(
|
|
857
|
+
input.projectRoot,
|
|
858
|
+
input.options.shared,
|
|
859
|
+
input.options.dryRun,
|
|
860
|
+
input.options.quiet ?? false
|
|
861
|
+
);
|
|
862
|
+
mutations.push({
|
|
863
|
+
type: "file_modified",
|
|
864
|
+
path: ".gitignore",
|
|
865
|
+
description: "Appended .inspecto/ ignore rules"
|
|
866
|
+
});
|
|
867
|
+
} else {
|
|
868
|
+
reporter.dryRun("Would update .gitignore");
|
|
869
|
+
}
|
|
870
|
+
if (!input.options.dryRun && mutations.length > 0) {
|
|
871
|
+
const lock = {
|
|
872
|
+
version: "1.0.0",
|
|
873
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
874
|
+
mutations
|
|
875
|
+
};
|
|
876
|
+
await writeJSON(path5.join(settingsDir, "install.lock"), lock);
|
|
877
|
+
}
|
|
878
|
+
const shouldInstallExt = !input.options.noExtension && (!input.selectedIDE || input.selectedIDE.supported);
|
|
879
|
+
let manualExtensionInstallNeeded = false;
|
|
880
|
+
if (input.options.noExtension) {
|
|
881
|
+
reporter.warn("Skipping IDE extension (--no-extension)");
|
|
882
|
+
} else if (shouldInstallExt) {
|
|
883
|
+
const extMutation = await installExtension(
|
|
884
|
+
input.options.dryRun,
|
|
885
|
+
input.selectedIDE?.ide,
|
|
886
|
+
input.options.quiet ?? false
|
|
887
|
+
);
|
|
888
|
+
if (extMutation && !input.options.dryRun) {
|
|
889
|
+
mutations.push(extMutation);
|
|
890
|
+
if (extMutation.manual_action_required) {
|
|
891
|
+
manualExtensionInstallNeeded = true;
|
|
892
|
+
}
|
|
893
|
+
const lockPath = path5.join(settingsDir, "install.lock");
|
|
894
|
+
const lock = await readJSON(lockPath);
|
|
895
|
+
if (lock) {
|
|
896
|
+
lock.mutations = mutations;
|
|
897
|
+
await writeJSON(lockPath, lock);
|
|
898
|
+
}
|
|
899
|
+
} else if (extMutation === null && !input.options.dryRun) {
|
|
900
|
+
manualExtensionInstallNeeded = true;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (!input.options.dryRun) {
|
|
904
|
+
if (installFailed) {
|
|
905
|
+
nextSteps.push(`Install dependencies manually in ${input.projectRoot}: ${installCmd}`);
|
|
906
|
+
}
|
|
907
|
+
if (injectionFailed) {
|
|
908
|
+
nextSteps.push(
|
|
909
|
+
"Plugin injection skipped. Follow manual instructions printed above to update your config."
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
if (manualExtensionInstallNeeded) {
|
|
913
|
+
nextSteps.push("Install the Inspecto IDE extension manually");
|
|
914
|
+
}
|
|
915
|
+
if (input.manualConfigRequiredFor === "Nuxt") {
|
|
916
|
+
nextSteps.push(
|
|
917
|
+
"Nuxt detected\u2014please follow the Nuxt instructions printed above to finish setup."
|
|
918
|
+
);
|
|
919
|
+
} else if (input.manualConfigRequiredFor === "Next.js") {
|
|
920
|
+
nextSteps.push(
|
|
921
|
+
"Next.js detected\u2014please follow the Next.js instructions printed above to finish setup."
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
status: resultStatus(nextSteps),
|
|
927
|
+
mutations,
|
|
928
|
+
postInstall: {
|
|
929
|
+
installFailed,
|
|
930
|
+
injectionFailed,
|
|
931
|
+
manualExtensionInstallNeeded,
|
|
932
|
+
nextSteps
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/detect/build-tool.ts
|
|
938
|
+
import path6 from "path";
|
|
939
|
+
import fs2 from "fs/promises";
|
|
940
|
+
import { createRequire } from "module";
|
|
941
|
+
function isPackageResolvable(pkgName, root) {
|
|
942
|
+
try {
|
|
943
|
+
const require2 = createRequire(path6.join(root, "package.json"));
|
|
944
|
+
try {
|
|
945
|
+
require2.resolve(`${pkgName}/package.json`, { paths: [root] });
|
|
946
|
+
return true;
|
|
947
|
+
} catch {
|
|
948
|
+
require2.resolve(pkgName, { paths: [root] });
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
} catch {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async function getResolvedPackageVersion(pkgName, root) {
|
|
956
|
+
try {
|
|
957
|
+
const require2 = createRequire(path6.join(root, "package.json"));
|
|
958
|
+
const pkgJsonPath = require2.resolve(`${pkgName}/package.json`, { paths: [root] });
|
|
959
|
+
const pkg = await readJSON(pkgJsonPath);
|
|
960
|
+
return pkg?.version || null;
|
|
961
|
+
} catch {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
var SUPPORTED_PATTERNS = [
|
|
966
|
+
{
|
|
967
|
+
tool: "vite",
|
|
968
|
+
files: [
|
|
969
|
+
"vite.config.ts",
|
|
970
|
+
"vite.config.js",
|
|
971
|
+
"vite.config.mts",
|
|
972
|
+
"vite.config.mjs",
|
|
973
|
+
"vite.config.cjs",
|
|
974
|
+
"vite.config.cts"
|
|
975
|
+
],
|
|
976
|
+
label: "Vite"
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
tool: "rspack",
|
|
980
|
+
files: ["rspack.config.js", "rspack.config.ts", "rspack.config.mjs"],
|
|
981
|
+
label: "Rspack"
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
tool: "rsbuild",
|
|
985
|
+
files: ["rsbuild.config.js", "rsbuild.config.ts", "rsbuild.config.mjs"],
|
|
986
|
+
label: "Rsbuild"
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
tool: "webpack",
|
|
990
|
+
files: ["webpack.config.js", "webpack.config.ts", "webpack.config.mjs", "webpack.config.cjs"],
|
|
991
|
+
label: "Webpack"
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
tool: "esbuild",
|
|
995
|
+
files: ["esbuild.config.js", "esbuild.config.ts", "esbuild.config.mjs", "build.js", "build.ts"],
|
|
996
|
+
label: "esbuild"
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
tool: "rollup",
|
|
1000
|
+
files: ["rollup.config.js", "rollup.config.ts", "rollup.config.mjs"],
|
|
1001
|
+
label: "Rollup"
|
|
1002
|
+
}
|
|
1003
|
+
];
|
|
1004
|
+
var UNSUPPORTED_META = [
|
|
1005
|
+
{ name: "Next.js", dep: "next", files: ["next.config.mjs", "next.config.js", "next.config.ts"] },
|
|
1006
|
+
{ name: "Nuxt", dep: "nuxt", files: ["nuxt.config.ts", "nuxt.config.js"] },
|
|
1007
|
+
{ name: "Remix", dep: "@remix-run/dev", files: ["remix.config.js", "remix.config.ts"] },
|
|
1008
|
+
{ name: "Astro", dep: "astro", files: ["astro.config.mjs", "astro.config.ts"] },
|
|
1009
|
+
{ name: "SvelteKit", dep: "@sveltejs/kit", files: ["svelte.config.js", "svelte.config.ts"] }
|
|
1010
|
+
];
|
|
1011
|
+
function normalizeRelativePath(root, filePath) {
|
|
1012
|
+
const relative = path6.relative(root, filePath);
|
|
1013
|
+
const normalized = relative.split(path6.sep).join("/");
|
|
1014
|
+
return normalized || path6.basename(filePath);
|
|
1015
|
+
}
|
|
1016
|
+
function createTargets(root, packagePaths) {
|
|
1017
|
+
if (!packagePaths || packagePaths.length === 0) {
|
|
1018
|
+
return [{ packagePath: "", absolutePath: root }];
|
|
1019
|
+
}
|
|
1020
|
+
return packagePaths.map((pkg) => ({
|
|
1021
|
+
packagePath: pkg,
|
|
1022
|
+
absolutePath: pkg ? path6.join(root, pkg) : root
|
|
1023
|
+
}));
|
|
1024
|
+
}
|
|
1025
|
+
async function getWorkspacePackagePatterns(root) {
|
|
1026
|
+
const pkg = await readJSON(
|
|
1027
|
+
path6.join(root, "package.json")
|
|
1028
|
+
);
|
|
1029
|
+
const workspaces = pkg?.workspaces;
|
|
1030
|
+
if (Array.isArray(workspaces)) {
|
|
1031
|
+
return workspaces;
|
|
1032
|
+
}
|
|
1033
|
+
if (workspaces && Array.isArray(workspaces.packages)) {
|
|
1034
|
+
return workspaces.packages;
|
|
1035
|
+
}
|
|
1036
|
+
const pnpmWorkspace = await readFile(path6.join(root, "pnpm-workspace.yaml"));
|
|
1037
|
+
if (!pnpmWorkspace) {
|
|
1038
|
+
return [];
|
|
1039
|
+
}
|
|
1040
|
+
const patterns = [];
|
|
1041
|
+
for (const line of pnpmWorkspace.split("\n")) {
|
|
1042
|
+
const match = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/);
|
|
1043
|
+
if (match?.[1]) {
|
|
1044
|
+
patterns.push(match[1]);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return patterns;
|
|
1048
|
+
}
|
|
1049
|
+
async function expandWorkspacePattern(root, pattern) {
|
|
1050
|
+
const normalized = pattern.replace(/\\/g, "/").replace(/\/$/, "");
|
|
1051
|
+
if (!normalized || normalized.startsWith("!")) {
|
|
1052
|
+
return [];
|
|
1053
|
+
}
|
|
1054
|
+
if (!normalized.includes("*")) {
|
|
1055
|
+
return await exists(path6.join(root, normalized)) ? [normalized] : [];
|
|
1056
|
+
}
|
|
1057
|
+
const starIndex = normalized.indexOf("*");
|
|
1058
|
+
const baseDir = normalized.slice(0, starIndex).replace(/\/$/, "");
|
|
1059
|
+
const suffix = normalized.slice(starIndex + 1);
|
|
1060
|
+
if (suffix && suffix !== "") {
|
|
1061
|
+
return [];
|
|
1062
|
+
}
|
|
1063
|
+
const absoluteBaseDir = path6.join(root, baseDir);
|
|
1064
|
+
if (!await exists(absoluteBaseDir)) {
|
|
1065
|
+
return [];
|
|
1066
|
+
}
|
|
1067
|
+
try {
|
|
1068
|
+
const entries = await fs2.readdir(absoluteBaseDir, { withFileTypes: true });
|
|
1069
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => path6.posix.join(baseDir, entry.name));
|
|
1070
|
+
} catch {
|
|
1071
|
+
return [];
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
async function detectWorkspaceTargets(root) {
|
|
1075
|
+
const patterns = await getWorkspacePackagePatterns(root);
|
|
1076
|
+
if (patterns.length === 0) {
|
|
1077
|
+
return [];
|
|
1078
|
+
}
|
|
1079
|
+
const packagePaths = /* @__PURE__ */ new Set();
|
|
1080
|
+
for (const pattern of patterns) {
|
|
1081
|
+
const expanded = await expandWorkspacePattern(root, pattern);
|
|
1082
|
+
for (const packagePath of expanded) {
|
|
1083
|
+
packagePaths.add(packagePath);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return Array.from(packagePaths).map((packagePath) => ({
|
|
1087
|
+
packagePath,
|
|
1088
|
+
absolutePath: path6.join(root, packagePath)
|
|
1089
|
+
}));
|
|
1090
|
+
}
|
|
1091
|
+
async function detectBuildTools(root, packagePaths) {
|
|
1092
|
+
const supported = [];
|
|
1093
|
+
const unsupported = /* @__PURE__ */ new Set();
|
|
1094
|
+
const explicitTargets = createTargets(root, packagePaths);
|
|
1095
|
+
const workspaceTargets = !packagePaths || packagePaths.length === 0 ? await detectWorkspaceTargets(root) : [];
|
|
1096
|
+
const targets = workspaceTargets.length > 0 ? workspaceTargets : explicitTargets;
|
|
1097
|
+
for (const target of targets) {
|
|
1098
|
+
const pkg = await readJSON(path6.join(target.absolutePath, "package.json"));
|
|
1099
|
+
const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
1100
|
+
const scripts = pkg?.scripts || {};
|
|
1101
|
+
const supportedChecks = SUPPORTED_PATTERNS.map(
|
|
1102
|
+
(pattern) => detectPattern({
|
|
1103
|
+
pattern,
|
|
1104
|
+
workspaceRoot: root,
|
|
1105
|
+
targetRoot: target.absolutePath,
|
|
1106
|
+
packagePath: target.packagePath,
|
|
1107
|
+
allDeps,
|
|
1108
|
+
scripts
|
|
1109
|
+
})
|
|
1110
|
+
);
|
|
1111
|
+
const supportedResults = await Promise.all(supportedChecks);
|
|
1112
|
+
for (const result of supportedResults) {
|
|
1113
|
+
if (result) {
|
|
1114
|
+
supported.push(result);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const unsupportedChecks = UNSUPPORTED_META.map(async (meta) => {
|
|
1118
|
+
if (!(meta.dep in allDeps)) return null;
|
|
1119
|
+
for (const file of meta.files) {
|
|
1120
|
+
if (await exists(path6.join(target.absolutePath, file))) {
|
|
1121
|
+
return meta.name;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return null;
|
|
1125
|
+
});
|
|
1126
|
+
const unsupportedResults = await Promise.all(unsupportedChecks);
|
|
1127
|
+
for (const result of unsupportedResults) {
|
|
1128
|
+
if (result) {
|
|
1129
|
+
unsupported.add(result);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return { supported, unsupported: Array.from(unsupported) };
|
|
1134
|
+
}
|
|
1135
|
+
async function detectPattern({
|
|
1136
|
+
pattern,
|
|
1137
|
+
workspaceRoot,
|
|
1138
|
+
targetRoot,
|
|
1139
|
+
packagePath,
|
|
1140
|
+
allDeps,
|
|
1141
|
+
scripts
|
|
1142
|
+
}) {
|
|
1143
|
+
let hasDep;
|
|
1144
|
+
let resolvedVersion = null;
|
|
1145
|
+
if (pattern.tool === "rspack") {
|
|
1146
|
+
const depName = allDeps["@rspack/cli"] ? "@rspack/cli" : "@rspack/core";
|
|
1147
|
+
hasDep = !!allDeps["@rspack/cli"] || !!allDeps["@rspack/core"] || isPackageResolvable("@rspack/core", targetRoot);
|
|
1148
|
+
if (hasDep) {
|
|
1149
|
+
resolvedVersion = allDeps[depName] || await getResolvedPackageVersion("@rspack/core", targetRoot);
|
|
1150
|
+
}
|
|
1151
|
+
} else if (pattern.tool === "webpack") {
|
|
1152
|
+
const depName = allDeps["webpack"] ? "webpack" : "webpack-cli";
|
|
1153
|
+
hasDep = !!allDeps["webpack"] || !!allDeps["webpack-cli"] || isPackageResolvable("webpack", targetRoot);
|
|
1154
|
+
if (hasDep) {
|
|
1155
|
+
resolvedVersion = allDeps[depName] || await getResolvedPackageVersion("webpack", targetRoot);
|
|
1156
|
+
}
|
|
1157
|
+
} else if (pattern.tool === "rsbuild") {
|
|
1158
|
+
hasDep = !!allDeps["@rsbuild/core"] || isPackageResolvable("@rsbuild/core", targetRoot);
|
|
1159
|
+
} else {
|
|
1160
|
+
hasDep = !!allDeps[pattern.tool] || isPackageResolvable(pattern.tool, targetRoot);
|
|
1161
|
+
}
|
|
1162
|
+
let detectedFile = "";
|
|
1163
|
+
let inferredFromScripts = false;
|
|
1164
|
+
if (pattern.tool === "esbuild" && !hasDep) {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
for (const file of pattern.files) {
|
|
1168
|
+
if (await exists(path6.join(targetRoot, file))) {
|
|
1169
|
+
detectedFile = file;
|
|
1170
|
+
break;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (hasDep && !detectedFile && (pattern.tool === "esbuild" || pattern.tool === "rollup" || pattern.tool === "webpack" || pattern.tool === "rspack" || pattern.tool === "rsbuild")) {
|
|
1174
|
+
for (const cmd of Object.values(scripts)) {
|
|
1175
|
+
if (cmd.includes("node ")) {
|
|
1176
|
+
const match = cmd.match(/node\s+([^\s]+\.(js|mjs|cjs|ts))/);
|
|
1177
|
+
if (match && match[1]) {
|
|
1178
|
+
if (await exists(path6.join(targetRoot, match[1]))) {
|
|
1179
|
+
if (cmd.includes(pattern.tool) || match[1].includes(pattern.tool)) {
|
|
1180
|
+
detectedFile = match[1];
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
} else if (cmd.includes(`${pattern.tool} `)) {
|
|
1186
|
+
if (pattern.tool === "webpack" || pattern.tool === "rspack") {
|
|
1187
|
+
const configMatch = cmd.match(/--config\s+([^\s]+)/);
|
|
1188
|
+
if (configMatch && configMatch[1]) {
|
|
1189
|
+
if (await exists(path6.join(targetRoot, configMatch[1]))) {
|
|
1190
|
+
detectedFile = configMatch[1];
|
|
1191
|
+
break;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (!detectedFile) {
|
|
1196
|
+
inferredFromScripts = true;
|
|
1197
|
+
detectedFile = "package.json (scripts)";
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (!detectedFile) {
|
|
1204
|
+
if (hasDep && (pattern.tool === "rollup" || pattern.tool === "webpack" || pattern.tool === "rspack" || pattern.tool === "esbuild")) {
|
|
1205
|
+
return {
|
|
1206
|
+
tool: pattern.tool,
|
|
1207
|
+
configPath: "package.json (dependency)",
|
|
1208
|
+
label: `${pattern.label} (detected via dependency)`,
|
|
1209
|
+
packagePath: packagePath || void 0
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
let isLegacyRspack = false;
|
|
1215
|
+
let isLegacyWebpack = false;
|
|
1216
|
+
if (pattern.tool === "rspack") {
|
|
1217
|
+
const version = resolvedVersion;
|
|
1218
|
+
if (version && (version.includes("0.3.") || version.includes("0.2.") || version.includes("0.1."))) {
|
|
1219
|
+
isLegacyRspack = true;
|
|
1220
|
+
}
|
|
1221
|
+
} else if (pattern.tool === "webpack") {
|
|
1222
|
+
const version = resolvedVersion;
|
|
1223
|
+
if (version && version.includes("^4") || version?.startsWith("4.")) {
|
|
1224
|
+
isLegacyWebpack = true;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
const absoluteConfig = path6.join(targetRoot, detectedFile);
|
|
1228
|
+
const relativeConfig = normalizeRelativePath(workspaceRoot, absoluteConfig);
|
|
1229
|
+
return {
|
|
1230
|
+
tool: pattern.tool,
|
|
1231
|
+
configPath: relativeConfig,
|
|
1232
|
+
label: `${pattern.label} (${relativeConfig})${isLegacyRspack ? " [Legacy]" : ""}${isLegacyWebpack ? " [Webpack 4]" : ""}${inferredFromScripts ? " [Scripts Detected]" : ""}`,
|
|
1233
|
+
isLegacyRspack,
|
|
1234
|
+
isLegacyWebpack,
|
|
1235
|
+
packagePath: packagePath || void 0
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function resolveInjectionTarget(detections) {
|
|
1239
|
+
if (detections.length === 0) return null;
|
|
1240
|
+
if (detections.length === 1) return detections[0];
|
|
1241
|
+
return "ambiguous";
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/detect/framework.ts
|
|
1245
|
+
import path7 from "path";
|
|
1246
|
+
import { createRequire as createRequire2 } from "module";
|
|
1247
|
+
var META_FRAMEWORK_MAP = {
|
|
1248
|
+
next: "react",
|
|
1249
|
+
nuxt: "vue",
|
|
1250
|
+
"@remix-run/react": "react",
|
|
1251
|
+
"@remix-run/dev": "react",
|
|
1252
|
+
"@vue/nuxt": "vue",
|
|
1253
|
+
"vite-plugin-vue": "vue",
|
|
1254
|
+
"@vitejs/plugin-vue": "vue",
|
|
1255
|
+
"@vitejs/plugin-react": "react",
|
|
1256
|
+
"@vitejs/plugin-react-swc": "react"
|
|
1257
|
+
};
|
|
1258
|
+
var SUPPORTED_FRAMEWORKS = [
|
|
1259
|
+
{ framework: "react", deps: ["react", "react-dom"] },
|
|
1260
|
+
{ framework: "vue", deps: ["vue"] }
|
|
1261
|
+
];
|
|
1262
|
+
var UNSUPPORTED_FRAMEWORKS = [
|
|
1263
|
+
{ name: "Solid", dep: "solid-js" },
|
|
1264
|
+
{ name: "Svelte", dep: "svelte" },
|
|
1265
|
+
{ name: "Angular", dep: "@angular/core" },
|
|
1266
|
+
{ name: "Preact", dep: "preact" },
|
|
1267
|
+
{ name: "Lit", dep: "lit" }
|
|
1268
|
+
];
|
|
1269
|
+
function isPackageResolvable2(pkgName, root) {
|
|
1270
|
+
try {
|
|
1271
|
+
const require2 = createRequire2(path7.join(root, "package.json"));
|
|
1272
|
+
try {
|
|
1273
|
+
require2.resolve(`${pkgName}/package.json`, { paths: [root] });
|
|
1274
|
+
return true;
|
|
1275
|
+
} catch {
|
|
1276
|
+
require2.resolve(pkgName, { paths: [root] });
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
} catch {
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async function detectFrameworks(root) {
|
|
1284
|
+
const pkg = await readJSON(path7.join(root, "package.json"));
|
|
1285
|
+
const allDeps = {
|
|
1286
|
+
...pkg?.dependencies || {},
|
|
1287
|
+
...pkg?.devDependencies || {},
|
|
1288
|
+
...pkg?.peerDependencies || {}
|
|
1289
|
+
};
|
|
1290
|
+
const supportedSet = /* @__PURE__ */ new Set();
|
|
1291
|
+
const unsupported = [];
|
|
1292
|
+
const isTest = root.includes("/mock/root");
|
|
1293
|
+
for (const [metaPkg, framework] of Object.entries(META_FRAMEWORK_MAP)) {
|
|
1294
|
+
if (metaPkg in allDeps || !isTest && isPackageResolvable2(metaPkg, root)) {
|
|
1295
|
+
supportedSet.add(framework);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
for (const { framework, deps } of SUPPORTED_FRAMEWORKS) {
|
|
1299
|
+
if (supportedSet.has(framework)) continue;
|
|
1300
|
+
for (const dep of deps) {
|
|
1301
|
+
if (dep in allDeps || !isTest && isPackageResolvable2(dep, root)) {
|
|
1302
|
+
supportedSet.add(framework);
|
|
1303
|
+
break;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
for (const fw of UNSUPPORTED_FRAMEWORKS) {
|
|
1308
|
+
if (fw.dep in allDeps || !isTest && isPackageResolvable2(fw.dep, root)) {
|
|
1309
|
+
unsupported.push(fw);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
supported: Array.from(supportedSet),
|
|
1314
|
+
unsupported
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// src/detect/ide.ts
|
|
1319
|
+
import path8 from "path";
|
|
1320
|
+
var SUPPORTED_IDE = "vscode";
|
|
1321
|
+
async function detectIDE(root) {
|
|
1322
|
+
const detected = /* @__PURE__ */ new Map();
|
|
1323
|
+
if (process.env.CURSOR_TRACE_DIR || process.env.CURSOR_CHANNEL) {
|
|
1324
|
+
detected.set("Cursor", { ide: "cursor", supported: true });
|
|
1325
|
+
}
|
|
1326
|
+
if (process.env.TRAE_APP_DIR || process.env.__CFBundleIdentifier === "com.byteocean.trae" || process.env.COCO_IDE_PLUGIN_TYPE === "Trae" || process.env.npm_config_user_agent && process.env.npm_config_user_agent.includes("trae")) {
|
|
1327
|
+
detected.set("Trae", { ide: "trae", supported: true });
|
|
1328
|
+
}
|
|
1329
|
+
if (process.env.ZED_TERM) {
|
|
1330
|
+
detected.set("Zed", { ide: "Zed", supported: false });
|
|
1331
|
+
}
|
|
1332
|
+
if (process.env.WINDSURF_APP_DIR || process.env.WINDSURF_CHANNEL || process.env.__CFBundleIdentifier === "com.codeium.windsurf" || process.env.npm_config_user_agent && process.env.npm_config_user_agent.includes("windsurf")) {
|
|
1333
|
+
detected.set("Windsurf", { ide: "Windsurf", supported: false });
|
|
1334
|
+
}
|
|
1335
|
+
const [hasTrae, hasCursor, hasVscode, hasIdea] = await Promise.all([
|
|
1336
|
+
exists(path8.join(root, ".trae")),
|
|
1337
|
+
exists(path8.join(root, ".cursor")),
|
|
1338
|
+
exists(path8.join(root, ".vscode")),
|
|
1339
|
+
exists(path8.join(root, ".idea"))
|
|
1340
|
+
]);
|
|
1341
|
+
if (hasTrae && !detected.has("Trae")) {
|
|
1342
|
+
detected.set("Trae", { ide: "trae", supported: true });
|
|
1343
|
+
}
|
|
1344
|
+
if (hasCursor && !detected.has("Cursor")) {
|
|
1345
|
+
detected.set("Cursor", { ide: "cursor", supported: true });
|
|
1346
|
+
}
|
|
1347
|
+
if (hasVscode && !detected.has("vscode")) {
|
|
1348
|
+
detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
|
|
1349
|
+
}
|
|
1350
|
+
if (hasIdea && !detected.has("JetBrains IDE")) {
|
|
1351
|
+
detected.set("JetBrains IDE", { ide: "JetBrains IDE", supported: false });
|
|
1352
|
+
}
|
|
1353
|
+
if (detected.size === 0 && process.env.TERM_PROGRAM === "vscode") {
|
|
1354
|
+
detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
|
|
1355
|
+
}
|
|
1356
|
+
return {
|
|
1357
|
+
detected: Array.from(detected.values())
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/detect/provider.ts
|
|
1362
|
+
import path9 from "path";
|
|
1363
|
+
var KNOWN_CLI_TOOLS = [
|
|
1364
|
+
{ id: "claude-code", bin: "claude", label: "Claude Code", supported: true },
|
|
1365
|
+
{ id: "coco", bin: "coco", label: "Trae CLI (Coco)", supported: true },
|
|
1366
|
+
{ id: "codex", bin: "codex", label: "Codex CLI", supported: true },
|
|
1367
|
+
{ id: "gemini", bin: "gemini", label: "Gemini CLI", supported: true }
|
|
1368
|
+
];
|
|
1369
|
+
var KNOWN_IDE_PLUGINS = [
|
|
1370
|
+
{ id: "claude-code", extId: "anthropic.claude-code", label: "Claude Code", supported: true },
|
|
1371
|
+
{ id: "copilot", extId: "github.copilot", label: "GitHub Copilot", supported: true },
|
|
1372
|
+
{ id: "codex", extId: "openai.chatgpt", label: "Codex (ChatGPT)", supported: true },
|
|
1373
|
+
{ id: "gemini", extId: "google.geminicodeassist", label: "Gemini Code Assist", supported: true }
|
|
1374
|
+
];
|
|
1375
|
+
async function detectProviders(root) {
|
|
1376
|
+
const detectedMap = /* @__PURE__ */ new Map();
|
|
1377
|
+
const cliChecks = KNOWN_CLI_TOOLS.map(async (tool) => {
|
|
1378
|
+
if (await which(tool.bin)) {
|
|
1379
|
+
detectedMap.set(tool.id, {
|
|
1380
|
+
id: tool.id,
|
|
1381
|
+
label: tool.label,
|
|
1382
|
+
supported: tool.supported,
|
|
1383
|
+
providerModes: ["cli"],
|
|
1384
|
+
preferredMode: "cli"
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
await Promise.all(cliChecks);
|
|
1389
|
+
const extensionsJsonPath = path9.join(root, ".vscode", "extensions.json");
|
|
1390
|
+
let recommendedExts = [];
|
|
1391
|
+
if (await exists(extensionsJsonPath)) {
|
|
1392
|
+
try {
|
|
1393
|
+
const extData = await readJSON(extensionsJsonPath);
|
|
1394
|
+
if (extData && Array.isArray(extData.recommendations)) {
|
|
1395
|
+
recommendedExts = extData.recommendations.map((e) => e.toLowerCase());
|
|
1396
|
+
}
|
|
1397
|
+
} catch {
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
1401
|
+
const globalExtDir = path9.join(homeDir, ".vscode", "extensions");
|
|
1402
|
+
const globalExtExists = await exists(globalExtDir);
|
|
1403
|
+
let installedExtensionFolders = [];
|
|
1404
|
+
if (globalExtExists) {
|
|
1405
|
+
try {
|
|
1406
|
+
const { readdir } = await import("fs/promises");
|
|
1407
|
+
installedExtensionFolders = await readdir(globalExtDir);
|
|
1408
|
+
const obsoletePath = path9.join(globalExtDir, ".obsolete");
|
|
1409
|
+
if (await exists(obsoletePath)) {
|
|
1410
|
+
try {
|
|
1411
|
+
const obsoleteData = await readJSON(obsoletePath);
|
|
1412
|
+
if (obsoleteData) {
|
|
1413
|
+
const obsoleteKeys = Object.keys(obsoleteData);
|
|
1414
|
+
installedExtensionFolders = installedExtensionFolders.filter((folder) => {
|
|
1415
|
+
return !obsoleteKeys.includes(folder);
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
} catch {
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
} catch {
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
for (const plugin of KNOWN_IDE_PLUGINS) {
|
|
1425
|
+
let isInstalled = false;
|
|
1426
|
+
if (recommendedExts.includes(plugin.extId.toLowerCase())) {
|
|
1427
|
+
isInstalled = true;
|
|
1428
|
+
} else if (installedExtensionFolders.some((f) => {
|
|
1429
|
+
const lower = f.toLowerCase();
|
|
1430
|
+
return lower === plugin.extId.toLowerCase() || lower.startsWith(plugin.extId.toLowerCase() + "-");
|
|
1431
|
+
})) {
|
|
1432
|
+
isInstalled = true;
|
|
1433
|
+
}
|
|
1434
|
+
if (isInstalled) {
|
|
1435
|
+
const existing = detectedMap.get(plugin.id);
|
|
1436
|
+
if (existing) {
|
|
1437
|
+
existing.providerModes.push("extension");
|
|
1438
|
+
existing.preferredMode = "extension";
|
|
1439
|
+
} else {
|
|
1440
|
+
detectedMap.set(plugin.id, {
|
|
1441
|
+
id: plugin.id,
|
|
1442
|
+
label: plugin.label,
|
|
1443
|
+
supported: plugin.supported,
|
|
1444
|
+
providerModes: ["extension"],
|
|
1445
|
+
preferredMode: "extension"
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return { detected: Array.from(detectedMap.values()) };
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/onboarding/context.ts
|
|
1454
|
+
async function buildOnboardingContext(root) {
|
|
1455
|
+
const [packageManager, buildTools, frameworks, ides, providers] = await Promise.all([
|
|
1456
|
+
detectPackageManager(root),
|
|
1457
|
+
detectBuildTools(root),
|
|
1458
|
+
detectFrameworks(root),
|
|
1459
|
+
detectIDE(root),
|
|
1460
|
+
detectProviders(root)
|
|
1461
|
+
]);
|
|
1462
|
+
return {
|
|
1463
|
+
root,
|
|
1464
|
+
packageManager,
|
|
1465
|
+
buildTools: {
|
|
1466
|
+
supported: buildTools.supported,
|
|
1467
|
+
unsupported: buildTools.unsupported
|
|
1468
|
+
},
|
|
1469
|
+
frameworks: {
|
|
1470
|
+
supported: frameworks.supported,
|
|
1471
|
+
unsupported: frameworks.unsupported.map((item) => item.name)
|
|
1472
|
+
},
|
|
1473
|
+
ides: ides.detected.map(({ ide, supported }) => ({ ide, supported })),
|
|
1474
|
+
providers: providers.detected.map(({ id, label, supported, preferredMode }) => ({
|
|
1475
|
+
id,
|
|
1476
|
+
label,
|
|
1477
|
+
supported,
|
|
1478
|
+
preferredMode
|
|
1479
|
+
}))
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/onboarding/planner.ts
|
|
1484
|
+
function message(code, message2) {
|
|
1485
|
+
return { code, message: message2 };
|
|
1486
|
+
}
|
|
1487
|
+
function uniqueMessages(messages) {
|
|
1488
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1489
|
+
return messages.filter((item) => {
|
|
1490
|
+
const key = `${item.code}:${item.message}`;
|
|
1491
|
+
if (seen.has(key)) return false;
|
|
1492
|
+
seen.add(key);
|
|
1493
|
+
return true;
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
function detectionStatus(warnings, blockers) {
|
|
1497
|
+
if (blockers.length > 0) return "blocked";
|
|
1498
|
+
if (warnings.length > 0) return "warning";
|
|
1499
|
+
return "ok";
|
|
1500
|
+
}
|
|
1501
|
+
function planStatus(warnings, blockers) {
|
|
1502
|
+
if (blockers.length > 0) return "blocked";
|
|
1503
|
+
if (warnings.length > 0) return "warning";
|
|
1504
|
+
return "ok";
|
|
1505
|
+
}
|
|
1506
|
+
function supportedIde(context) {
|
|
1507
|
+
return context.ides.find((ide) => ide.supported)?.ide;
|
|
1508
|
+
}
|
|
1509
|
+
function supportedProvider(context) {
|
|
1510
|
+
return context.providers.find((provider) => provider.supported)?.id;
|
|
1511
|
+
}
|
|
1512
|
+
function buildToolBlockers(context) {
|
|
1513
|
+
if (context.buildTools.unsupported.length > 0) {
|
|
1514
|
+
return [
|
|
1515
|
+
message(
|
|
1516
|
+
"unsupported-build-tool",
|
|
1517
|
+
`Detected unsupported build tool(s): ${context.buildTools.unsupported.join(", ")}`
|
|
1518
|
+
)
|
|
1519
|
+
];
|
|
1520
|
+
}
|
|
1521
|
+
if (context.buildTools.supported.length > 0) {
|
|
1522
|
+
if (context.buildTools.supported.length === 1) {
|
|
1523
|
+
return [];
|
|
1524
|
+
}
|
|
1525
|
+
const targets = context.buildTools.supported.map((target) => target.packagePath ?? target.configPath).join(", ");
|
|
1526
|
+
return [
|
|
1527
|
+
message(
|
|
1528
|
+
"multiple-supported-build-targets",
|
|
1529
|
+
`Multiple supported build targets detected: ${targets}. Run inspecto apply from a single app/package root until explicit target selection is available.`
|
|
1530
|
+
)
|
|
1531
|
+
];
|
|
1532
|
+
}
|
|
1533
|
+
return [message("missing-build-tool", "No supported build tool detected")];
|
|
1534
|
+
}
|
|
1535
|
+
function frameworkBlockers(context) {
|
|
1536
|
+
if (context.frameworks.supported.length > 0) {
|
|
1537
|
+
return [];
|
|
1538
|
+
}
|
|
1539
|
+
if (context.frameworks.unsupported.length > 0) {
|
|
1540
|
+
return [
|
|
1541
|
+
message(
|
|
1542
|
+
"unsupported-framework",
|
|
1543
|
+
`Detected unsupported framework(s): ${context.frameworks.unsupported.join(", ")}`
|
|
1544
|
+
)
|
|
1545
|
+
];
|
|
1546
|
+
}
|
|
1547
|
+
return [message("missing-framework", "No supported frontend framework detected")];
|
|
1548
|
+
}
|
|
1549
|
+
function unsupportedEnvironmentWarnings(context) {
|
|
1550
|
+
const warnings = [];
|
|
1551
|
+
if (context.frameworks.unsupported.length > 0 && context.frameworks.supported.length > 0) {
|
|
1552
|
+
warnings.push(
|
|
1553
|
+
message(
|
|
1554
|
+
"unsupported-framework-present",
|
|
1555
|
+
`Unsupported framework(s) also detected: ${context.frameworks.unsupported.join(", ")}`
|
|
1556
|
+
)
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
const unsupportedIdes = context.ides.filter((ide) => !ide.supported).map((ide) => ide.ide);
|
|
1560
|
+
if (unsupportedIdes.length > 0) {
|
|
1561
|
+
warnings.push(
|
|
1562
|
+
message("unsupported-ide", `Unsupported IDE(s) detected: ${unsupportedIdes.join(", ")}`)
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
const unsupportedProviders = context.providers.filter((provider) => !provider.supported).map((provider) => provider.label);
|
|
1566
|
+
if (unsupportedProviders.length > 0) {
|
|
1567
|
+
warnings.push(
|
|
1568
|
+
message(
|
|
1569
|
+
"unsupported-provider",
|
|
1570
|
+
`Unsupported provider(s) detected: ${unsupportedProviders.join(", ")}`
|
|
1571
|
+
)
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
return warnings;
|
|
1575
|
+
}
|
|
1576
|
+
function manualBuildToolActions(context) {
|
|
1577
|
+
if (context.buildTools.unsupported.length > 0) {
|
|
1578
|
+
return [
|
|
1579
|
+
{
|
|
1580
|
+
type: "manual_step",
|
|
1581
|
+
target: context.buildTools.unsupported.join(", "),
|
|
1582
|
+
description: "Inspecto cannot auto-configure this build stack yet. Follow the manual setup guide for the detected framework or build tool."
|
|
1583
|
+
}
|
|
1584
|
+
];
|
|
1585
|
+
}
|
|
1586
|
+
if (context.buildTools.supported.length > 1) {
|
|
1587
|
+
const targets = context.buildTools.supported.map((target) => target.packagePath ?? target.configPath).join(", ");
|
|
1588
|
+
return [
|
|
1589
|
+
{
|
|
1590
|
+
type: "manual_step",
|
|
1591
|
+
target: targets,
|
|
1592
|
+
description: "Run inspecto apply from the target app/package root. Root-level apply is blocked when multiple supported targets are detected."
|
|
1593
|
+
}
|
|
1594
|
+
];
|
|
1595
|
+
}
|
|
1596
|
+
return [
|
|
1597
|
+
{
|
|
1598
|
+
type: "manual_step",
|
|
1599
|
+
target: context.root,
|
|
1600
|
+
description: "No supported build tool was detected. Add a supported build config before trying Inspecto again."
|
|
1601
|
+
}
|
|
1602
|
+
];
|
|
1603
|
+
}
|
|
1604
|
+
function manualFrameworkActions(context) {
|
|
1605
|
+
if (context.frameworks.unsupported.length > 0) {
|
|
1606
|
+
return [
|
|
1607
|
+
{
|
|
1608
|
+
type: "manual_step",
|
|
1609
|
+
target: context.frameworks.unsupported.join(", "),
|
|
1610
|
+
description: "Inspecto cannot auto-configure this framework yet. Follow the manual setup guide for the detected framework."
|
|
1611
|
+
}
|
|
1612
|
+
];
|
|
1613
|
+
}
|
|
1614
|
+
return [
|
|
1615
|
+
{
|
|
1616
|
+
type: "manual_step",
|
|
1617
|
+
target: context.root,
|
|
1618
|
+
description: "No supported frontend framework was detected. Add a supported React or Vue app before trying Inspecto again."
|
|
1619
|
+
}
|
|
1620
|
+
];
|
|
1621
|
+
}
|
|
1622
|
+
async function createDetectionResult(root) {
|
|
1623
|
+
const context = await buildOnboardingContext(root);
|
|
1624
|
+
const warnings = uniqueMessages([...unsupportedEnvironmentWarnings(context)]);
|
|
1625
|
+
const buildToolResult = buildToolBlockers(context);
|
|
1626
|
+
const frameworkResult = frameworkBlockers(context);
|
|
1627
|
+
const blockers = uniqueMessages([...buildToolResult, ...frameworkResult]);
|
|
1628
|
+
return {
|
|
1629
|
+
status: detectionStatus(warnings, blockers),
|
|
1630
|
+
warnings,
|
|
1631
|
+
blockers,
|
|
1632
|
+
project: {
|
|
1633
|
+
root: context.root,
|
|
1634
|
+
packageManager: context.packageManager
|
|
1635
|
+
},
|
|
1636
|
+
environment: {
|
|
1637
|
+
frameworks: context.frameworks.supported,
|
|
1638
|
+
unsupportedFrameworks: context.frameworks.unsupported,
|
|
1639
|
+
buildTools: context.buildTools.supported,
|
|
1640
|
+
unsupportedBuildTools: context.buildTools.unsupported,
|
|
1641
|
+
ides: context.ides,
|
|
1642
|
+
providers: context.providers
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
function createPlanResult(context) {
|
|
1647
|
+
const warnings = uniqueMessages(unsupportedEnvironmentWarnings(context));
|
|
1648
|
+
const blockers = uniqueMessages([...buildToolBlockers(context), ...frameworkBlockers(context)]);
|
|
1649
|
+
const actions = [];
|
|
1650
|
+
let strategy = "supported";
|
|
1651
|
+
if (blockers.length > 0) {
|
|
1652
|
+
strategy = "manual";
|
|
1653
|
+
if (context.buildTools.unsupported.length > 0 || context.buildTools.supported.length === 0 || context.buildTools.supported.length > 1) {
|
|
1654
|
+
actions.push(...manualBuildToolActions(context));
|
|
1655
|
+
}
|
|
1656
|
+
if (frameworkBlockers(context).length > 0) {
|
|
1657
|
+
actions.push(...manualFrameworkActions(context));
|
|
1658
|
+
}
|
|
1659
|
+
} else {
|
|
1660
|
+
actions.push({
|
|
1661
|
+
type: "install_dependency",
|
|
1662
|
+
target: "@inspecto-dev/plugin @inspecto-dev/core",
|
|
1663
|
+
description: `Install the Inspecto runtime packages with ${context.packageManager}.`
|
|
1664
|
+
});
|
|
1665
|
+
for (const buildTool of context.buildTools.supported) {
|
|
1666
|
+
actions.push({
|
|
1667
|
+
type: "modify_file",
|
|
1668
|
+
target: buildTool.configPath,
|
|
1669
|
+
description: `Inject the Inspecto plugin into ${buildTool.label}.`
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
const ide2 = supportedIde(context);
|
|
1673
|
+
if (ide2 === "vscode") {
|
|
1674
|
+
actions.push({
|
|
1675
|
+
type: "install_extension",
|
|
1676
|
+
target: "vscode",
|
|
1677
|
+
description: "Install the Inspecto VS Code extension."
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const defaults = {
|
|
1682
|
+
shared: false,
|
|
1683
|
+
extension: supportedIde(context) === "vscode"
|
|
1684
|
+
};
|
|
1685
|
+
const provider = supportedProvider(context);
|
|
1686
|
+
if (provider) {
|
|
1687
|
+
defaults.provider = provider;
|
|
1688
|
+
}
|
|
1689
|
+
const ide = supportedIde(context);
|
|
1690
|
+
if (ide) {
|
|
1691
|
+
defaults.ide = ide;
|
|
1692
|
+
}
|
|
1693
|
+
return {
|
|
1694
|
+
status: planStatus(warnings, blockers),
|
|
1695
|
+
warnings,
|
|
1696
|
+
blockers,
|
|
1697
|
+
strategy,
|
|
1698
|
+
actions,
|
|
1699
|
+
defaults
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
function planManualFollowUp(result) {
|
|
1703
|
+
return result.actions.filter((action) => action.type === "manual_step").map((action) => action.description);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// src/commands/apply.ts
|
|
1707
|
+
function getProviderDefault(providerId, preferredMode) {
|
|
1708
|
+
if (!providerId) return void 0;
|
|
1709
|
+
const mode = preferredMode ?? (providerId === "coco" ? "cli" : "extension");
|
|
1710
|
+
return `${providerId}.${mode}`;
|
|
1711
|
+
}
|
|
1712
|
+
function statusRank(status) {
|
|
1713
|
+
switch (status) {
|
|
1714
|
+
case "error":
|
|
1715
|
+
return 3;
|
|
1716
|
+
case "blocked":
|
|
1717
|
+
return 2;
|
|
1718
|
+
case "warning":
|
|
1719
|
+
return 1;
|
|
1720
|
+
case "ok":
|
|
1721
|
+
default:
|
|
1722
|
+
return 0;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function mergeStatus(planStatus2, applyStatus) {
|
|
1726
|
+
return statusRank(planStatus2) >= statusRank(applyStatus) ? planStatus2 : applyStatus;
|
|
1727
|
+
}
|
|
1728
|
+
function printApplyResult(result) {
|
|
1729
|
+
const manualSteps = result.postInstall.nextSteps.filter(
|
|
1730
|
+
(step) => !result.plan.blockers.some((blocker) => blocker.message === step)
|
|
1731
|
+
);
|
|
1732
|
+
log.header("Inspecto Apply");
|
|
1733
|
+
log.info(`Status: ${result.status}`);
|
|
1734
|
+
log.info(`Strategy: ${result.plan.strategy}`);
|
|
1735
|
+
for (const blocker of result.plan.blockers) {
|
|
1736
|
+
log.error(blocker.message);
|
|
1737
|
+
}
|
|
1738
|
+
for (const warning of result.plan.warnings) {
|
|
1739
|
+
log.warn(warning.message);
|
|
1740
|
+
}
|
|
1741
|
+
if (manualSteps.length > 0 || result.plan.blockers.length > 0) {
|
|
1742
|
+
log.blank();
|
|
1743
|
+
log.warn("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Manual Steps Required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1744
|
+
manualSteps.forEach((step) => log.error(step));
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (result.plan.warnings.length > 0) {
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
log.ready("Ready! Inspecto is set up.");
|
|
1751
|
+
log.info("Next:");
|
|
1752
|
+
log.hint("1. Start or restart your dev server.");
|
|
1753
|
+
log.hint("2. Open your app in the browser.");
|
|
1754
|
+
log.hint("3. Hold Alt + Click any element to inspect.");
|
|
1755
|
+
}
|
|
1756
|
+
async function apply(options = {}) {
|
|
1757
|
+
const root = process.cwd();
|
|
1758
|
+
const context = await buildOnboardingContext(root);
|
|
1759
|
+
const plan2 = createPlanResult(context);
|
|
1760
|
+
const selectedProvider = context.providers.find((provider) => provider.id === plan2.defaults.provider) ?? null;
|
|
1761
|
+
const selectedIDE = context.ides.find((ide) => ide.ide === plan2.defaults.ide) ?? context.ides.find((ide) => ide.supported) ?? null;
|
|
1762
|
+
const applyResult = await applyOnboardingPlan({
|
|
1763
|
+
repoRoot: root,
|
|
1764
|
+
projectRoot: root,
|
|
1765
|
+
packageManager: context.packageManager,
|
|
1766
|
+
supportedBuildTargets: context.buildTools.supported,
|
|
1767
|
+
options: {
|
|
1768
|
+
shared: options.shared ?? plan2.defaults.shared,
|
|
1769
|
+
skipInstall: options.skipInstall ?? false,
|
|
1770
|
+
dryRun: options.dryRun ?? false,
|
|
1771
|
+
noExtension: options.noExtension ?? !plan2.defaults.extension,
|
|
1772
|
+
quiet: options.json ?? false
|
|
1773
|
+
},
|
|
1774
|
+
selectedIDE,
|
|
1775
|
+
providerDefault: getProviderDefault(plan2.defaults.provider, selectedProvider?.preferredMode),
|
|
1776
|
+
plan: plan2
|
|
1777
|
+
});
|
|
1778
|
+
const result = {
|
|
1779
|
+
...applyResult,
|
|
1780
|
+
status: mergeStatus(plan2.status, applyResult.status),
|
|
1781
|
+
plan: plan2
|
|
1782
|
+
};
|
|
1783
|
+
return writeCommandOutput(result, options.json ?? false, printApplyResult);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// src/commands/detect.ts
|
|
1787
|
+
function printDetectionResult(result) {
|
|
1788
|
+
const suppressedCodes = /* @__PURE__ */ new Set([
|
|
1789
|
+
"unsupported-build-tool",
|
|
1790
|
+
"unsupported-build-tool-present",
|
|
1791
|
+
"unsupported-framework",
|
|
1792
|
+
"unsupported-framework-present"
|
|
1793
|
+
]);
|
|
1794
|
+
log.header("Inspecto Detect");
|
|
1795
|
+
log.info(`Status: ${result.status}`);
|
|
1796
|
+
log.info(`Root: ${result.project.root}`);
|
|
1797
|
+
log.info(`Package manager: ${result.project.packageManager}`);
|
|
1798
|
+
if (result.environment.frameworks.length > 0) {
|
|
1799
|
+
log.success(`Supported frameworks: ${result.environment.frameworks.join(", ")}`);
|
|
1800
|
+
}
|
|
1801
|
+
if (result.environment.unsupportedFrameworks.length > 0) {
|
|
1802
|
+
log.warn(`Unsupported frameworks: ${result.environment.unsupportedFrameworks.join(", ")}`);
|
|
1803
|
+
}
|
|
1804
|
+
if (result.environment.buildTools.length > 0) {
|
|
1805
|
+
log.success(
|
|
1806
|
+
`Supported build tools: ${result.environment.buildTools.map((tool) => tool.label).join(", ")}`
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
if (result.environment.unsupportedBuildTools.length > 0) {
|
|
1810
|
+
log.warn(`Unsupported build tools: ${result.environment.unsupportedBuildTools.join(", ")}`);
|
|
1811
|
+
}
|
|
1812
|
+
const supportedIdes = result.environment.ides.filter((ide) => ide.supported).map((ide) => ide.ide);
|
|
1813
|
+
if (supportedIdes.length > 0) {
|
|
1814
|
+
log.success(`Supported IDEs: ${supportedIdes.join(", ")}`);
|
|
1815
|
+
}
|
|
1816
|
+
const supportedProviders = result.environment.providers.filter((provider) => provider.supported).map((provider) => provider.label);
|
|
1817
|
+
if (supportedProviders.length > 0) {
|
|
1818
|
+
log.success(`Supported providers: ${supportedProviders.join(", ")}`);
|
|
1819
|
+
}
|
|
1820
|
+
for (const blocker of result.blockers) {
|
|
1821
|
+
if (suppressedCodes.has(blocker.code)) continue;
|
|
1822
|
+
log.error(blocker.message);
|
|
1823
|
+
}
|
|
1824
|
+
for (const warning of result.warnings) {
|
|
1825
|
+
if (suppressedCodes.has(warning.code)) continue;
|
|
1826
|
+
log.warn(warning.message);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
async function detect(json = false) {
|
|
1830
|
+
const result = await createDetectionResult(process.cwd());
|
|
1831
|
+
return writeCommandOutput(result, json, printDetectionResult);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// src/commands/init.ts
|
|
1835
|
+
import path10 from "path";
|
|
1836
|
+
|
|
1837
|
+
// src/onboarding/target-resolution.ts
|
|
1838
|
+
function normalizePackagePath(packagePath) {
|
|
1839
|
+
if (!packagePath || packagePath === ".") return "";
|
|
1840
|
+
return packagePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
1841
|
+
}
|
|
1842
|
+
function looksLikeAppPackage(packagePath) {
|
|
1843
|
+
if (!packagePath) return true;
|
|
1844
|
+
return /(^|\/)(app|apps|web|client|frontend|site)(\/|$)/i.test(packagePath);
|
|
1845
|
+
}
|
|
1846
|
+
function looksLikeAuxiliaryPackage(packagePath) {
|
|
1847
|
+
return /(^|\/)(docs?|example|examples|playground|storybook|demo)(\/|$)/i.test(packagePath);
|
|
1848
|
+
}
|
|
1849
|
+
function buildCandidates(input) {
|
|
1850
|
+
return input.buildTools.map((buildTool) => {
|
|
1851
|
+
const packagePath = normalizePackagePath(buildTool.packagePath);
|
|
1852
|
+
return {
|
|
1853
|
+
packagePath,
|
|
1854
|
+
configPath: buildTool.configPath,
|
|
1855
|
+
buildTool: buildTool.tool,
|
|
1856
|
+
frameworks: input.frameworkSupportByPackage[packagePath] ?? [],
|
|
1857
|
+
automaticInjection: true
|
|
1858
|
+
};
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
function rankCandidate(candidate) {
|
|
1862
|
+
let score = 0;
|
|
1863
|
+
if (candidate.frameworks.length > 0) score += 4;
|
|
1864
|
+
if (candidate.automaticInjection) score += 2;
|
|
1865
|
+
if (looksLikeAppPackage(candidate.packagePath)) score += 1;
|
|
1866
|
+
if (looksLikeAuxiliaryPackage(candidate.packagePath)) score -= 2;
|
|
1867
|
+
return score;
|
|
1868
|
+
}
|
|
1869
|
+
function rankCandidates(candidates) {
|
|
1870
|
+
return candidates.map((candidate) => ({
|
|
1871
|
+
candidate,
|
|
1872
|
+
score: rankCandidate(candidate)
|
|
1873
|
+
})).sort((left, right) => right.score - left.score);
|
|
1874
|
+
}
|
|
1875
|
+
function resolveOnboardingTarget(input) {
|
|
1876
|
+
const candidates = buildCandidates(input);
|
|
1877
|
+
if (candidates.length === 0) {
|
|
1878
|
+
return {
|
|
1879
|
+
status: "needs_selection",
|
|
1880
|
+
candidates,
|
|
1881
|
+
reason: "No supported targets were detected."
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
const explicitlySelected = normalizePackagePath(input.selectedPackagePath);
|
|
1885
|
+
if (input.selectedPackagePath !== void 0) {
|
|
1886
|
+
const selected = candidates.find((candidate) => candidate.packagePath === explicitlySelected);
|
|
1887
|
+
if (selected) {
|
|
1888
|
+
return {
|
|
1889
|
+
status: "resolved",
|
|
1890
|
+
selected,
|
|
1891
|
+
candidates,
|
|
1892
|
+
reason: `Using the explicitly selected target: ${selected.packagePath || "."}.`
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
if (candidates.length === 1) {
|
|
1897
|
+
return {
|
|
1898
|
+
status: "resolved",
|
|
1899
|
+
selected: candidates[0],
|
|
1900
|
+
candidates,
|
|
1901
|
+
reason: "Only one supported target was detected."
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
const ranked = rankCandidates(candidates);
|
|
1905
|
+
if (ranked.length > 1 && ranked[0].score === ranked[1].score) {
|
|
1906
|
+
return {
|
|
1907
|
+
status: "needs_selection",
|
|
1908
|
+
candidates,
|
|
1909
|
+
reason: "Multiple supported targets look equally plausible."
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
return {
|
|
1913
|
+
status: "resolved",
|
|
1914
|
+
selected: ranked[0].candidate,
|
|
1915
|
+
candidates,
|
|
1916
|
+
reason: `Preselected ${ranked[0].candidate.packagePath || "."} because it has the strongest supported app signal.`
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// src/prompts.ts
|
|
1921
|
+
import prompts from "prompts";
|
|
1922
|
+
async function promptIDEChoice(detections) {
|
|
1923
|
+
if (!process.stdin.isTTY) {
|
|
1924
|
+
log.warn("Multiple IDEs detected but stdin is not interactive");
|
|
1925
|
+
log.hint(`Using: ${detections[0].ide} (first match)`);
|
|
1926
|
+
return detections[0];
|
|
1927
|
+
}
|
|
1928
|
+
const { choice } = await prompts({
|
|
1929
|
+
type: "select",
|
|
1930
|
+
name: "choice",
|
|
1931
|
+
message: "Detected multiple IDEs, please choose one:",
|
|
1932
|
+
choices: detections.map((d, i) => ({
|
|
1933
|
+
title: `${d.ide} ${d.supported ? "(supported)" : "(unsupported/limited)"}`,
|
|
1934
|
+
value: i
|
|
1935
|
+
}))
|
|
1936
|
+
});
|
|
1937
|
+
if (choice === void 0) return null;
|
|
1938
|
+
return detections[choice];
|
|
1939
|
+
}
|
|
1940
|
+
async function promptProviderChoice(detections) {
|
|
1941
|
+
if (!process.stdin.isTTY) {
|
|
1942
|
+
log.warn("Multiple AI tools detected but stdin is not interactive");
|
|
1943
|
+
log.hint(`Using: ${detections[0].label} (first match)`);
|
|
1944
|
+
return detections[0];
|
|
1945
|
+
}
|
|
1946
|
+
const { choice } = await prompts({
|
|
1947
|
+
type: "select",
|
|
1948
|
+
name: "choice",
|
|
1949
|
+
message: "Detected multiple providers, please choose one:",
|
|
1950
|
+
choices: detections.map((d, i) => {
|
|
1951
|
+
const modeLabels = d.providerModes.map(
|
|
1952
|
+
(mode) => mode === "extension" ? "VS Code Extension" : "Terminal CLI"
|
|
1953
|
+
);
|
|
1954
|
+
const modeStr = modeLabels.join(" & ");
|
|
1955
|
+
return {
|
|
1956
|
+
title: `${d.label} ${d.supported ? `(supported ${modeStr})` : "(unsupported/limited)"}`,
|
|
1957
|
+
value: i
|
|
1958
|
+
};
|
|
1959
|
+
}).concat({ title: "Skip (configure later)", value: -1 })
|
|
1960
|
+
});
|
|
1961
|
+
if (choice === void 0 || choice === -1) return null;
|
|
1962
|
+
return detections[choice];
|
|
1963
|
+
}
|
|
1964
|
+
async function promptConfigChoice(detections) {
|
|
1965
|
+
if (!process.stdin.isTTY) {
|
|
1966
|
+
log.warn("Multiple config files detected but stdin is not interactive");
|
|
1967
|
+
log.hint(`Using: ${detections[0].label} (first match)`);
|
|
1968
|
+
return detections[0];
|
|
1969
|
+
}
|
|
1970
|
+
const choices = detections.map((d, i) => ({
|
|
1971
|
+
title: d.label,
|
|
1972
|
+
value: i
|
|
1973
|
+
}));
|
|
1974
|
+
choices.push({
|
|
1975
|
+
title: "Skip (I'll configure manually)",
|
|
1976
|
+
value: -1
|
|
1977
|
+
});
|
|
1978
|
+
const { choice } = await prompts({
|
|
1979
|
+
type: "select",
|
|
1980
|
+
name: "choice",
|
|
1981
|
+
message: "Detected multiple build tool configs, please choose one to inject:",
|
|
1982
|
+
choices
|
|
1983
|
+
});
|
|
1984
|
+
if (choice === void 0 || choice === -1) return null;
|
|
1985
|
+
return detections[choice];
|
|
1986
|
+
}
|
|
1987
|
+
async function promptMonorepoPackageChoice(detections) {
|
|
1988
|
+
const uniquePackages = Array.from(
|
|
1989
|
+
new Map(
|
|
1990
|
+
detections.filter((d) => !!d.packagePath).map((d) => [d.packagePath, d])
|
|
1991
|
+
).values()
|
|
1992
|
+
);
|
|
1993
|
+
if (uniquePackages.length === 0) {
|
|
1994
|
+
return null;
|
|
1995
|
+
}
|
|
1996
|
+
if (!process.stdin.isTTY) {
|
|
1997
|
+
log.error("Monorepo root detected, but stdin is not interactive.");
|
|
1998
|
+
log.hint(
|
|
1999
|
+
"Re-run `inspecto init` inside a specific app directory, or pass --packages <app-path>."
|
|
2000
|
+
);
|
|
2001
|
+
return null;
|
|
2002
|
+
}
|
|
2003
|
+
const { choice } = await prompts({
|
|
2004
|
+
type: "select",
|
|
2005
|
+
name: "choice",
|
|
2006
|
+
message: "Monorepo root detected. Choose the app to initialize:",
|
|
2007
|
+
choices: uniquePackages.map((d, i) => ({
|
|
2008
|
+
title: `${d.packagePath} (${d.tool})`,
|
|
2009
|
+
description: d.configPath,
|
|
2010
|
+
value: i
|
|
2011
|
+
}))
|
|
2012
|
+
});
|
|
2013
|
+
if (choice === void 0) return null;
|
|
2014
|
+
return uniquePackages[choice].packagePath;
|
|
2015
|
+
}
|
|
2016
|
+
async function promptUnsupportedFrameworkContinue() {
|
|
2017
|
+
if (!process.stdin.isTTY) {
|
|
2018
|
+
log.error("Unsupported framework detected in non-interactive environment.");
|
|
2019
|
+
log.hint("Use --force to skip this check and continue anyway.");
|
|
2020
|
+
return false;
|
|
2021
|
+
}
|
|
2022
|
+
const { confirm } = await prompts({
|
|
2023
|
+
type: "confirm",
|
|
2024
|
+
name: "confirm",
|
|
2025
|
+
message: "Inspecto may not work properly. Do you want to continue anyway?",
|
|
2026
|
+
initial: false
|
|
2027
|
+
});
|
|
2028
|
+
return !!confirm;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// src/instructions.ts
|
|
2032
|
+
function printNuxtManualInstructions() {
|
|
2033
|
+
log.blank();
|
|
2034
|
+
log.hint("Nuxt requires manual setup in the current version.");
|
|
2035
|
+
log.hint("1. Update `nuxt.config.ts` to register the Inspecto Vite plugin:");
|
|
2036
|
+
log.copyableCodeBlock([
|
|
2037
|
+
"import { vitePlugin as inspecto } from '@inspecto-dev/plugin'",
|
|
2038
|
+
"",
|
|
2039
|
+
"export default defineNuxtConfig({",
|
|
2040
|
+
" vite: {",
|
|
2041
|
+
" plugins: [inspecto()],",
|
|
2042
|
+
" },",
|
|
2043
|
+
"})"
|
|
2044
|
+
]);
|
|
2045
|
+
log.hint("2. Create `plugins/inspecto.client.ts` to mount `@inspecto-dev/core` in development:");
|
|
2046
|
+
log.copyableCodeBlock([
|
|
2047
|
+
"export default defineNuxtPlugin(() => {",
|
|
2048
|
+
" if (import.meta.dev) {",
|
|
2049
|
+
" import('@inspecto-dev/core').then(({ mountInspector }) => {",
|
|
2050
|
+
" mountInspector()",
|
|
2051
|
+
" })",
|
|
2052
|
+
" }",
|
|
2053
|
+
"})"
|
|
2054
|
+
]);
|
|
2055
|
+
log.hint("3. Restart your Nuxt dev server after updating the config.");
|
|
2056
|
+
}
|
|
2057
|
+
function printNextJsManualInstructions() {
|
|
2058
|
+
log.blank();
|
|
2059
|
+
log.hint("Next.js requires manual setup in the current version.");
|
|
2060
|
+
log.hint("1. Update `next.config.mjs` to register the Inspecto webpack plugin:");
|
|
2061
|
+
log.copyableCodeBlock([
|
|
2062
|
+
"import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'",
|
|
2063
|
+
"",
|
|
2064
|
+
"/** @type {import('next').NextConfig} */",
|
|
2065
|
+
"const nextConfig = {",
|
|
2066
|
+
" webpack: (config, { dev, isServer }) => {",
|
|
2067
|
+
" if (dev && !isServer) {",
|
|
2068
|
+
" config.plugins.push(inspecto())",
|
|
2069
|
+
" }",
|
|
2070
|
+
" return config",
|
|
2071
|
+
" },",
|
|
2072
|
+
"}",
|
|
2073
|
+
"",
|
|
2074
|
+
"export default nextConfig"
|
|
2075
|
+
]);
|
|
2076
|
+
log.hint(
|
|
2077
|
+
"2. Initialize `@inspecto-dev/core` from a client component such as `app/layout.tsx` or `pages/_app.tsx`:"
|
|
2078
|
+
);
|
|
2079
|
+
log.copyableCodeBlock([
|
|
2080
|
+
"'use client'",
|
|
2081
|
+
"",
|
|
2082
|
+
"import { useEffect } from 'react'",
|
|
2083
|
+
"",
|
|
2084
|
+
"export default function RootLayout({ children }) {",
|
|
2085
|
+
" useEffect(() => {",
|
|
2086
|
+
" if (process.env.NODE_ENV !== 'production') {",
|
|
2087
|
+
" import('@inspecto-dev/core').then(({ mountInspector }) => {",
|
|
2088
|
+
" mountInspector({ serverUrl: 'http://127.0.0.1:5678' })",
|
|
2089
|
+
" })",
|
|
2090
|
+
" }",
|
|
2091
|
+
" }, [])",
|
|
2092
|
+
"",
|
|
2093
|
+
" return <html><body>{children}</body></html>",
|
|
2094
|
+
"}"
|
|
2095
|
+
]);
|
|
2096
|
+
log.hint("3. Restart your Next.js dev server after updating the config.");
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// src/commands/init.ts
|
|
2100
|
+
async function init(options) {
|
|
2101
|
+
const repoRoot = process.cwd();
|
|
2102
|
+
let projectRoot = repoRoot;
|
|
2103
|
+
const mutations = [];
|
|
2104
|
+
const normalizedPackages = normalizePackageList(options.packages);
|
|
2105
|
+
const verifiedPackages = [];
|
|
2106
|
+
for (const pkg of normalizedPackages) {
|
|
2107
|
+
if (!pkg) {
|
|
2108
|
+
verifiedPackages.push(pkg);
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
const absolutePath = path10.join(repoRoot, pkg);
|
|
2112
|
+
if (await exists(absolutePath)) {
|
|
2113
|
+
verifiedPackages.push(pkg);
|
|
2114
|
+
} else {
|
|
2115
|
+
log.warn(`Package path "${pkg}" not found (skipping)`);
|
|
2116
|
+
log.hint("Ensure --packages values are relative to the project root");
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
if (normalizedPackages.length > 0 && verifiedPackages.length === 0) {
|
|
2120
|
+
log.error("No valid packages found from --packages input");
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
log.header("Inspecto Setup");
|
|
2124
|
+
if (!await exists(path10.join(repoRoot, "package.json"))) {
|
|
2125
|
+
log.error("No package.json found in current directory");
|
|
2126
|
+
log.hint("Run this command from your project root");
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
const pm = await detectPackageManager(repoRoot);
|
|
2130
|
+
let buildResult = await detectBuildTools(
|
|
2131
|
+
repoRoot,
|
|
2132
|
+
verifiedPackages.length > 0 ? verifiedPackages : void 0
|
|
2133
|
+
);
|
|
2134
|
+
if (verifiedPackages.length === 0) {
|
|
2135
|
+
const monorepoCandidates = buildResult.supported.filter((detection) => !!detection.packagePath);
|
|
2136
|
+
if (monorepoCandidates.length > 0) {
|
|
2137
|
+
const frameworkSupportByPackage = await detectFrameworkSupportByPackage(
|
|
2138
|
+
repoRoot,
|
|
2139
|
+
monorepoCandidates
|
|
2140
|
+
);
|
|
2141
|
+
const targetResolution = resolveOnboardingTarget({
|
|
2142
|
+
repoRoot,
|
|
2143
|
+
buildTools: monorepoCandidates,
|
|
2144
|
+
frameworkSupportByPackage
|
|
2145
|
+
});
|
|
2146
|
+
if (targetResolution.status === "needs_selection") {
|
|
2147
|
+
log.warn("Monorepo root detected with multiple candidate apps.");
|
|
2148
|
+
const selectedPackage = await promptMonorepoPackageChoice(
|
|
2149
|
+
monorepoCandidates.filter(
|
|
2150
|
+
(detection) => targetResolution.candidates.some(
|
|
2151
|
+
(candidate) => candidate.packagePath === (detection.packagePath ?? "")
|
|
2152
|
+
)
|
|
2153
|
+
)
|
|
2154
|
+
);
|
|
2155
|
+
if (!selectedPackage) {
|
|
2156
|
+
log.hint("Run `inspecto init` inside the target app, or pass --packages <app-path>.");
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
projectRoot = path10.join(repoRoot, selectedPackage);
|
|
2160
|
+
buildResult = await detectBuildTools(repoRoot, [selectedPackage]);
|
|
2161
|
+
log.info(`Continuing initialization in ${selectedPackage}`);
|
|
2162
|
+
} else if (targetResolution.selected?.packagePath) {
|
|
2163
|
+
const selectedPackage = targetResolution.selected.packagePath;
|
|
2164
|
+
projectRoot = path10.join(repoRoot, selectedPackage);
|
|
2165
|
+
buildResult = await detectBuildTools(repoRoot, [selectedPackage]);
|
|
2166
|
+
log.warn(`Monorepo root detected. Using the only candidate app: ${selectedPackage}`);
|
|
2167
|
+
log.hint("Run `inspecto init` inside that app next time to skip this prompt.");
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
const [frameworkResult, ideProbe, providerProbe] = await Promise.all([
|
|
2172
|
+
detectFrameworks(projectRoot),
|
|
2173
|
+
detectIDE(projectRoot),
|
|
2174
|
+
detectProviders(projectRoot)
|
|
2175
|
+
]);
|
|
2176
|
+
log.success(`Detected package manager: ${pm}`);
|
|
2177
|
+
if (frameworkResult.supported.length > 0) {
|
|
2178
|
+
const frameworks = frameworkResult.supported.join(", ");
|
|
2179
|
+
log.success(`Detected framework: ${frameworks}`);
|
|
2180
|
+
if (frameworkResult.unsupported.length > 0) {
|
|
2181
|
+
log.hint(
|
|
2182
|
+
`Other frameworks detected (${frameworkResult.unsupported.map((f) => f.name).join(", ")}) will be skipped in this setup.`
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
const isSupported = frameworkResult.supported.length > 0;
|
|
2187
|
+
const hasUnsupported = frameworkResult.unsupported.length > 0;
|
|
2188
|
+
if (!isSupported) {
|
|
2189
|
+
if (hasUnsupported) {
|
|
2190
|
+
const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
|
|
2191
|
+
log.warn(`Detected ${names} \u2014 not supported in v1 (React / Vue only)`);
|
|
2192
|
+
} else {
|
|
2193
|
+
log.warn("No frontend framework detected");
|
|
2194
|
+
log.hint("Inspecto current version supports React and Vue projects");
|
|
2195
|
+
}
|
|
2196
|
+
if (!options.force) {
|
|
2197
|
+
const shouldContinue = await promptUnsupportedFrameworkContinue();
|
|
2198
|
+
if (!shouldContinue) {
|
|
2199
|
+
log.warn("Initialization aborted.");
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
} else {
|
|
2203
|
+
log.warn("Continuing anyway (--force)");
|
|
2204
|
+
}
|
|
2205
|
+
} else if (hasUnsupported) {
|
|
2206
|
+
const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
|
|
2207
|
+
log.hint(
|
|
2208
|
+
`Note: Inspecto will be configured for ${frameworkResult.supported.join(", ")}. Other detected frameworks (${names}) will be ignored.`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
let manualConfigRequiredFor = "";
|
|
2212
|
+
if (verifiedPackages.length > 0 && buildResult.supported.length === 0) {
|
|
2213
|
+
log.warn(
|
|
2214
|
+
`No supported build configs detected for: ${verifiedPackages.map((pkg) => pkg ? pkg : ".").join(", ")}`
|
|
2215
|
+
);
|
|
2216
|
+
log.hint("Double-check the --packages values or run without the flag to scan the repo root");
|
|
2217
|
+
}
|
|
2218
|
+
if (buildResult.supported.length > 0) {
|
|
2219
|
+
buildResult.supported.forEach((bt) => log.success(`Detected: ${bt.label}`));
|
|
2220
|
+
}
|
|
2221
|
+
if (buildResult.unsupported.length > 0) {
|
|
2222
|
+
const names = buildResult.unsupported.join(", ");
|
|
2223
|
+
manualConfigRequiredFor = buildResult.unsupported[0] || "";
|
|
2224
|
+
log.warn(`Detected ${names} \u2014 automatic plugin injection is not supported in current version`);
|
|
2225
|
+
log.hint("You can still manually configure it by modifying your configuration file");
|
|
2226
|
+
if (buildResult.unsupported.includes("Next.js")) {
|
|
2227
|
+
printNextJsManualInstructions();
|
|
2228
|
+
}
|
|
2229
|
+
if (buildResult.unsupported.includes("Nuxt")) {
|
|
2230
|
+
printNuxtManualInstructions();
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if (buildResult.supported.length === 0 && buildResult.unsupported.length === 0) {
|
|
2234
|
+
log.warn("No recognized build tool detected");
|
|
2235
|
+
log.hint("current version supports: Vite, Webpack, Rspack, esbuild, Rollup");
|
|
2236
|
+
log.hint("Dependency will be installed but plugin injection will be skipped");
|
|
2237
|
+
log.hint(
|
|
2238
|
+
"Please refer to the manual setup guide: https://inspecto-dev.github.io/inspecto/guide/manual-installation"
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
let selectedIDE = null;
|
|
2242
|
+
if (ideProbe.detected.length === 0) {
|
|
2243
|
+
log.error("No IDE detected in current project");
|
|
2244
|
+
log.hint("Please open this project in a supported IDE (like VS Code)");
|
|
2245
|
+
} else if (ideProbe.detected.length === 1) {
|
|
2246
|
+
selectedIDE = ideProbe.detected[0];
|
|
2247
|
+
} else {
|
|
2248
|
+
selectedIDE = await promptIDEChoice(ideProbe.detected);
|
|
2249
|
+
}
|
|
2250
|
+
if (selectedIDE) {
|
|
2251
|
+
if (selectedIDE.supported) {
|
|
2252
|
+
log.success(`Selected IDE: ${selectedIDE.ide}`);
|
|
2253
|
+
} else {
|
|
2254
|
+
log.warn(`Selected IDE: ${selectedIDE.ide}`);
|
|
2255
|
+
log.hint(
|
|
2256
|
+
`Note: Inspecto currently requires VS Code (or compatible forks) to function properly.`
|
|
2257
|
+
);
|
|
2258
|
+
log.hint(`Features may be severely limited or unavailable in ${selectedIDE.ide}.`);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
let selectedProvider = null;
|
|
2262
|
+
const explicitProvider = options.provider ? providerProbe.detected.find((provider) => provider.id === options.provider) ?? null : null;
|
|
2263
|
+
if (!options.provider) {
|
|
2264
|
+
if (providerProbe.detected.length === 0) {
|
|
2265
|
+
log.warn("No supported AI tools detected");
|
|
2266
|
+
log.hint("Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot");
|
|
2267
|
+
} else if (providerProbe.detected.length === 1) {
|
|
2268
|
+
selectedProvider = providerProbe.detected[0];
|
|
2269
|
+
if (selectedProvider.supported) {
|
|
2270
|
+
log.success(`Detected AI tool: ${selectedProvider.label}`);
|
|
2271
|
+
}
|
|
2272
|
+
} else {
|
|
2273
|
+
log.info("Multiple providers detected, waiting for your selection...");
|
|
2274
|
+
selectedProvider = await promptProviderChoice(providerProbe.detected);
|
|
2275
|
+
if (selectedProvider) {
|
|
2276
|
+
log.success(`Selected provider: ${selectedProvider.label}`);
|
|
2277
|
+
} else {
|
|
2278
|
+
log.warn("No provider selected. You can set provider.default later in .inspecto/settings.");
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
let injectionSkippedRequiresManualConfig = false;
|
|
2283
|
+
const supportedBuildTargets = [];
|
|
2284
|
+
if (buildResult.supported.length > 0) {
|
|
2285
|
+
if (verifiedPackages.length > 0) {
|
|
2286
|
+
const targets = buildResult.supported.filter(
|
|
2287
|
+
(detection) => matchesAnyPackage(detection, verifiedPackages)
|
|
2288
|
+
);
|
|
2289
|
+
const unmatchedPackages = verifiedPackages.filter(
|
|
2290
|
+
(pkg) => !buildResult.supported.some((detection) => matchesPackage(detection, pkg))
|
|
2291
|
+
);
|
|
2292
|
+
if (unmatchedPackages.length > 0) {
|
|
2293
|
+
log.warn(
|
|
2294
|
+
`No supported build configs detected for: ${unmatchedPackages.map((pkg) => pkg ? pkg : ".").join(", ")}`
|
|
2295
|
+
);
|
|
2296
|
+
log.hint("Check the package paths or run without --packages to inspect the repo root");
|
|
2297
|
+
}
|
|
2298
|
+
if (targets.length === 0) {
|
|
2299
|
+
injectionSkippedRequiresManualConfig = true;
|
|
2300
|
+
}
|
|
2301
|
+
supportedBuildTargets.push(...targets);
|
|
2302
|
+
} else {
|
|
2303
|
+
let target = resolveInjectionTarget(buildResult.supported);
|
|
2304
|
+
if (target === "ambiguous") {
|
|
2305
|
+
target = await promptConfigChoice(buildResult.supported);
|
|
2306
|
+
}
|
|
2307
|
+
if (target) {
|
|
2308
|
+
supportedBuildTargets.push(target);
|
|
2309
|
+
} else {
|
|
2310
|
+
injectionSkippedRequiresManualConfig = true;
|
|
2311
|
+
log.warn("Skipping plugin injection (manual configuration required)");
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
const providerDefault = options.provider ? `${options.provider}.${explicitProvider?.preferredMode ?? (options.provider === "coco" ? "cli" : "extension")}` : selectedProvider ? `${selectedProvider.id}.${selectedProvider.preferredMode === "cli" ? "cli" : "extension"}` : void 0;
|
|
2316
|
+
const applyResult = await applyOnboardingPlan({
|
|
2317
|
+
repoRoot,
|
|
2318
|
+
projectRoot,
|
|
2319
|
+
packageManager: pm,
|
|
2320
|
+
supportedBuildTargets,
|
|
2321
|
+
options: {
|
|
2322
|
+
shared: options.shared,
|
|
2323
|
+
skipInstall: options.skipInstall,
|
|
2324
|
+
dryRun: options.dryRun,
|
|
2325
|
+
noExtension: options.noExtension
|
|
2326
|
+
},
|
|
2327
|
+
selectedIDE,
|
|
2328
|
+
providerDefault,
|
|
2329
|
+
manualConfigRequiredFor,
|
|
2330
|
+
injectionSkippedRequiresManualConfig,
|
|
2331
|
+
allowManualPlanApply: true
|
|
2332
|
+
});
|
|
2333
|
+
mutations.push(...applyResult.mutations);
|
|
2334
|
+
if (options.dryRun) {
|
|
2335
|
+
log.blank();
|
|
2336
|
+
log.warn("Dry run complete. No files were modified.");
|
|
2337
|
+
} else {
|
|
2338
|
+
log.blank();
|
|
2339
|
+
if (applyResult.postInstall.nextSteps.length > 0) {
|
|
2340
|
+
log.warn("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Manual Steps Required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2341
|
+
applyResult.postInstall.nextSteps.forEach((step) => log.error(step));
|
|
2342
|
+
log.hint("Complete the items above.");
|
|
2343
|
+
log.blank();
|
|
2344
|
+
} else {
|
|
2345
|
+
log.ready("Ready! Inspecto is set up.");
|
|
2346
|
+
log.info("Next:");
|
|
2347
|
+
log.hint("1. Start or restart your dev server.");
|
|
2348
|
+
log.hint("2. Open your app in the browser.");
|
|
2349
|
+
log.hint("3. Hold Alt + Click any element to inspect.");
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
function normalizePackageList(packages) {
|
|
2354
|
+
if (!packages || packages.length === 0) return [];
|
|
2355
|
+
const normalized = packages.map((pkg) => {
|
|
2356
|
+
const trimmed = pkg.trim();
|
|
2357
|
+
if (trimmed === "") return null;
|
|
2358
|
+
if (trimmed === "." || trimmed === "./") return "";
|
|
2359
|
+
return trimmed.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
2360
|
+
}).filter((value) => value !== null);
|
|
2361
|
+
return Array.from(new Set(normalized));
|
|
2362
|
+
}
|
|
2363
|
+
function matchesPackage(detection, pkg) {
|
|
2364
|
+
const configPath = detection.configPath.replace(/\\/g, "/");
|
|
2365
|
+
if (!pkg) {
|
|
2366
|
+
return !configPath.includes("/");
|
|
2367
|
+
}
|
|
2368
|
+
return configPath === pkg || configPath.startsWith(`${pkg}/`);
|
|
2369
|
+
}
|
|
2370
|
+
function matchesAnyPackage(detection, packages) {
|
|
2371
|
+
if (packages.length === 0) return true;
|
|
2372
|
+
return packages.some((pkg) => matchesPackage(detection, pkg));
|
|
2373
|
+
}
|
|
2374
|
+
async function detectFrameworkSupportByPackage(repoRoot, buildTools) {
|
|
2375
|
+
const packagePaths = Array.from(
|
|
2376
|
+
new Set(buildTools.map((buildTool) => buildTool.packagePath).filter((value) => !!value))
|
|
2377
|
+
);
|
|
2378
|
+
const supportByPackage = {};
|
|
2379
|
+
await Promise.all(
|
|
2380
|
+
packagePaths.map(async (packagePath) => {
|
|
2381
|
+
const result = await detectFrameworks(path10.join(repoRoot, packagePath));
|
|
2382
|
+
supportByPackage[packagePath] = result.supported;
|
|
2383
|
+
})
|
|
2384
|
+
);
|
|
2385
|
+
return supportByPackage;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// src/commands/doctor.ts
|
|
2389
|
+
import path11 from "path";
|
|
2390
|
+
function createDiagnostic(code, status, message2, hints = [], details) {
|
|
2391
|
+
return {
|
|
2392
|
+
code,
|
|
2393
|
+
status,
|
|
2394
|
+
message: message2,
|
|
2395
|
+
hints,
|
|
2396
|
+
...details ? { details } : {}
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
function doctorStatus(errors, warnings) {
|
|
2400
|
+
if (errors > 0) return "blocked";
|
|
2401
|
+
if (warnings > 0) return "warning";
|
|
2402
|
+
return "ok";
|
|
2403
|
+
}
|
|
2404
|
+
function printDoctorResult(result) {
|
|
2405
|
+
log.header("Inspecto Doctor");
|
|
2406
|
+
for (const check of result.checks) {
|
|
2407
|
+
if (check.status === "ok") {
|
|
2408
|
+
log.success(check.message);
|
|
2409
|
+
} else if (check.status === "warning") {
|
|
2410
|
+
log.warn(check.message);
|
|
2411
|
+
} else {
|
|
2412
|
+
log.error(check.message);
|
|
2413
|
+
}
|
|
2414
|
+
for (const hint of check.hints) {
|
|
2415
|
+
log.hint(hint);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
log.blank();
|
|
2419
|
+
if (result.summary.errors === 0 && result.summary.warnings === 0) {
|
|
2420
|
+
log.success("All checks passed. Hold Alt + Click to start!");
|
|
2421
|
+
} else {
|
|
2422
|
+
const parts = [];
|
|
2423
|
+
if (result.summary.errors > 0) parts.push(`${result.summary.errors} error(s)`);
|
|
2424
|
+
if (result.summary.warnings > 0) parts.push(`${result.summary.warnings} warning(s)`);
|
|
2425
|
+
console.log(
|
|
2426
|
+
` ${parts.join(", ")}. ${result.summary.errors > 0 ? "Fix the errors above to get started." : ""}`
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
log.blank();
|
|
2430
|
+
}
|
|
2431
|
+
async function collectDoctorResult(root = process.cwd()) {
|
|
2432
|
+
const checks = [];
|
|
2433
|
+
if (!await exists(path11.join(root, "package.json"))) {
|
|
2434
|
+
const diagnostic = createDiagnostic("missing-package-json", "error", "No package.json found", [
|
|
2435
|
+
"Run this command from your project root"
|
|
2436
|
+
]);
|
|
2437
|
+
checks.push(diagnostic);
|
|
2438
|
+
return {
|
|
2439
|
+
status: "blocked",
|
|
2440
|
+
summary: { errors: 1, warnings: 0 },
|
|
2441
|
+
project: { root },
|
|
2442
|
+
errors: [diagnostic],
|
|
2443
|
+
warnings: [],
|
|
2444
|
+
checks
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
const [ideProbe, frameworkResult, providerProbe, pm, buildResult, extInstalled] = await Promise.all([
|
|
2448
|
+
detectIDE(root),
|
|
2449
|
+
detectFrameworks(root),
|
|
2450
|
+
detectProviders(root),
|
|
2451
|
+
detectPackageManager(root),
|
|
2452
|
+
detectBuildTools(root),
|
|
2453
|
+
isExtensionInstalled()
|
|
2454
|
+
]);
|
|
2455
|
+
if (ideProbe.detected.length === 0) {
|
|
2456
|
+
checks.push(createDiagnostic("ide-not-detected", "warning", "IDE: not detected"));
|
|
2457
|
+
} else {
|
|
2458
|
+
const hasSupported = ideProbe.detected.some((d) => d.supported);
|
|
2459
|
+
if (hasSupported) {
|
|
2460
|
+
checks.push(
|
|
2461
|
+
createDiagnostic(
|
|
2462
|
+
"ide-supported",
|
|
2463
|
+
"ok",
|
|
2464
|
+
`IDE: ${ideProbe.detected.filter((d) => d.supported).map((d) => d.ide).join(", ")}`,
|
|
2465
|
+
[],
|
|
2466
|
+
{
|
|
2467
|
+
detected: ideProbe.detected
|
|
2468
|
+
}
|
|
2469
|
+
)
|
|
2470
|
+
);
|
|
2471
|
+
} else {
|
|
2472
|
+
const names = ideProbe.detected.map((d) => d.ide).join(", ");
|
|
2473
|
+
checks.push(
|
|
2474
|
+
createDiagnostic(
|
|
2475
|
+
"ide-unsupported",
|
|
2476
|
+
"warning",
|
|
2477
|
+
`IDE: ${names} (not supported in v1, VS Code, Cursor, Trae only)`,
|
|
2478
|
+
[],
|
|
2479
|
+
{
|
|
2480
|
+
detected: ideProbe.detected
|
|
2481
|
+
}
|
|
2482
|
+
)
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
if (frameworkResult.supported.length > 0) {
|
|
2487
|
+
checks.push(
|
|
2488
|
+
createDiagnostic(
|
|
2489
|
+
"framework-supported",
|
|
2490
|
+
"ok",
|
|
2491
|
+
`Framework: ${frameworkResult.supported.join(", ")}`
|
|
2492
|
+
)
|
|
2493
|
+
);
|
|
2494
|
+
} else if (frameworkResult.unsupported.length > 0) {
|
|
2495
|
+
const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
|
|
2496
|
+
checks.push(
|
|
2497
|
+
createDiagnostic(
|
|
2498
|
+
"framework-unsupported",
|
|
2499
|
+
"warning",
|
|
2500
|
+
`Framework: ${names} (not supported in v1, React/Vue only)`
|
|
2501
|
+
)
|
|
2502
|
+
);
|
|
2503
|
+
} else {
|
|
2504
|
+
checks.push(
|
|
2505
|
+
createDiagnostic(
|
|
2506
|
+
"framework-not-detected",
|
|
2507
|
+
"warning",
|
|
2508
|
+
"Framework: not detected (React / Vue expected)"
|
|
2509
|
+
)
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
if (providerProbe.detected.length === 0) {
|
|
2513
|
+
checks.push(
|
|
2514
|
+
createDiagnostic("provider-missing", "warning", "Provider: none detected", [
|
|
2515
|
+
"Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot"
|
|
2516
|
+
])
|
|
2517
|
+
);
|
|
2518
|
+
} else {
|
|
2519
|
+
const aiNames = providerProbe.detected.map((d) => {
|
|
2520
|
+
const modeLabels = d.providerModes.map(
|
|
2521
|
+
(mode) => mode === "extension" ? "VS Code Extension" : "Terminal CLI"
|
|
2522
|
+
);
|
|
2523
|
+
return `${d.label} (${modeLabels.join(" & ")})`;
|
|
2524
|
+
}).join(", ");
|
|
2525
|
+
checks.push(createDiagnostic("provider-detected", "ok", `Provider: ${aiNames}`));
|
|
2526
|
+
}
|
|
2527
|
+
const pluginPath = path11.join(root, "node_modules", "@inspecto-dev", "plugin");
|
|
2528
|
+
if (await exists(pluginPath)) {
|
|
2529
|
+
const pkgJson = await readJSON(path11.join(pluginPath, "package.json"));
|
|
2530
|
+
const version = pkgJson?.version ?? "unknown";
|
|
2531
|
+
checks.push(
|
|
2532
|
+
createDiagnostic("plugin-installed", "ok", `@inspecto-dev/plugin@${version} installed`, [], {
|
|
2533
|
+
version
|
|
2534
|
+
})
|
|
2535
|
+
);
|
|
2536
|
+
} else {
|
|
2537
|
+
checks.push(
|
|
2538
|
+
createDiagnostic("plugin-missing", "error", "@inspecto-dev/plugin not installed", [
|
|
2539
|
+
`Fix: ${getInstallCommand(pm, "@inspecto-dev/plugin")}`
|
|
2540
|
+
])
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
if (buildResult.supported.length > 0) {
|
|
2544
|
+
let injected = false;
|
|
2545
|
+
for (const bt of buildResult.supported) {
|
|
2546
|
+
const content = await readFile(path11.join(root, bt.configPath));
|
|
2547
|
+
if (content && content.includes("@inspecto-dev/plugin")) {
|
|
2548
|
+
checks.push(
|
|
2549
|
+
createDiagnostic("plugin-configured", "ok", `Plugin configured in ${bt.configPath}`, [], {
|
|
2550
|
+
configPath: bt.configPath,
|
|
2551
|
+
buildTool: bt.tool
|
|
2552
|
+
})
|
|
2553
|
+
);
|
|
2554
|
+
injected = true;
|
|
2555
|
+
break;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
if (!injected) {
|
|
2559
|
+
checks.push(
|
|
2560
|
+
createDiagnostic(
|
|
2561
|
+
"plugin-not-configured",
|
|
2562
|
+
"error",
|
|
2563
|
+
"Plugin not configured in any build config",
|
|
2564
|
+
["Fix: npx @inspecto-dev/cli init"]
|
|
2565
|
+
)
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
} else if (buildResult.unsupported.length > 0) {
|
|
2569
|
+
const names = buildResult.unsupported.join(", ");
|
|
2570
|
+
checks.push(
|
|
2571
|
+
createDiagnostic(
|
|
2572
|
+
`build-tool-unsupported`,
|
|
2573
|
+
"warning",
|
|
2574
|
+
`Build tool: ${names} (not supported in v1)`,
|
|
2575
|
+
["current version supports: Vite, Webpack, Rspack, esbuild, Rollup"]
|
|
2576
|
+
)
|
|
2577
|
+
);
|
|
2578
|
+
} else {
|
|
2579
|
+
checks.push(
|
|
2580
|
+
createDiagnostic("build-tool-missing", "warning", "No recognized build config found")
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
if (extInstalled) {
|
|
2584
|
+
checks.push(createDiagnostic("extension-installed", "ok", "VS Code extension detected"));
|
|
2585
|
+
} else {
|
|
2586
|
+
const hasVSCode = ideProbe.detected.some((d) => d.supported && d.ide === "vscode");
|
|
2587
|
+
const hasSupportedNonVSCode = ideProbe.detected.some((d) => d.supported && d.ide !== "vscode");
|
|
2588
|
+
if (hasSupportedNonVSCode && !hasVSCode) {
|
|
2589
|
+
checks.push(
|
|
2590
|
+
createDiagnostic(
|
|
2591
|
+
"extension-not-applicable",
|
|
2592
|
+
"warning",
|
|
2593
|
+
"VS Code extension not applicable (non-VS Code IDE)"
|
|
2594
|
+
)
|
|
2595
|
+
);
|
|
2596
|
+
} else {
|
|
2597
|
+
checks.push(
|
|
2598
|
+
createDiagnostic("extension-missing", "error", "VS Code extension not found", [
|
|
2599
|
+
"Fix: code --install-extension inspecto.inspecto",
|
|
2600
|
+
"Or: https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto"
|
|
2601
|
+
])
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
const settingsJsonPath = path11.join(root, ".inspecto", "settings.json");
|
|
2606
|
+
const settingsLocalPath = path11.join(root, ".inspecto", "settings.local.json");
|
|
2607
|
+
const hasSettingsJson = await exists(settingsJsonPath);
|
|
2608
|
+
const hasSettingsLocal = await exists(settingsLocalPath);
|
|
2609
|
+
if (hasSettingsJson || hasSettingsLocal) {
|
|
2610
|
+
const targetPath = hasSettingsLocal ? settingsLocalPath : settingsJsonPath;
|
|
2611
|
+
const fileName = hasSettingsLocal ? "settings.local.json" : "settings.json";
|
|
2612
|
+
const settings = await readJSON(targetPath);
|
|
2613
|
+
if (settings) {
|
|
2614
|
+
checks.push(createDiagnostic("settings-valid", "ok", `.inspecto/${fileName} valid`));
|
|
2615
|
+
} else {
|
|
2616
|
+
checks.push(
|
|
2617
|
+
createDiagnostic(
|
|
2618
|
+
"settings-invalid-json",
|
|
2619
|
+
"error",
|
|
2620
|
+
`.inspecto/${fileName} has invalid JSON`,
|
|
2621
|
+
[
|
|
2622
|
+
"Fix: Manually correct the syntax errors, or delete the file and re-run npx @inspecto-dev/cli init"
|
|
2623
|
+
],
|
|
2624
|
+
{
|
|
2625
|
+
fileName
|
|
2626
|
+
}
|
|
2627
|
+
)
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
} else {
|
|
2631
|
+
checks.push(
|
|
2632
|
+
createDiagnostic(
|
|
2633
|
+
"settings-missing",
|
|
2634
|
+
"warning",
|
|
2635
|
+
"No .inspecto/settings.json or settings.local.json found (using defaults)",
|
|
2636
|
+
["Optional: npx @inspecto-dev/cli init"]
|
|
2637
|
+
)
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
const gitignoreContent = await readFile(path11.join(root, ".gitignore"));
|
|
2641
|
+
if (gitignoreContent) {
|
|
2642
|
+
const hasLockIgnore = gitignoreContent.includes(".inspecto/install.lock") || gitignoreContent.includes(".inspecto/");
|
|
2643
|
+
if (!hasLockIgnore) {
|
|
2644
|
+
checks.push(
|
|
2645
|
+
createDiagnostic(
|
|
2646
|
+
"gitignore-missing-install-lock",
|
|
2647
|
+
"warning",
|
|
2648
|
+
".inspecto/install.lock not in .gitignore",
|
|
2649
|
+
["install.lock contains local machine state and should not be committed"]
|
|
2650
|
+
)
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
const errors = checks.filter((check) => check.status === "error");
|
|
2655
|
+
const warnings = checks.filter((check) => check.status === "warning");
|
|
2656
|
+
return {
|
|
2657
|
+
status: doctorStatus(errors.length, warnings.length),
|
|
2658
|
+
summary: {
|
|
2659
|
+
errors: errors.length,
|
|
2660
|
+
warnings: warnings.length
|
|
2661
|
+
},
|
|
2662
|
+
project: {
|
|
2663
|
+
root,
|
|
2664
|
+
packageManager: pm
|
|
2665
|
+
},
|
|
2666
|
+
errors,
|
|
2667
|
+
warnings,
|
|
2668
|
+
checks
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
async function doctor(options = {}) {
|
|
2672
|
+
const json = typeof options === "boolean" ? options : options.json ?? false;
|
|
2673
|
+
const result = await collectDoctorResult(process.cwd());
|
|
2674
|
+
return writeCommandOutput(result, json, printDoctorResult);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// src/onboarding/session.ts
|
|
2678
|
+
import path12 from "path";
|
|
2679
|
+
function normalizePackagePath2(packagePath) {
|
|
2680
|
+
if (!packagePath || packagePath === ".") return "";
|
|
2681
|
+
return packagePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
2682
|
+
}
|
|
2683
|
+
function getProviderDefault2(providerId, preferredMode) {
|
|
2684
|
+
if (!providerId) return void 0;
|
|
2685
|
+
const mode = preferredMode ?? (providerId === "coco" ? "cli" : "extension");
|
|
2686
|
+
return `${providerId}.${mode}`;
|
|
2687
|
+
}
|
|
2688
|
+
function getVerificationCommand(packageManager) {
|
|
2689
|
+
switch (packageManager) {
|
|
2690
|
+
case "pnpm":
|
|
2691
|
+
return "pnpm dev";
|
|
2692
|
+
case "yarn":
|
|
2693
|
+
return "yarn dev";
|
|
2694
|
+
case "bun":
|
|
2695
|
+
return "bun run dev";
|
|
2696
|
+
case "npm":
|
|
2697
|
+
default:
|
|
2698
|
+
return "npm run dev";
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
async function buildVerification(projectRoot, packageManager) {
|
|
2702
|
+
const packageJson = await readJSON(
|
|
2703
|
+
path12.join(projectRoot, "package.json")
|
|
2704
|
+
);
|
|
2705
|
+
if (packageJson?.scripts?.dev) {
|
|
2706
|
+
const devCommand = getVerificationCommand(packageManager);
|
|
2707
|
+
return {
|
|
2708
|
+
available: true,
|
|
2709
|
+
devCommand,
|
|
2710
|
+
message: `Start the local dev server with \`${devCommand}\` to verify Inspecto in the browser.`
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
return {
|
|
2714
|
+
available: false,
|
|
2715
|
+
message: "Start your normal local dev server command to verify Inspecto in the browser."
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
function buildIdeExtensionStatus(input) {
|
|
2719
|
+
if (!input.required) {
|
|
2720
|
+
return {
|
|
2721
|
+
required: false,
|
|
2722
|
+
installed: false,
|
|
2723
|
+
manualRequired: false
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
return {
|
|
2727
|
+
required: true,
|
|
2728
|
+
installed: input.installed,
|
|
2729
|
+
manualRequired: input.manualRequired,
|
|
2730
|
+
installCommand: "code --install-extension inspecto.inspecto",
|
|
2731
|
+
marketplaceUrl: "https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto",
|
|
2732
|
+
openVsxUrl: "https://open-vsx.org/extension/inspecto/inspecto"
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
async function detectFrameworkSupportByPackage2(repoRoot, context) {
|
|
2736
|
+
const packagePaths = new Set(
|
|
2737
|
+
context.buildTools.supported.map((item) => normalizePackagePath2(item.packagePath))
|
|
2738
|
+
);
|
|
2739
|
+
const supportByPackage = {};
|
|
2740
|
+
await Promise.all(
|
|
2741
|
+
Array.from(packagePaths).map(async (packagePath) => {
|
|
2742
|
+
const frameworkResult = await detectFrameworks(
|
|
2743
|
+
packagePath ? path12.join(repoRoot, packagePath) : repoRoot
|
|
2744
|
+
);
|
|
2745
|
+
supportByPackage[packagePath] = frameworkResult.supported;
|
|
2746
|
+
})
|
|
2747
|
+
);
|
|
2748
|
+
return supportByPackage;
|
|
2749
|
+
}
|
|
2750
|
+
async function buildTargetedContext(rootContext, packagePath) {
|
|
2751
|
+
const projectRoot = packagePath ? path12.join(rootContext.root, packagePath) : rootContext.root;
|
|
2752
|
+
const [frameworks, ides, providers] = await Promise.all([
|
|
2753
|
+
detectFrameworks(projectRoot),
|
|
2754
|
+
detectIDE(projectRoot),
|
|
2755
|
+
detectProviders(projectRoot)
|
|
2756
|
+
]);
|
|
2757
|
+
return {
|
|
2758
|
+
root: projectRoot,
|
|
2759
|
+
packageManager: rootContext.packageManager,
|
|
2760
|
+
buildTools: {
|
|
2761
|
+
supported: rootContext.buildTools.supported.filter((item) => {
|
|
2762
|
+
return normalizePackagePath2(item.packagePath) === packagePath;
|
|
2763
|
+
}),
|
|
2764
|
+
unsupported: []
|
|
2765
|
+
},
|
|
2766
|
+
frameworks: {
|
|
2767
|
+
supported: frameworks.supported,
|
|
2768
|
+
unsupported: frameworks.unsupported.map((item) => item.name)
|
|
2769
|
+
},
|
|
2770
|
+
ides: ides.detected.map(({ ide, supported }) => ({ ide, supported })),
|
|
2771
|
+
providers: providers.detected.map(({ id, label, supported, preferredMode }) => ({
|
|
2772
|
+
id,
|
|
2773
|
+
label,
|
|
2774
|
+
supported,
|
|
2775
|
+
preferredMode
|
|
2776
|
+
}))
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
function buildOnboardingSummary(plan2, projectRoot) {
|
|
2780
|
+
const changes = plan2.actions.filter((action) => action.type !== "manual_step").map((action) => action.description);
|
|
2781
|
+
const risks = [...plan2.warnings.map((item) => item.message)];
|
|
2782
|
+
const manualFollowUp = planManualFollowUp(plan2);
|
|
2783
|
+
let headline = `Inspecto is ready to onboard ${projectRoot}.`;
|
|
2784
|
+
if (manualFollowUp.length > 0) {
|
|
2785
|
+
headline = `Inspecto can partially onboard ${projectRoot}, but manual follow-up remains.`;
|
|
2786
|
+
} else if (plan2.status === "blocked") {
|
|
2787
|
+
headline = `Inspecto could not build an automatic onboarding path for ${projectRoot}.`;
|
|
2788
|
+
}
|
|
2789
|
+
return {
|
|
2790
|
+
headline,
|
|
2791
|
+
changes,
|
|
2792
|
+
risks,
|
|
2793
|
+
manualFollowUp
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
function buildConfirmation(plan2, summary, session, options) {
|
|
2797
|
+
if (options.yes) {
|
|
2798
|
+
return { required: false };
|
|
2799
|
+
}
|
|
2800
|
+
const reasons = [];
|
|
2801
|
+
if (session.targetWasHeuristic) reasons.push("a monorepo target was preselected");
|
|
2802
|
+
if (summary.manualFollowUp.length > 0) reasons.push("manual follow-up will remain after setup");
|
|
2803
|
+
if (options.noExtension || options.skipInstall) reasons.push("non-core setup steps are being skipped");
|
|
2804
|
+
if (session.supportedIdeCount > 1 || session.supportedProviderCount > 1) {
|
|
2805
|
+
reasons.push("multiple IDE or provider choices are still relevant");
|
|
2806
|
+
}
|
|
2807
|
+
if (plan2.warnings.length > 0) reasons.push("the CLI detected non-blocking risk");
|
|
2808
|
+
if (reasons.length === 0) {
|
|
2809
|
+
return { required: false };
|
|
2810
|
+
}
|
|
2811
|
+
return {
|
|
2812
|
+
required: true,
|
|
2813
|
+
reason: reasons.join("; "),
|
|
2814
|
+
question: "Proceed with Inspecto onboarding using the proposed default target and settings?"
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
function buildPreApplyResult(status, session) {
|
|
2818
|
+
const diagnostics = session.summary.risks.length > 0 || session.summary.manualFollowUp.length > 0 || session.plan.blockers.length > 0 ? {
|
|
2819
|
+
warnings: session.summary.risks,
|
|
2820
|
+
errors: session.plan.blockers.map((item) => item.message),
|
|
2821
|
+
nextSteps: session.summary.manualFollowUp
|
|
2822
|
+
} : void 0;
|
|
2823
|
+
return {
|
|
2824
|
+
status,
|
|
2825
|
+
target: session.target,
|
|
2826
|
+
summary: session.summary,
|
|
2827
|
+
confirmation: session.confirmation,
|
|
2828
|
+
ideExtension: buildIdeExtensionStatus({
|
|
2829
|
+
required: session.plan.defaults.extension,
|
|
2830
|
+
installed: false,
|
|
2831
|
+
manualRequired: session.plan.defaults.extension
|
|
2832
|
+
}),
|
|
2833
|
+
verification: session.verification,
|
|
2834
|
+
diagnostics
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
function buildExecutionResult(session, applyResult) {
|
|
2838
|
+
return {
|
|
2839
|
+
changedFiles: Array.from(
|
|
2840
|
+
new Set(applyResult.mutations.map((item) => item.path).filter((value) => !!value))
|
|
2841
|
+
),
|
|
2842
|
+
installedDependencies: applyResult.mutations.map((item) => item.name).filter((value) => !!value),
|
|
2843
|
+
selectedProviderDefault: session.providerDefault,
|
|
2844
|
+
selectedIDE: session.selectedIDE?.ide,
|
|
2845
|
+
mutations: applyResult.mutations
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
function buildExecutionDiagnostics(session, applyResult) {
|
|
2849
|
+
const warnings = [...session.plan.warnings.map((item) => item.message)];
|
|
2850
|
+
const errors = [];
|
|
2851
|
+
if (applyResult.postInstall.installFailed) {
|
|
2852
|
+
errors.push("Dependency installation failed during onboarding.");
|
|
2853
|
+
}
|
|
2854
|
+
if (applyResult.postInstall.injectionFailed) {
|
|
2855
|
+
warnings.push("Automatic plugin injection did not finish cleanly.");
|
|
2856
|
+
}
|
|
2857
|
+
if (applyResult.postInstall.manualExtensionInstallNeeded) {
|
|
2858
|
+
warnings.push("IDE extension installation still needs manual completion.");
|
|
2859
|
+
}
|
|
2860
|
+
if (warnings.length === 0 && errors.length === 0 && applyResult.postInstall.nextSteps.length === 0) {
|
|
2861
|
+
return void 0;
|
|
2862
|
+
}
|
|
2863
|
+
return {
|
|
2864
|
+
warnings,
|
|
2865
|
+
errors,
|
|
2866
|
+
nextSteps: applyResult.postInstall.nextSteps
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
async function resolveOnboardingSession(root, options = {}) {
|
|
2870
|
+
const rootContext = await buildOnboardingContext(root);
|
|
2871
|
+
const rootVerification = await buildVerification(root, rootContext.packageManager);
|
|
2872
|
+
const frameworkSupportByPackage = await detectFrameworkSupportByPackage2(root, rootContext);
|
|
2873
|
+
const target = resolveOnboardingTarget({
|
|
2874
|
+
repoRoot: root,
|
|
2875
|
+
buildTools: rootContext.buildTools.supported,
|
|
2876
|
+
frameworkSupportByPackage,
|
|
2877
|
+
selectedPackagePath: options.target
|
|
2878
|
+
});
|
|
2879
|
+
if (target.candidates.length === 0) {
|
|
2880
|
+
const plan3 = createPlanResult(rootContext);
|
|
2881
|
+
return {
|
|
2882
|
+
status: "error",
|
|
2883
|
+
target,
|
|
2884
|
+
summary: buildOnboardingSummary(plan3, root),
|
|
2885
|
+
confirmation: { required: false },
|
|
2886
|
+
verification: rootVerification,
|
|
2887
|
+
context: rootContext,
|
|
2888
|
+
plan: plan3,
|
|
2889
|
+
projectRoot: root
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
if (target.status === "needs_selection") {
|
|
2893
|
+
const plan3 = createPlanResult(rootContext);
|
|
2894
|
+
const summary2 = {
|
|
2895
|
+
headline: "Inspecto found multiple plausible app targets and needs one selection.",
|
|
2896
|
+
changes: [],
|
|
2897
|
+
risks: [],
|
|
2898
|
+
manualFollowUp: []
|
|
2899
|
+
};
|
|
2900
|
+
return {
|
|
2901
|
+
status: "needs_target_selection",
|
|
2902
|
+
target,
|
|
2903
|
+
summary: summary2,
|
|
2904
|
+
confirmation: { required: false },
|
|
2905
|
+
verification: rootVerification,
|
|
2906
|
+
context: rootContext,
|
|
2907
|
+
plan: plan3,
|
|
2908
|
+
projectRoot: root
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
const packagePath = normalizePackagePath2(target.selected?.packagePath);
|
|
2912
|
+
const context = await buildTargetedContext(rootContext, packagePath);
|
|
2913
|
+
const verification = await buildVerification(context.root, context.packageManager);
|
|
2914
|
+
const plan2 = createPlanResult(context);
|
|
2915
|
+
const summary = buildOnboardingSummary(plan2, context.root);
|
|
2916
|
+
const confirmation = buildConfirmation(
|
|
2917
|
+
plan2,
|
|
2918
|
+
summary,
|
|
2919
|
+
{
|
|
2920
|
+
targetWasHeuristic: target.candidates.length > 1 && options.target === void 0,
|
|
2921
|
+
supportedIdeCount: context.ides.filter((item) => item.supported).length,
|
|
2922
|
+
supportedProviderCount: context.providers.filter((item) => item.supported).length
|
|
2923
|
+
},
|
|
2924
|
+
options
|
|
2925
|
+
);
|
|
2926
|
+
const selectedProvider = context.providers.find((provider) => provider.id === plan2.defaults.provider) ?? null;
|
|
2927
|
+
const selectedIDE = context.ides.find((ide) => ide.ide === plan2.defaults.ide) ?? context.ides.find((ide) => ide.supported) ?? null;
|
|
2928
|
+
let status = "success";
|
|
2929
|
+
if (plan2.status === "blocked") {
|
|
2930
|
+
status = "error";
|
|
2931
|
+
} else if (confirmation.required) {
|
|
2932
|
+
status = "needs_confirmation";
|
|
2933
|
+
} else if (summary.manualFollowUp.length > 0 || plan2.warnings.length > 0) {
|
|
2934
|
+
status = "partial_success";
|
|
2935
|
+
}
|
|
2936
|
+
return {
|
|
2937
|
+
status,
|
|
2938
|
+
target,
|
|
2939
|
+
summary,
|
|
2940
|
+
confirmation,
|
|
2941
|
+
verification,
|
|
2942
|
+
context,
|
|
2943
|
+
plan: plan2,
|
|
2944
|
+
projectRoot: context.root,
|
|
2945
|
+
selectedIDE,
|
|
2946
|
+
providerDefault: getProviderDefault2(plan2.defaults.provider, selectedProvider?.preferredMode)
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
async function applyResolvedOnboardingSession(session, options = {}) {
|
|
2950
|
+
const verification = await buildVerification(session.projectRoot, session.context.packageManager);
|
|
2951
|
+
const applyResult = await applyOnboardingPlan({
|
|
2952
|
+
repoRoot: process.cwd(),
|
|
2953
|
+
projectRoot: session.projectRoot,
|
|
2954
|
+
packageManager: session.context.packageManager,
|
|
2955
|
+
supportedBuildTargets: session.context.buildTools.supported,
|
|
2956
|
+
options: {
|
|
2957
|
+
shared: options.shared ?? session.plan.defaults.shared,
|
|
2958
|
+
skipInstall: options.skipInstall ?? false,
|
|
2959
|
+
dryRun: options.dryRun ?? false,
|
|
2960
|
+
noExtension: options.noExtension ?? !session.plan.defaults.extension,
|
|
2961
|
+
quiet: options.json ?? false
|
|
2962
|
+
},
|
|
2963
|
+
selectedIDE: session.selectedIDE,
|
|
2964
|
+
providerDefault: session.providerDefault,
|
|
2965
|
+
plan: session.plan
|
|
2966
|
+
});
|
|
2967
|
+
const diagnostics = buildExecutionDiagnostics(session, applyResult);
|
|
2968
|
+
const status = applyResult.postInstall.installFailed && session.context.buildTools.supported.length === 0 ? "error" : diagnostics?.nextSteps.length || diagnostics?.errors.length || diagnostics?.warnings.length ? "partial_success" : "success";
|
|
2969
|
+
return {
|
|
2970
|
+
status,
|
|
2971
|
+
target: session.target,
|
|
2972
|
+
summary: session.summary,
|
|
2973
|
+
confirmation: session.confirmation,
|
|
2974
|
+
ideExtension: buildIdeExtensionStatus({
|
|
2975
|
+
required: session.plan.defaults.extension,
|
|
2976
|
+
installed: session.plan.defaults.extension && !applyResult.postInstall.manualExtensionInstallNeeded,
|
|
2977
|
+
manualRequired: applyResult.postInstall.manualExtensionInstallNeeded
|
|
2978
|
+
}),
|
|
2979
|
+
verification,
|
|
2980
|
+
result: buildExecutionResult(session, applyResult),
|
|
2981
|
+
diagnostics
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
function buildDeferredOnboardResult(session) {
|
|
2985
|
+
return buildPreApplyResult(session.status, session);
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// src/commands/onboard.ts
|
|
2989
|
+
function printOnboardResult(result) {
|
|
2990
|
+
log.header("Inspecto Onboard");
|
|
2991
|
+
log.info(`Status: ${result.status}`);
|
|
2992
|
+
log.info(result.summary.headline);
|
|
2993
|
+
for (const change of result.summary.changes) {
|
|
2994
|
+
log.hint(change);
|
|
2995
|
+
}
|
|
2996
|
+
for (const step of result.diagnostics?.nextSteps ?? []) {
|
|
2997
|
+
log.warn(step);
|
|
2998
|
+
}
|
|
2999
|
+
if (result.confirmation.required && result.confirmation.question) {
|
|
3000
|
+
log.warn(result.confirmation.question);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
async function onboard(options = {}) {
|
|
3004
|
+
const root = process.cwd();
|
|
3005
|
+
const session = await resolveOnboardingSession(root, options);
|
|
3006
|
+
if (session.status === "error" || session.status === "needs_target_selection" || session.status === "needs_confirmation") {
|
|
3007
|
+
return writeCommandOutput(
|
|
3008
|
+
buildDeferredOnboardResult(session),
|
|
3009
|
+
options.json ?? false,
|
|
3010
|
+
printOnboardResult
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
const result = await applyResolvedOnboardingSession(session, options);
|
|
3014
|
+
return writeCommandOutput(result, options.json ?? false, printOnboardResult);
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
// src/commands/plan.ts
|
|
3018
|
+
function printPlanResult(result) {
|
|
3019
|
+
log.header("Inspecto Plan");
|
|
3020
|
+
log.info(`Status: ${result.status}`);
|
|
3021
|
+
log.info(`Strategy: ${result.strategy}`);
|
|
3022
|
+
if (result.defaults.provider) {
|
|
3023
|
+
log.info(`Default provider: ${result.defaults.provider}`);
|
|
3024
|
+
}
|
|
3025
|
+
if (result.defaults.ide) {
|
|
3026
|
+
log.info(`Default IDE: ${result.defaults.ide}`);
|
|
3027
|
+
}
|
|
3028
|
+
log.info(`Shared mode: ${result.defaults.shared ? "enabled" : "disabled"}`);
|
|
3029
|
+
log.info(`Extension mode: ${result.defaults.extension ? "enabled" : "disabled"}`);
|
|
3030
|
+
if (result.actions.length > 0) {
|
|
3031
|
+
log.blank();
|
|
3032
|
+
log.info("Actions:");
|
|
3033
|
+
for (const action of result.actions) {
|
|
3034
|
+
log.hint(`${action.type}: ${action.target} \u2014 ${action.description}`);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
for (const blocker of result.blockers) {
|
|
3038
|
+
log.error(blocker.message);
|
|
3039
|
+
}
|
|
3040
|
+
for (const warning of result.warnings) {
|
|
3041
|
+
log.warn(warning.message);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
async function plan(json = false) {
|
|
3045
|
+
const context = await buildOnboardingContext(process.cwd());
|
|
3046
|
+
const result = createPlanResult(context);
|
|
3047
|
+
return writeCommandOutput(result, json, printPlanResult);
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
// src/commands/teardown.ts
|
|
3051
|
+
import path13 from "path";
|
|
3052
|
+
async function teardown() {
|
|
3053
|
+
const root = process.cwd();
|
|
3054
|
+
log.header("Inspecto Teardown");
|
|
3055
|
+
const lockPath = path13.join(root, ".inspecto", "install.lock");
|
|
3056
|
+
const lock = await readJSON(lockPath);
|
|
3057
|
+
if (!lock) {
|
|
3058
|
+
log.warn("No .inspecto/install.lock found. Running in best-effort mode.");
|
|
3059
|
+
log.blank();
|
|
3060
|
+
const pm = await detectPackageManager(root);
|
|
3061
|
+
try {
|
|
3062
|
+
const cmd = getUninstallCommand(pm, "@inspecto-dev/plugin");
|
|
3063
|
+
await shell(cmd, root);
|
|
3064
|
+
log.success("Removed @inspecto-dev/plugin from devDependencies");
|
|
3065
|
+
} catch {
|
|
3066
|
+
log.warn("Could not remove @inspecto-dev/plugin (may not be installed)");
|
|
3067
|
+
}
|
|
3068
|
+
if (await exists(path13.join(root, ".inspecto"))) {
|
|
3069
|
+
await removeDir(path13.join(root, ".inspecto"));
|
|
3070
|
+
log.success("Deleted .inspecto/ directory");
|
|
3071
|
+
}
|
|
3072
|
+
await cleanGitignore(root);
|
|
3073
|
+
log.success("Cleaned .gitignore entries");
|
|
3074
|
+
log.warn("Cannot restore build config auto-magically");
|
|
3075
|
+
log.hint("Please manually remove the inspecto() plugin from your build config");
|
|
3076
|
+
log.blank();
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
log.success("Reading .inspecto/install.lock...");
|
|
3080
|
+
log.blank();
|
|
3081
|
+
for (const mutation of lock.mutations) {
|
|
3082
|
+
switch (mutation.type) {
|
|
3083
|
+
case "file_modified": {
|
|
3084
|
+
if (!mutation.path) break;
|
|
3085
|
+
if (mutation.path === ".gitignore") {
|
|
3086
|
+
log.success("Cleaned .gitignore entries");
|
|
3087
|
+
} else if (mutation.path) {
|
|
3088
|
+
log.warn(`Cannot auto-restore ${mutation.path}`);
|
|
3089
|
+
log.hint(`Please manually remove the inspecto plugin from ${mutation.path}`);
|
|
3090
|
+
}
|
|
3091
|
+
break;
|
|
3092
|
+
}
|
|
3093
|
+
case "file_created": {
|
|
3094
|
+
break;
|
|
3095
|
+
}
|
|
3096
|
+
case "dependency_added": {
|
|
3097
|
+
if (mutation.name) {
|
|
3098
|
+
const pm = await detectPackageManager(root);
|
|
3099
|
+
try {
|
|
3100
|
+
const cmd = getUninstallCommand(pm, mutation.name);
|
|
3101
|
+
await shell(cmd, root);
|
|
3102
|
+
log.success(`Removed ${mutation.name} from devDependencies`);
|
|
3103
|
+
} catch {
|
|
3104
|
+
log.warn(`Could not remove ${mutation.name}`);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
break;
|
|
3108
|
+
}
|
|
3109
|
+
case "extension_installed": {
|
|
3110
|
+
if (mutation.id) {
|
|
3111
|
+
log.warn(`VS Code extension not auto-uninstalled`);
|
|
3112
|
+
log.hint(`Run: code --uninstall-extension ${mutation.id}`);
|
|
3113
|
+
}
|
|
3114
|
+
break;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
await removeDir(path13.join(root, ".inspecto"));
|
|
3119
|
+
log.success("Deleted .inspecto/ directory");
|
|
3120
|
+
await cleanGitignore(root);
|
|
3121
|
+
log.blank();
|
|
3122
|
+
log.success("Done. All Inspecto traces removed.");
|
|
3123
|
+
log.blank();
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
export {
|
|
3127
|
+
exists,
|
|
3128
|
+
writeFile,
|
|
3129
|
+
log,
|
|
3130
|
+
writeCommandOutput,
|
|
3131
|
+
reportCommandError,
|
|
3132
|
+
apply,
|
|
3133
|
+
detect,
|
|
3134
|
+
init,
|
|
3135
|
+
collectDoctorResult,
|
|
3136
|
+
doctor,
|
|
3137
|
+
onboard,
|
|
3138
|
+
plan,
|
|
3139
|
+
teardown
|
|
3140
|
+
};
|