@sixsevenai/ai-dlc-installer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +4185 -0
- package/dist/cli.js.map +1 -0
- package/package.json +53 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/orchestrator/workflow.ts
|
|
4
|
+
import * as path6 from "path";
|
|
5
|
+
|
|
6
|
+
// src/orchestrator/types.ts
|
|
7
|
+
var EXIT_SUCCESS = 0;
|
|
8
|
+
var EXIT_ERROR = 1;
|
|
9
|
+
var EXIT_CANCELLED = 2;
|
|
10
|
+
|
|
11
|
+
// src/orchestrator/config.ts
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
import * as fs from "fs/promises";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var INSTALLER_VERSION = "0.1.0";
|
|
16
|
+
var CLAUDE_DIRECTORY_NAME = ".claude";
|
|
17
|
+
var SOURCE_LIBRARY_RELATIVE = "../../ai-dlc-library";
|
|
18
|
+
async function resolveSourceDirectory() {
|
|
19
|
+
const envSource = process.env["AIDLC_SOURCE_DIR"];
|
|
20
|
+
if (envSource) {
|
|
21
|
+
try {
|
|
22
|
+
const stat5 = await fs.stat(envSource);
|
|
23
|
+
if (stat5.isDirectory()) {
|
|
24
|
+
return path.resolve(envSource);
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
30
|
+
const thisDir = path.dirname(thisFile);
|
|
31
|
+
const resolved = path.resolve(thisDir, SOURCE_LIBRARY_RELATIVE);
|
|
32
|
+
try {
|
|
33
|
+
const stat5 = await fs.stat(resolved);
|
|
34
|
+
if (stat5.isDirectory()) {
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
const cwdSource = path.resolve(process.cwd(), "src", "ai-dlc-library");
|
|
40
|
+
try {
|
|
41
|
+
const stat5 = await fs.stat(cwdSource);
|
|
42
|
+
if (stat5.isDirectory()) {
|
|
43
|
+
return cwdSource;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Cannot locate AI-DLC library source directory. Set the AIDLC_SOURCE_DIR environment variable to the library path, or run the installer from the SixSevenAI repository root."
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
var WORKFLOW_STEPS = [
|
|
52
|
+
{ id: "detect-platform", name: "Detect Platform", order: 1, isSkippable: false },
|
|
53
|
+
{ id: "check-prerequisites", name: "Check Prerequisites", order: 2, isSkippable: false },
|
|
54
|
+
{ id: "show-banner", name: "Show Banner", order: 3, isSkippable: true },
|
|
55
|
+
{ id: "select-directory", name: "Select Target Directory", order: 4, isSkippable: true },
|
|
56
|
+
{ id: "scan-source", name: "Scan Source Components", order: 5, isSkippable: false },
|
|
57
|
+
{ id: "detect-conflicts", name: "Detect Conflicts", order: 6, isSkippable: true },
|
|
58
|
+
{ id: "confirm-installation", name: "Confirm Installation", order: 7, isSkippable: true },
|
|
59
|
+
{ id: "execute-installation", name: "Install Files", order: 8, isSkippable: false },
|
|
60
|
+
{ id: "validate-installation", name: "Validate Installation", order: 9, isSkippable: false },
|
|
61
|
+
{ id: "show-summary", name: "Show Summary", order: 10, isSkippable: false }
|
|
62
|
+
];
|
|
63
|
+
function shouldSkipStep(stepId, options, isInteractive) {
|
|
64
|
+
switch (stepId) {
|
|
65
|
+
case "show-banner":
|
|
66
|
+
return !isInteractive || options.quiet;
|
|
67
|
+
case "select-directory":
|
|
68
|
+
return options.target !== null;
|
|
69
|
+
case "detect-conflicts":
|
|
70
|
+
return options.force;
|
|
71
|
+
case "confirm-installation":
|
|
72
|
+
return options.force || !isInteractive;
|
|
73
|
+
default:
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/ui/theme.ts
|
|
79
|
+
import chalk from "chalk";
|
|
80
|
+
import gradientString from "gradient-string";
|
|
81
|
+
var TRUECOLOR_SCHEME = {
|
|
82
|
+
header: "#3B82F6",
|
|
83
|
+
accent: "#06B6D4",
|
|
84
|
+
success: "#10B981",
|
|
85
|
+
error: "#EF4444",
|
|
86
|
+
warning: "#F59E0B",
|
|
87
|
+
muted: "#6B7280"
|
|
88
|
+
};
|
|
89
|
+
var BASIC_SCHEME = {
|
|
90
|
+
header: "blue",
|
|
91
|
+
accent: "cyan",
|
|
92
|
+
success: "green",
|
|
93
|
+
error: "red",
|
|
94
|
+
warning: "yellow",
|
|
95
|
+
muted: "gray"
|
|
96
|
+
};
|
|
97
|
+
var PLAIN_SCHEME = {
|
|
98
|
+
header: "",
|
|
99
|
+
accent: "",
|
|
100
|
+
success: "",
|
|
101
|
+
error: "",
|
|
102
|
+
warning: "",
|
|
103
|
+
muted: ""
|
|
104
|
+
};
|
|
105
|
+
var UNICODE_SYMBOLS = {
|
|
106
|
+
pending: "\u25CB",
|
|
107
|
+
// White circle
|
|
108
|
+
running: "\u25CF",
|
|
109
|
+
// Filled circle
|
|
110
|
+
completed: "\u2713",
|
|
111
|
+
// Checkmark
|
|
112
|
+
failed: "\u2717",
|
|
113
|
+
// Cross
|
|
114
|
+
skipped: "\u2500",
|
|
115
|
+
// Horizontal line
|
|
116
|
+
bullet: "\u2022",
|
|
117
|
+
// Bullet
|
|
118
|
+
arrow: "\u25B8",
|
|
119
|
+
// Right-pointing triangle
|
|
120
|
+
boxTopLeft: "\u250C",
|
|
121
|
+
boxTopRight: "\u2510",
|
|
122
|
+
boxBottomLeft: "\u2514",
|
|
123
|
+
boxBottomRight: "\u2518",
|
|
124
|
+
boxHorizontal: "\u2500",
|
|
125
|
+
boxVertical: "\u2502",
|
|
126
|
+
boxDividerLeft: "\u251C",
|
|
127
|
+
boxDividerRight: "\u2524",
|
|
128
|
+
doubleTopLeft: "\u2554",
|
|
129
|
+
doubleTopRight: "\u2557",
|
|
130
|
+
doubleBottomLeft: "\u255A",
|
|
131
|
+
doubleBottomRight: "\u255D",
|
|
132
|
+
doubleHorizontal: "\u2550",
|
|
133
|
+
doubleVertical: "\u2551"
|
|
134
|
+
};
|
|
135
|
+
var ASCII_SYMBOLS = {
|
|
136
|
+
pending: "o",
|
|
137
|
+
running: "*",
|
|
138
|
+
completed: "+",
|
|
139
|
+
failed: "x",
|
|
140
|
+
skipped: "-",
|
|
141
|
+
bullet: "*",
|
|
142
|
+
arrow: ">",
|
|
143
|
+
boxTopLeft: "+",
|
|
144
|
+
boxTopRight: "+",
|
|
145
|
+
boxBottomLeft: "+",
|
|
146
|
+
boxBottomRight: "+",
|
|
147
|
+
boxHorizontal: "-",
|
|
148
|
+
boxVertical: "|",
|
|
149
|
+
boxDividerLeft: "+",
|
|
150
|
+
boxDividerRight: "+",
|
|
151
|
+
doubleTopLeft: "+",
|
|
152
|
+
doubleTopRight: "+",
|
|
153
|
+
doubleBottomLeft: "+",
|
|
154
|
+
doubleBottomRight: "+",
|
|
155
|
+
doubleHorizontal: "=",
|
|
156
|
+
doubleVertical: "|"
|
|
157
|
+
};
|
|
158
|
+
function selectTier(capabilities, overrides) {
|
|
159
|
+
if (overrides.forceNoColor) {
|
|
160
|
+
return "plain";
|
|
161
|
+
}
|
|
162
|
+
if (!capabilities.unicodeSupport) {
|
|
163
|
+
return "plain";
|
|
164
|
+
}
|
|
165
|
+
if (capabilities.colorSupport === "none") {
|
|
166
|
+
return "plain";
|
|
167
|
+
}
|
|
168
|
+
if (capabilities.colorSupport === "truecolor") {
|
|
169
|
+
return "full";
|
|
170
|
+
}
|
|
171
|
+
return "basic";
|
|
172
|
+
}
|
|
173
|
+
var ThemeEngine = class _ThemeEngine {
|
|
174
|
+
_tier;
|
|
175
|
+
_colorScheme;
|
|
176
|
+
_symbolSet;
|
|
177
|
+
_terminalWidth;
|
|
178
|
+
_forceNoColor;
|
|
179
|
+
constructor(params) {
|
|
180
|
+
this._tier = params.tier;
|
|
181
|
+
this._colorScheme = params.colorScheme;
|
|
182
|
+
this._symbolSet = params.symbolSet;
|
|
183
|
+
this._terminalWidth = params.terminalWidth;
|
|
184
|
+
this._forceNoColor = params.forceNoColor;
|
|
185
|
+
}
|
|
186
|
+
/** Current rendering tier. */
|
|
187
|
+
get tier() {
|
|
188
|
+
return this._tier;
|
|
189
|
+
}
|
|
190
|
+
/** Active color scheme. */
|
|
191
|
+
get colorScheme() {
|
|
192
|
+
return this._colorScheme;
|
|
193
|
+
}
|
|
194
|
+
/** Active symbol set. */
|
|
195
|
+
get symbolSet() {
|
|
196
|
+
return this._symbolSet;
|
|
197
|
+
}
|
|
198
|
+
/** Detected or default terminal width. */
|
|
199
|
+
get terminalWidth() {
|
|
200
|
+
return this._terminalWidth;
|
|
201
|
+
}
|
|
202
|
+
/** Whether color output is forcibly disabled. */
|
|
203
|
+
get forceNoColor() {
|
|
204
|
+
return this._forceNoColor;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Apply a semantic color to text. Returns unmodified text for Plain tier.
|
|
208
|
+
*/
|
|
209
|
+
colorize(text2, color) {
|
|
210
|
+
if (this._tier === "plain") {
|
|
211
|
+
return text2;
|
|
212
|
+
}
|
|
213
|
+
const colorValue = this._colorScheme[color];
|
|
214
|
+
if (!colorValue) {
|
|
215
|
+
return text2;
|
|
216
|
+
}
|
|
217
|
+
if (this._tier === "full") {
|
|
218
|
+
return chalk.hex(colorValue)(text2);
|
|
219
|
+
}
|
|
220
|
+
const chalkFn = this.getBasicChalkFn(color);
|
|
221
|
+
return chalkFn(text2);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Apply bold styling to text. Returns unmodified text for Plain tier.
|
|
225
|
+
*/
|
|
226
|
+
bold(text2) {
|
|
227
|
+
if (this._tier === "plain") {
|
|
228
|
+
return text2;
|
|
229
|
+
}
|
|
230
|
+
return chalk.bold(text2);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Apply dim styling to text. Returns unmodified text for Plain tier.
|
|
234
|
+
*/
|
|
235
|
+
dim(text2) {
|
|
236
|
+
if (this._tier === "plain") {
|
|
237
|
+
return text2;
|
|
238
|
+
}
|
|
239
|
+
return chalk.dim(text2);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Apply underline styling to text. Returns unmodified text for Plain tier.
|
|
243
|
+
*/
|
|
244
|
+
underline(text2) {
|
|
245
|
+
if (this._tier === "plain") {
|
|
246
|
+
return text2;
|
|
247
|
+
}
|
|
248
|
+
return chalk.underline(text2);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Apply gradient coloring to text. Full tier uses gradient-string.
|
|
252
|
+
* Basic tier applies a single accent color. Plain tier returns unmodified text.
|
|
253
|
+
*/
|
|
254
|
+
applyGradient(text2, gradient) {
|
|
255
|
+
if (this._tier === "plain") {
|
|
256
|
+
return text2;
|
|
257
|
+
}
|
|
258
|
+
if (this._tier === "full") {
|
|
259
|
+
const colors = gradient?.colors ?? ["#4F46E5", "#06B6D4"];
|
|
260
|
+
const grad = gradientString(...colors);
|
|
261
|
+
return grad(text2);
|
|
262
|
+
}
|
|
263
|
+
return chalk.blue(text2);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get the status symbol for a step status, colorized for the active tier.
|
|
267
|
+
*/
|
|
268
|
+
statusSymbol(status) {
|
|
269
|
+
const symbol = this._symbolSet[status];
|
|
270
|
+
if (this._tier === "plain") {
|
|
271
|
+
return `[${symbol}]`;
|
|
272
|
+
}
|
|
273
|
+
switch (status) {
|
|
274
|
+
case "completed":
|
|
275
|
+
return this.colorize(symbol, "success");
|
|
276
|
+
case "failed":
|
|
277
|
+
return this.colorize(symbol, "error");
|
|
278
|
+
case "running":
|
|
279
|
+
return this.colorize(symbol, "accent");
|
|
280
|
+
case "skipped":
|
|
281
|
+
return this.colorize(symbol, "muted");
|
|
282
|
+
case "pending":
|
|
283
|
+
return this.colorize(symbol, "muted");
|
|
284
|
+
default:
|
|
285
|
+
return symbol;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Get a chalk function for basic (16-color) mode.
|
|
290
|
+
*/
|
|
291
|
+
getBasicChalkFn(color) {
|
|
292
|
+
switch (color) {
|
|
293
|
+
case "header":
|
|
294
|
+
return chalk.blue;
|
|
295
|
+
case "accent":
|
|
296
|
+
return chalk.cyan;
|
|
297
|
+
case "success":
|
|
298
|
+
return chalk.green;
|
|
299
|
+
case "error":
|
|
300
|
+
return chalk.red;
|
|
301
|
+
case "warning":
|
|
302
|
+
return chalk.yellow;
|
|
303
|
+
case "muted":
|
|
304
|
+
return chalk.gray;
|
|
305
|
+
default:
|
|
306
|
+
return (t) => t;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Create and initialize a ThemeEngine from terminal capabilities and overrides.
|
|
311
|
+
*/
|
|
312
|
+
static initialize(capabilities, overrides = { forceNoColor: false, forceColor: false }) {
|
|
313
|
+
const tier = selectTier(capabilities, overrides);
|
|
314
|
+
let colorScheme;
|
|
315
|
+
let symbolSet;
|
|
316
|
+
switch (tier) {
|
|
317
|
+
case "full":
|
|
318
|
+
colorScheme = TRUECOLOR_SCHEME;
|
|
319
|
+
symbolSet = UNICODE_SYMBOLS;
|
|
320
|
+
break;
|
|
321
|
+
case "basic":
|
|
322
|
+
colorScheme = BASIC_SCHEME;
|
|
323
|
+
symbolSet = UNICODE_SYMBOLS;
|
|
324
|
+
break;
|
|
325
|
+
case "plain":
|
|
326
|
+
default:
|
|
327
|
+
colorScheme = PLAIN_SCHEME;
|
|
328
|
+
symbolSet = ASCII_SYMBOLS;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
const terminalWidth = capabilities.width > 0 ? capabilities.width : 80;
|
|
332
|
+
return new _ThemeEngine({
|
|
333
|
+
tier,
|
|
334
|
+
colorScheme,
|
|
335
|
+
symbolSet,
|
|
336
|
+
terminalWidth,
|
|
337
|
+
forceNoColor: overrides.forceNoColor
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// src/ui/banner.ts
|
|
343
|
+
import figlet from "figlet";
|
|
344
|
+
var DEFAULT_GRADIENT = {
|
|
345
|
+
colors: ["#4F46E5", "#06B6D4"],
|
|
346
|
+
direction: "horizontal"
|
|
347
|
+
};
|
|
348
|
+
var DEFAULT_FONT = "ANSI Shadow";
|
|
349
|
+
var NARROW_FONT = "Small";
|
|
350
|
+
function generateFigletText(text2, font) {
|
|
351
|
+
try {
|
|
352
|
+
const result = figlet.textSync(text2, {
|
|
353
|
+
font,
|
|
354
|
+
horizontalLayout: "default",
|
|
355
|
+
verticalLayout: "default"
|
|
356
|
+
});
|
|
357
|
+
return result;
|
|
358
|
+
} catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function renderFallbackBanner(version2, theme) {
|
|
363
|
+
const separator = "=".repeat(Math.min(43, theme.terminalWidth - 4));
|
|
364
|
+
const lines = [];
|
|
365
|
+
lines.push(` ${separator}`);
|
|
366
|
+
lines.push(" SixSevenAI - AI-DLC Framework");
|
|
367
|
+
lines.push(` Installer ${version2}`);
|
|
368
|
+
lines.push(` ${separator}`);
|
|
369
|
+
lines.push("");
|
|
370
|
+
return lines;
|
|
371
|
+
}
|
|
372
|
+
function renderBanner(version2, theme, config) {
|
|
373
|
+
const title = config?.title ?? "SixSevenAI";
|
|
374
|
+
const subtitle = config?.subtitle ?? "AI-DLC Framework Installer";
|
|
375
|
+
const tier = theme.tier;
|
|
376
|
+
const width = theme.terminalWidth;
|
|
377
|
+
let font;
|
|
378
|
+
if (tier === "plain") {
|
|
379
|
+
font = config?.font ?? "Standard";
|
|
380
|
+
} else {
|
|
381
|
+
font = config?.font ?? (width >= 80 ? DEFAULT_FONT : NARROW_FONT);
|
|
382
|
+
}
|
|
383
|
+
const figletText = generateFigletText(title, font);
|
|
384
|
+
if (!figletText) {
|
|
385
|
+
return renderFallbackBanner(version2, theme);
|
|
386
|
+
}
|
|
387
|
+
const figletLines = figletText.split("\n").filter((line) => line.trim().length > 0);
|
|
388
|
+
const maxLineWidth = Math.max(...figletLines.map((line) => line.length));
|
|
389
|
+
if (maxLineWidth > width - 4) {
|
|
390
|
+
if (font !== NARROW_FONT) {
|
|
391
|
+
const narrowText = generateFigletText(title, NARROW_FONT);
|
|
392
|
+
if (narrowText) {
|
|
393
|
+
const narrowLines = narrowText.split("\n").filter((line) => line.trim().length > 0);
|
|
394
|
+
const narrowMax = Math.max(...narrowLines.map((line) => line.length));
|
|
395
|
+
if (narrowMax <= width - 4) {
|
|
396
|
+
return buildBannerOutput(narrowLines, subtitle, version2, theme);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return renderFallbackBanner(version2, theme);
|
|
401
|
+
}
|
|
402
|
+
return buildBannerOutput(figletLines, subtitle, version2, theme);
|
|
403
|
+
}
|
|
404
|
+
function buildBannerOutput(artLines, subtitle, version2, theme) {
|
|
405
|
+
const lines = [];
|
|
406
|
+
for (const artLine of artLines) {
|
|
407
|
+
const indented = ` ${artLine}`;
|
|
408
|
+
switch (theme.tier) {
|
|
409
|
+
case "full":
|
|
410
|
+
lines.push(theme.applyGradient(indented, DEFAULT_GRADIENT));
|
|
411
|
+
break;
|
|
412
|
+
case "basic":
|
|
413
|
+
lines.push(theme.colorize(indented, "header"));
|
|
414
|
+
break;
|
|
415
|
+
case "plain":
|
|
416
|
+
default:
|
|
417
|
+
lines.push(indented);
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
lines.push("");
|
|
422
|
+
const versionStr = version2.startsWith("v") ? version2 : `v${version2}`;
|
|
423
|
+
const subtitleLine = ` ${subtitle} ${versionStr}`;
|
|
424
|
+
switch (theme.tier) {
|
|
425
|
+
case "full": {
|
|
426
|
+
const styledSubtitle = theme.dim(subtitle);
|
|
427
|
+
const styledVersion = theme.bold(theme.colorize(versionStr, "accent"));
|
|
428
|
+
lines.push(` ${styledSubtitle} ${styledVersion}`);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case "basic": {
|
|
432
|
+
const styledSubtitle = theme.dim(subtitle);
|
|
433
|
+
const styledVersion = theme.colorize(versionStr, "accent");
|
|
434
|
+
lines.push(` ${styledSubtitle} ${styledVersion}`);
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
case "plain":
|
|
438
|
+
default:
|
|
439
|
+
lines.push(subtitleLine);
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
lines.push("");
|
|
443
|
+
return lines;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/ui/progress.ts
|
|
447
|
+
import ora from "ora";
|
|
448
|
+
|
|
449
|
+
// src/ui/types.ts
|
|
450
|
+
function formatDuration(ms) {
|
|
451
|
+
if (ms < 1e3) {
|
|
452
|
+
return { milliseconds: ms, formatted: `${ms}ms` };
|
|
453
|
+
}
|
|
454
|
+
if (ms < 6e4) {
|
|
455
|
+
const seconds2 = (ms / 1e3).toFixed(1);
|
|
456
|
+
return { milliseconds: ms, formatted: `${seconds2}s` };
|
|
457
|
+
}
|
|
458
|
+
const minutes = Math.floor(ms / 6e4);
|
|
459
|
+
const seconds = Math.round(ms % 6e4 / 1e3);
|
|
460
|
+
return { milliseconds: ms, formatted: `${minutes}m ${seconds}s` };
|
|
461
|
+
}
|
|
462
|
+
function mapColorSupport(level) {
|
|
463
|
+
switch (level) {
|
|
464
|
+
case 0:
|
|
465
|
+
return "none";
|
|
466
|
+
case 1:
|
|
467
|
+
return "basic";
|
|
468
|
+
case 2:
|
|
469
|
+
return "256";
|
|
470
|
+
case 3:
|
|
471
|
+
return "truecolor";
|
|
472
|
+
default:
|
|
473
|
+
return "none";
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/ui/progress.ts
|
|
478
|
+
var VALID_TRANSITIONS = {
|
|
479
|
+
pending: ["running", "skipped"],
|
|
480
|
+
running: ["completed", "failed"],
|
|
481
|
+
completed: [],
|
|
482
|
+
failed: [],
|
|
483
|
+
skipped: []
|
|
484
|
+
};
|
|
485
|
+
function isValidTransition(from, to) {
|
|
486
|
+
return VALID_TRANSITIONS[from].includes(to);
|
|
487
|
+
}
|
|
488
|
+
var ProgressTracker = class {
|
|
489
|
+
_steps = [];
|
|
490
|
+
_startTime = null;
|
|
491
|
+
_endTime = null;
|
|
492
|
+
_isActive = false;
|
|
493
|
+
_spinner = null;
|
|
494
|
+
/** Ordered list of top-level steps. */
|
|
495
|
+
get steps() {
|
|
496
|
+
return this._steps;
|
|
497
|
+
}
|
|
498
|
+
/** Whether tracking is in progress. */
|
|
499
|
+
get isActive() {
|
|
500
|
+
return this._isActive;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Append a top-level step. Throws if tracker is already finished.
|
|
504
|
+
*/
|
|
505
|
+
addStep(config) {
|
|
506
|
+
if (this._endTime !== null) {
|
|
507
|
+
throw new Error("Cannot add steps to a finished tracker.");
|
|
508
|
+
}
|
|
509
|
+
if (this.findStep(config.id)) {
|
|
510
|
+
throw new Error(`Duplicate step ID: "${config.id}".`);
|
|
511
|
+
}
|
|
512
|
+
const children = (config.children ?? []).map((child) => {
|
|
513
|
+
if (this.findStep(child.id)) {
|
|
514
|
+
throw new Error(`Duplicate step ID: "${child.id}".`);
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
id: child.id,
|
|
518
|
+
label: child.label,
|
|
519
|
+
status: "pending",
|
|
520
|
+
children: [],
|
|
521
|
+
startTime: null,
|
|
522
|
+
endTime: null,
|
|
523
|
+
error: null
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
this._steps.push({
|
|
527
|
+
id: config.id,
|
|
528
|
+
label: config.label,
|
|
529
|
+
status: "pending",
|
|
530
|
+
children,
|
|
531
|
+
startTime: null,
|
|
532
|
+
endTime: null,
|
|
533
|
+
error: null
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Mark the tracker as active and record the start time.
|
|
538
|
+
*/
|
|
539
|
+
start() {
|
|
540
|
+
this._startTime = Date.now();
|
|
541
|
+
this._isActive = true;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Transition a step from Pending to Running.
|
|
545
|
+
*/
|
|
546
|
+
startStep(stepId) {
|
|
547
|
+
const step = this.requireStep(stepId);
|
|
548
|
+
this.transitionStep(step, "running");
|
|
549
|
+
step.startTime = Date.now();
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Transition a step from Running to Completed.
|
|
553
|
+
*/
|
|
554
|
+
completeStep(stepId) {
|
|
555
|
+
const step = this.requireStep(stepId);
|
|
556
|
+
this.transitionStep(step, "completed");
|
|
557
|
+
step.endTime = Date.now();
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Transition a step from Running to Failed with error details.
|
|
561
|
+
*/
|
|
562
|
+
failStep(stepId, error) {
|
|
563
|
+
const step = this.requireStep(stepId);
|
|
564
|
+
this.transitionStep(step, "failed");
|
|
565
|
+
step.endTime = Date.now();
|
|
566
|
+
step.error = error;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Transition a step from Pending to Skipped.
|
|
570
|
+
*/
|
|
571
|
+
skipStep(stepId, _reason) {
|
|
572
|
+
const step = this.requireStep(stepId);
|
|
573
|
+
this.transitionStep(step, "skipped");
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Mark the tracker as finished. Throws if any steps are still Running.
|
|
577
|
+
*/
|
|
578
|
+
finish() {
|
|
579
|
+
const runningSteps = this.flattenSteps().filter((s) => s.status === "running");
|
|
580
|
+
if (runningSteps.length > 0) {
|
|
581
|
+
const ids = runningSteps.map((s) => s.id).join(", ");
|
|
582
|
+
throw new Error(`Cannot finish tracker while steps are running: ${ids}`);
|
|
583
|
+
}
|
|
584
|
+
this._endTime = Date.now();
|
|
585
|
+
this._isActive = false;
|
|
586
|
+
this.stopSpinner();
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Get the elapsed time since tracking started.
|
|
590
|
+
*/
|
|
591
|
+
getElapsedTime() {
|
|
592
|
+
if (this._startTime === null) {
|
|
593
|
+
return { milliseconds: 0, formatted: "0ms" };
|
|
594
|
+
}
|
|
595
|
+
const end = this._endTime ?? Date.now();
|
|
596
|
+
return formatDuration(end - this._startTime);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Get the completion ratio across all steps (including children).
|
|
600
|
+
*/
|
|
601
|
+
getCompletionRatio() {
|
|
602
|
+
const allSteps = this.flattenSteps();
|
|
603
|
+
const completed = allSteps.filter(
|
|
604
|
+
(s) => s.status === "completed" || s.status === "skipped"
|
|
605
|
+
).length;
|
|
606
|
+
const total = allSteps.length;
|
|
607
|
+
const percentage = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
608
|
+
return { completed, total, percentage };
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Render the current progress state as terminal output lines.
|
|
612
|
+
*
|
|
613
|
+
* @param theme - ThemeEngine for colors and symbols
|
|
614
|
+
* @returns Array of output lines
|
|
615
|
+
*/
|
|
616
|
+
render(theme) {
|
|
617
|
+
const lines = [];
|
|
618
|
+
const width = theme.terminalWidth;
|
|
619
|
+
for (const step of this._steps) {
|
|
620
|
+
lines.push(this.renderStep(step, 0, theme, width));
|
|
621
|
+
for (const child of step.children) {
|
|
622
|
+
lines.push(this.renderStep(child, 1, theme, width));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return lines;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Start the ora spinner for the currently running step.
|
|
629
|
+
* Used by the render service for animated Running indicators.
|
|
630
|
+
*/
|
|
631
|
+
startSpinner(text2) {
|
|
632
|
+
if (this._spinner) {
|
|
633
|
+
this._spinner.stop();
|
|
634
|
+
}
|
|
635
|
+
const runningStep = this.flattenSteps().find((s) => s.status === "running");
|
|
636
|
+
if (!runningStep) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
this._spinner = ora({
|
|
640
|
+
text: text2 ?? runningStep.label,
|
|
641
|
+
spinner: "dots"
|
|
642
|
+
});
|
|
643
|
+
this._spinner.start();
|
|
644
|
+
return this._spinner;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Stop the ora spinner if one is running.
|
|
648
|
+
*/
|
|
649
|
+
stopSpinner() {
|
|
650
|
+
if (this._spinner) {
|
|
651
|
+
this._spinner.stop();
|
|
652
|
+
this._spinner = null;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// -------------------------------------------------------------------------
|
|
656
|
+
// Private Helpers
|
|
657
|
+
// -------------------------------------------------------------------------
|
|
658
|
+
/**
|
|
659
|
+
* Find a step by ID across all levels of the hierarchy.
|
|
660
|
+
*/
|
|
661
|
+
findStep(stepId) {
|
|
662
|
+
for (const step of this._steps) {
|
|
663
|
+
if (step.id === stepId) return step;
|
|
664
|
+
for (const child of step.children) {
|
|
665
|
+
if (child.id === stepId) return child;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Find a step by ID, throwing if not found.
|
|
672
|
+
*/
|
|
673
|
+
requireStep(stepId) {
|
|
674
|
+
const step = this.findStep(stepId);
|
|
675
|
+
if (!step) {
|
|
676
|
+
throw new Error(`Step not found: "${stepId}".`);
|
|
677
|
+
}
|
|
678
|
+
return step;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Enforce valid status transitions.
|
|
682
|
+
*/
|
|
683
|
+
transitionStep(step, to) {
|
|
684
|
+
if (!isValidTransition(step.status, to)) {
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Invalid transition for step "${step.id}": ${step.status} -> ${to}. Allowed: [${VALID_TRANSITIONS[step.status].join(", ")}].`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
step.status = to;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Flatten all steps (parents + children) into a single array.
|
|
693
|
+
*/
|
|
694
|
+
flattenSteps() {
|
|
695
|
+
const result = [];
|
|
696
|
+
for (const step of this._steps) {
|
|
697
|
+
result.push(step);
|
|
698
|
+
result.push(...step.children);
|
|
699
|
+
}
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Render a single step line with indentation, symbol, label, and elapsed time.
|
|
704
|
+
*/
|
|
705
|
+
renderStep(step, depth, theme, terminalWidth) {
|
|
706
|
+
const tier = theme.tier;
|
|
707
|
+
const indentSize = tier === "plain" ? 4 : 2;
|
|
708
|
+
const indent = " ".repeat(2 + depth * indentSize);
|
|
709
|
+
const symbol = theme.statusSymbol(step.status);
|
|
710
|
+
let elapsed = "";
|
|
711
|
+
if (step.startTime !== null) {
|
|
712
|
+
const end = step.endTime ?? Date.now();
|
|
713
|
+
const duration = formatDuration(end - step.startTime);
|
|
714
|
+
elapsed = duration.formatted;
|
|
715
|
+
}
|
|
716
|
+
const labelPart = `${indent}${symbol} ${step.label}`;
|
|
717
|
+
if (!elapsed) {
|
|
718
|
+
return labelPart;
|
|
719
|
+
}
|
|
720
|
+
const elapsedDisplay = theme.dim(elapsed);
|
|
721
|
+
const rawLabelLength = stripAnsi(labelPart).length;
|
|
722
|
+
const rawElapsedLength = elapsed.length;
|
|
723
|
+
const padding = Math.max(4, terminalWidth - rawLabelLength - rawElapsedLength - 2);
|
|
724
|
+
return `${labelPart}${" ".repeat(padding)}${elapsedDisplay}`;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
function stripAnsi(text2) {
|
|
728
|
+
return text2.replace(/\x1b\[[0-9;]*m/g, "");
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/ui/prompts.ts
|
|
732
|
+
import * as clack from "@clack/prompts";
|
|
733
|
+
async function promptDirectorySelection(options, theme) {
|
|
734
|
+
const selectOptions = options.map((opt) => ({
|
|
735
|
+
value: opt.path,
|
|
736
|
+
label: opt.path,
|
|
737
|
+
hint: opt.isDefault ? "(default)" : opt.exists ? "(exists)" : "(will be created)"
|
|
738
|
+
}));
|
|
739
|
+
selectOptions.push({
|
|
740
|
+
value: "__custom__",
|
|
741
|
+
label: "Custom path...",
|
|
742
|
+
hint: "Enter a custom directory path"
|
|
743
|
+
});
|
|
744
|
+
const selected = await clack.select({
|
|
745
|
+
message: "Select installation directory:",
|
|
746
|
+
options: selectOptions,
|
|
747
|
+
initialValue: options.find((o) => o.isDefault)?.path ?? options[0]?.path
|
|
748
|
+
});
|
|
749
|
+
if (clack.isCancel(selected)) {
|
|
750
|
+
throw new UserCancellationError("Directory selection cancelled.");
|
|
751
|
+
}
|
|
752
|
+
if (selected === "__custom__") {
|
|
753
|
+
const customPath = await clack.text({
|
|
754
|
+
message: "Enter installation directory path:",
|
|
755
|
+
placeholder: process.cwd(),
|
|
756
|
+
validate: (value) => {
|
|
757
|
+
if (!value || value.trim().length === 0) {
|
|
758
|
+
return "Path cannot be empty.";
|
|
759
|
+
}
|
|
760
|
+
const isAbsolute = /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("/");
|
|
761
|
+
if (!isAbsolute) {
|
|
762
|
+
return "Path must be absolute (e.g., C:\\dev\\project or /home/user/project).";
|
|
763
|
+
}
|
|
764
|
+
return void 0;
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
if (clack.isCancel(customPath)) {
|
|
768
|
+
throw new UserCancellationError("Custom path entry cancelled.");
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
path: customPath,
|
|
772
|
+
isCustom: true
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
path: selected,
|
|
777
|
+
isCustom: false
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
async function promptConfirmation(message, defaultValue = true) {
|
|
781
|
+
const result = await clack.confirm({
|
|
782
|
+
message,
|
|
783
|
+
initialValue: defaultValue
|
|
784
|
+
});
|
|
785
|
+
if (clack.isCancel(result)) {
|
|
786
|
+
throw new UserCancellationError("Confirmation cancelled.");
|
|
787
|
+
}
|
|
788
|
+
return result;
|
|
789
|
+
}
|
|
790
|
+
async function promptOverwriteExisting(targetPath) {
|
|
791
|
+
const result = await clack.confirm({
|
|
792
|
+
message: `Directory "${targetPath}" already contains AI-DLC files. Overwrite?`,
|
|
793
|
+
initialValue: false
|
|
794
|
+
});
|
|
795
|
+
if (clack.isCancel(result)) {
|
|
796
|
+
throw new UserCancellationError("Overwrite confirmation cancelled.");
|
|
797
|
+
}
|
|
798
|
+
return result;
|
|
799
|
+
}
|
|
800
|
+
async function promptRetry() {
|
|
801
|
+
const result = await clack.confirm({
|
|
802
|
+
message: "Would you like to retry?",
|
|
803
|
+
initialValue: true
|
|
804
|
+
});
|
|
805
|
+
if (clack.isCancel(result)) {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
810
|
+
function promptIntro(title) {
|
|
811
|
+
clack.intro(title);
|
|
812
|
+
}
|
|
813
|
+
function promptOutro(message) {
|
|
814
|
+
clack.outro(message);
|
|
815
|
+
}
|
|
816
|
+
var UserCancellationError = class extends Error {
|
|
817
|
+
code = "USER_CANCELLED";
|
|
818
|
+
constructor(message = "Operation cancelled by user.") {
|
|
819
|
+
super(message);
|
|
820
|
+
this.name = "UserCancellationError";
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
// src/ui/error-display.ts
|
|
825
|
+
function renderError(content, theme) {
|
|
826
|
+
return renderPanel(content, "error", theme);
|
|
827
|
+
}
|
|
828
|
+
function renderPanel(content, panelType, theme) {
|
|
829
|
+
const symbols = theme.symbolSet;
|
|
830
|
+
const width = Math.min(theme.terminalWidth - 4, 75);
|
|
831
|
+
const innerWidth = width - 4;
|
|
832
|
+
const tier = theme.tier;
|
|
833
|
+
const colorFn = (text2) => {
|
|
834
|
+
return theme.colorize(text2, panelType === "error" ? "error" : "warning");
|
|
835
|
+
};
|
|
836
|
+
const lines = [];
|
|
837
|
+
const topBorder = buildHorizontalBorder(
|
|
838
|
+
symbols.boxTopLeft,
|
|
839
|
+
symbols.boxHorizontal,
|
|
840
|
+
symbols.boxTopRight,
|
|
841
|
+
width
|
|
842
|
+
);
|
|
843
|
+
lines.push(` ${colorFn(topBorder)}`);
|
|
844
|
+
const statusSymbol = panelType === "error" ? theme.statusSymbol("failed") : tier === "plain" ? "[!]" : "\u26A0";
|
|
845
|
+
const titleText = `${statusSymbol} ${content.title}`;
|
|
846
|
+
lines.push(buildContentLine(titleText, symbols.boxVertical, innerWidth, colorFn, theme));
|
|
847
|
+
const divider = buildHorizontalBorder(
|
|
848
|
+
symbols.boxDividerLeft,
|
|
849
|
+
symbols.boxHorizontal,
|
|
850
|
+
symbols.boxDividerRight,
|
|
851
|
+
width
|
|
852
|
+
);
|
|
853
|
+
lines.push(` ${colorFn(divider)}`);
|
|
854
|
+
lines.push(buildEmptyLine(symbols.boxVertical, innerWidth, colorFn));
|
|
855
|
+
const messageLines = wordWrap(content.message, innerWidth - 2);
|
|
856
|
+
for (const msgLine of messageLines) {
|
|
857
|
+
lines.push(buildContentLine(` ${msgLine}`, symbols.boxVertical, innerWidth, colorFn, theme));
|
|
858
|
+
}
|
|
859
|
+
if ("cause" in content && content.cause) {
|
|
860
|
+
lines.push(buildEmptyLine(symbols.boxVertical, innerWidth, colorFn));
|
|
861
|
+
const causePrefix = theme.bold("Cause:");
|
|
862
|
+
const causeLines = wordWrap(`${content.cause}`, innerWidth - 9);
|
|
863
|
+
lines.push(
|
|
864
|
+
buildContentLine(` ${causePrefix} ${causeLines[0] ?? ""}`, symbols.boxVertical, innerWidth, colorFn, theme)
|
|
865
|
+
);
|
|
866
|
+
for (let i = 1; i < causeLines.length; i++) {
|
|
867
|
+
lines.push(
|
|
868
|
+
buildContentLine(` ${causeLines[i]}`, symbols.boxVertical, innerWidth, colorFn, theme)
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (content.suggestedActions.length > 0) {
|
|
873
|
+
lines.push(buildEmptyLine(symbols.boxVertical, innerWidth, colorFn));
|
|
874
|
+
lines.push(
|
|
875
|
+
buildContentLine(" Suggested actions:", symbols.boxVertical, innerWidth, colorFn, theme)
|
|
876
|
+
);
|
|
877
|
+
lines.push(buildEmptyLine(symbols.boxVertical, innerWidth, colorFn));
|
|
878
|
+
for (const action of content.suggestedActions) {
|
|
879
|
+
lines.push(...renderSuggestedAction(action, symbols.boxVertical, innerWidth, colorFn, theme));
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
lines.push(buildEmptyLine(symbols.boxVertical, innerWidth, colorFn));
|
|
883
|
+
const bottomBorder = buildHorizontalBorder(
|
|
884
|
+
symbols.boxBottomLeft,
|
|
885
|
+
symbols.boxHorizontal,
|
|
886
|
+
symbols.boxBottomRight,
|
|
887
|
+
width
|
|
888
|
+
);
|
|
889
|
+
lines.push(` ${colorFn(bottomBorder)}`);
|
|
890
|
+
return lines;
|
|
891
|
+
}
|
|
892
|
+
function buildHorizontalBorder(left, horizontal, right, totalWidth) {
|
|
893
|
+
const fill = horizontal.repeat(totalWidth - 2);
|
|
894
|
+
return `${left}${fill}${right}`;
|
|
895
|
+
}
|
|
896
|
+
function buildContentLine(content, vertical, innerWidth, colorFn, theme) {
|
|
897
|
+
const strippedLength = stripAnsi2(content).length;
|
|
898
|
+
const padding = Math.max(0, innerWidth - strippedLength);
|
|
899
|
+
return ` ${colorFn(vertical)} ${content}${" ".repeat(padding)}${colorFn(vertical)}`;
|
|
900
|
+
}
|
|
901
|
+
function buildEmptyLine(vertical, innerWidth, colorFn) {
|
|
902
|
+
return ` ${colorFn(vertical)}${" ".repeat(innerWidth + 2)}${colorFn(vertical)}`;
|
|
903
|
+
}
|
|
904
|
+
function renderSuggestedAction(action, vertical, innerWidth, colorFn, theme) {
|
|
905
|
+
const lines = [];
|
|
906
|
+
const numberPrefix = ` ${action.step}. `;
|
|
907
|
+
const instructionLines = wordWrap(action.instruction, innerWidth - numberPrefix.length - 2);
|
|
908
|
+
lines.push(
|
|
909
|
+
buildContentLine(
|
|
910
|
+
`${numberPrefix}${instructionLines[0] ?? ""}`,
|
|
911
|
+
vertical,
|
|
912
|
+
innerWidth,
|
|
913
|
+
colorFn,
|
|
914
|
+
theme
|
|
915
|
+
)
|
|
916
|
+
);
|
|
917
|
+
const continuationIndent = " ".repeat(numberPrefix.length);
|
|
918
|
+
for (let i = 1; i < instructionLines.length; i++) {
|
|
919
|
+
lines.push(
|
|
920
|
+
buildContentLine(
|
|
921
|
+
`${continuationIndent}${instructionLines[i]}`,
|
|
922
|
+
vertical,
|
|
923
|
+
innerWidth,
|
|
924
|
+
colorFn,
|
|
925
|
+
theme
|
|
926
|
+
)
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
if (action.command) {
|
|
930
|
+
const commandText = theme.colorize(action.command, "accent");
|
|
931
|
+
lines.push(
|
|
932
|
+
buildContentLine(
|
|
933
|
+
`${continuationIndent}${commandText}`,
|
|
934
|
+
vertical,
|
|
935
|
+
innerWidth,
|
|
936
|
+
colorFn,
|
|
937
|
+
theme
|
|
938
|
+
)
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
lines.push(buildEmptyLine(vertical, innerWidth, colorFn));
|
|
942
|
+
return lines;
|
|
943
|
+
}
|
|
944
|
+
function wordWrap(text2, maxWidth) {
|
|
945
|
+
if (maxWidth <= 0) return [text2];
|
|
946
|
+
const words = text2.split(" ");
|
|
947
|
+
const lines = [];
|
|
948
|
+
let currentLine = "";
|
|
949
|
+
for (const word of words) {
|
|
950
|
+
if (currentLine.length === 0) {
|
|
951
|
+
currentLine = word;
|
|
952
|
+
} else if (currentLine.length + 1 + word.length <= maxWidth) {
|
|
953
|
+
currentLine += ` ${word}`;
|
|
954
|
+
} else {
|
|
955
|
+
lines.push(currentLine);
|
|
956
|
+
currentLine = word;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (currentLine.length > 0) {
|
|
960
|
+
lines.push(currentLine);
|
|
961
|
+
}
|
|
962
|
+
return lines.length > 0 ? lines : [""];
|
|
963
|
+
}
|
|
964
|
+
function stripAnsi2(text2) {
|
|
965
|
+
return text2.replace(/\x1b\[[0-9;]*m/g, "");
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/ui/summary.ts
|
|
969
|
+
function renderSummary(summary, nextSteps, theme) {
|
|
970
|
+
const width = theme.terminalWidth;
|
|
971
|
+
const lines = [];
|
|
972
|
+
lines.push(...renderHeaderBox(summary, theme, width));
|
|
973
|
+
lines.push("");
|
|
974
|
+
const dirLabel = summary.success ? "Installed to:" : "Target directory:";
|
|
975
|
+
lines.push(` ${dirLabel} ${theme.colorize(summary.targetDirectory, "accent")}`);
|
|
976
|
+
lines.push("");
|
|
977
|
+
if (summary.success || summary.totalFilesCopied > 0) {
|
|
978
|
+
lines.push(...renderComponentCounts(summary.componentCounts, theme));
|
|
979
|
+
lines.push("");
|
|
980
|
+
}
|
|
981
|
+
lines.push(...renderFileStatistics(summary, theme));
|
|
982
|
+
lines.push("");
|
|
983
|
+
lines.push(` Total time: ${theme.bold(summary.totalDuration.formatted)}`);
|
|
984
|
+
lines.push("");
|
|
985
|
+
lines.push(renderDivider(theme, width));
|
|
986
|
+
lines.push("");
|
|
987
|
+
if (nextSteps.length > 0) {
|
|
988
|
+
lines.push(...renderNextSteps(nextSteps, theme));
|
|
989
|
+
lines.push("");
|
|
990
|
+
lines.push(renderDivider(theme, width));
|
|
991
|
+
}
|
|
992
|
+
return lines;
|
|
993
|
+
}
|
|
994
|
+
function renderHeaderBox(summary, theme, width) {
|
|
995
|
+
const symbols = theme.symbolSet;
|
|
996
|
+
const boxWidth = Math.min(width - 4, 72);
|
|
997
|
+
const innerWidth = boxWidth - 4;
|
|
998
|
+
let statusSymbol;
|
|
999
|
+
let title;
|
|
1000
|
+
let colorName;
|
|
1001
|
+
if (summary.success && summary.totalFilesFailed === 0) {
|
|
1002
|
+
statusSymbol = theme.statusSymbol("completed");
|
|
1003
|
+
title = "Installation Complete!";
|
|
1004
|
+
colorName = "success";
|
|
1005
|
+
} else if (summary.success && summary.totalFilesFailed > 0) {
|
|
1006
|
+
statusSymbol = theme.tier === "plain" ? "[!]" : "\u26A0";
|
|
1007
|
+
title = "Installation Completed with Warnings";
|
|
1008
|
+
colorName = "warning";
|
|
1009
|
+
} else {
|
|
1010
|
+
statusSymbol = theme.statusSymbol("failed");
|
|
1011
|
+
title = "Installation Failed";
|
|
1012
|
+
colorName = "error";
|
|
1013
|
+
}
|
|
1014
|
+
const colorFn = (text2) => theme.colorize(text2, colorName);
|
|
1015
|
+
const lines = [];
|
|
1016
|
+
const topBorder = `${symbols.doubleTopLeft}${symbols.doubleHorizontal.repeat(boxWidth - 2)}${symbols.doubleTopRight}`;
|
|
1017
|
+
lines.push(` ${colorFn(topBorder)}`);
|
|
1018
|
+
const emptyLine = `${symbols.doubleVertical}${" ".repeat(boxWidth - 2)}${symbols.doubleVertical}`;
|
|
1019
|
+
lines.push(` ${colorFn(emptyLine)}`);
|
|
1020
|
+
const titleContent = `${statusSymbol} ${theme.bold(colorFn(title))}`;
|
|
1021
|
+
const strippedTitleLength = stripAnsi3(titleContent).length;
|
|
1022
|
+
const leftPad = Math.max(0, Math.floor((innerWidth - strippedTitleLength) / 2));
|
|
1023
|
+
const rightPad = Math.max(0, innerWidth - strippedTitleLength - leftPad);
|
|
1024
|
+
const titleLine = `${symbols.doubleVertical} ${" ".repeat(leftPad)}${titleContent}${" ".repeat(rightPad)} ${symbols.doubleVertical}`;
|
|
1025
|
+
lines.push(` ${colorFn(symbols.doubleVertical)} ${" ".repeat(leftPad)}${titleContent}${" ".repeat(rightPad)} ${colorFn(symbols.doubleVertical)}`);
|
|
1026
|
+
lines.push(` ${colorFn(emptyLine)}`);
|
|
1027
|
+
const bottomBorder = `${symbols.doubleBottomLeft}${symbols.doubleHorizontal.repeat(boxWidth - 2)}${symbols.doubleBottomRight}`;
|
|
1028
|
+
lines.push(` ${colorFn(bottomBorder)}`);
|
|
1029
|
+
return lines;
|
|
1030
|
+
}
|
|
1031
|
+
function renderComponentCounts(counts, theme) {
|
|
1032
|
+
const bullet = theme.symbolSet.bullet;
|
|
1033
|
+
const lines = [];
|
|
1034
|
+
lines.push(` ${theme.bold(theme.colorize("Components installed:", "header"))}`);
|
|
1035
|
+
const items = [
|
|
1036
|
+
{ label: "agents", count: counts.agents },
|
|
1037
|
+
{ label: "skills", count: counts.skills },
|
|
1038
|
+
{ label: "commands", count: counts.commands },
|
|
1039
|
+
{ label: "TypeScript CLI files", count: counts.cliFiles },
|
|
1040
|
+
{ label: "configuration files", count: counts.other }
|
|
1041
|
+
];
|
|
1042
|
+
for (const item of items) {
|
|
1043
|
+
if (item.count > 0) {
|
|
1044
|
+
const countStr = theme.colorize(String(item.count), "accent");
|
|
1045
|
+
lines.push(` ${bullet} ${countStr} ${item.label}`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return lines;
|
|
1049
|
+
}
|
|
1050
|
+
function renderFileStatistics(summary, theme) {
|
|
1051
|
+
const lines = [];
|
|
1052
|
+
lines.push(` ${theme.bold(theme.colorize("File statistics:", "header"))}`);
|
|
1053
|
+
const copiedSymbol = theme.statusSymbol("completed");
|
|
1054
|
+
lines.push(` ${copiedSymbol} ${summary.totalFilesCopied} files copied successfully`);
|
|
1055
|
+
const skippedSymbol = theme.statusSymbol("skipped");
|
|
1056
|
+
if (summary.totalFilesSkipped > 0) {
|
|
1057
|
+
lines.push(
|
|
1058
|
+
` ${skippedSymbol} ${summary.totalFilesSkipped} files skipped (already up-to-date)`
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
const failedSymbol = theme.statusSymbol("failed");
|
|
1062
|
+
const failedText = summary.totalFilesFailed > 0 ? `${summary.totalFilesFailed} files failed` : "0 files failed";
|
|
1063
|
+
lines.push(` ${failedSymbol} ${failedText}`);
|
|
1064
|
+
return lines;
|
|
1065
|
+
}
|
|
1066
|
+
function renderNextSteps(steps, theme) {
|
|
1067
|
+
const lines = [];
|
|
1068
|
+
lines.push(` ${theme.bold(theme.colorize("Next steps:", "header"))}`);
|
|
1069
|
+
lines.push("");
|
|
1070
|
+
for (const step of steps) {
|
|
1071
|
+
lines.push(` ${step.order}. ${step.title}:`);
|
|
1072
|
+
if (step.command) {
|
|
1073
|
+
lines.push(` ${theme.colorize(step.command, "accent")}`);
|
|
1074
|
+
} else {
|
|
1075
|
+
lines.push(` ${step.instruction}`);
|
|
1076
|
+
}
|
|
1077
|
+
lines.push("");
|
|
1078
|
+
}
|
|
1079
|
+
return lines;
|
|
1080
|
+
}
|
|
1081
|
+
function renderDivider(theme, width) {
|
|
1082
|
+
const dividerWidth = Math.min(width - 4, 72);
|
|
1083
|
+
const char = theme.symbolSet.boxHorizontal;
|
|
1084
|
+
return ` ${theme.dim(char.repeat(dividerWidth))}`;
|
|
1085
|
+
}
|
|
1086
|
+
function stripAnsi3(text2) {
|
|
1087
|
+
return text2.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1088
|
+
}
|
|
1089
|
+
function getDefaultNextSteps(success) {
|
|
1090
|
+
if (success) {
|
|
1091
|
+
return [
|
|
1092
|
+
{
|
|
1093
|
+
order: 1,
|
|
1094
|
+
title: "Verify the installation",
|
|
1095
|
+
instruction: "Run the status command to verify",
|
|
1096
|
+
command: "ai-dlc status"
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
order: 2,
|
|
1100
|
+
title: "Start your first AI-DLC project",
|
|
1101
|
+
instruction: "Create an intent to begin",
|
|
1102
|
+
command: 'ai-dlc start-intent "Your project idea"'
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
order: 3,
|
|
1106
|
+
title: "View available commands",
|
|
1107
|
+
instruction: "See all available commands",
|
|
1108
|
+
command: "ai-dlc help"
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
order: 4,
|
|
1112
|
+
title: "Read the documentation",
|
|
1113
|
+
instruction: "Visit the documentation site",
|
|
1114
|
+
command: "https://github.com/SixSevenAI/ai-dlc/docs"
|
|
1115
|
+
}
|
|
1116
|
+
];
|
|
1117
|
+
}
|
|
1118
|
+
return [
|
|
1119
|
+
{
|
|
1120
|
+
order: 1,
|
|
1121
|
+
title: "Review the error messages above",
|
|
1122
|
+
instruction: "Identify the root cause of the failure",
|
|
1123
|
+
command: null
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
order: 2,
|
|
1127
|
+
title: "Fix the underlying issue",
|
|
1128
|
+
instruction: "Address permissions, disk space, or network issues",
|
|
1129
|
+
command: null
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
order: 3,
|
|
1133
|
+
title: "Run the installer again",
|
|
1134
|
+
instruction: "Retry the installation",
|
|
1135
|
+
command: "npx @sixsevenai/ai-dlc install"
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
order: 4,
|
|
1139
|
+
title: "Get help",
|
|
1140
|
+
instruction: "Report an issue on GitHub",
|
|
1141
|
+
command: "https://github.com/SixSevenAI/ai-dlc/issues"
|
|
1142
|
+
}
|
|
1143
|
+
];
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/orchestrator/error-handler.ts
|
|
1147
|
+
var MAX_RETRIES = 2;
|
|
1148
|
+
var errorCounter = 0;
|
|
1149
|
+
var ErrorHandler = class {
|
|
1150
|
+
errors = [];
|
|
1151
|
+
retryCounts = /* @__PURE__ */ new Map();
|
|
1152
|
+
/**
|
|
1153
|
+
* Classify an error and determine the recovery action.
|
|
1154
|
+
*
|
|
1155
|
+
* @param error - Raw error from any subsystem
|
|
1156
|
+
* @param stepId - The workflow step where the error occurred
|
|
1157
|
+
* @returns Recovery action to take
|
|
1158
|
+
*/
|
|
1159
|
+
handleError(error, stepId) {
|
|
1160
|
+
const category = classifyCategory(error);
|
|
1161
|
+
const severity = classifySeverity(category);
|
|
1162
|
+
const recoveryStrategy = selectRecoveryStrategy(category, severity, this.canRetry(stepId));
|
|
1163
|
+
const classified = {
|
|
1164
|
+
errorId: `err-${++errorCounter}`,
|
|
1165
|
+
originalError: error,
|
|
1166
|
+
category,
|
|
1167
|
+
severity,
|
|
1168
|
+
sourceStep: stepId,
|
|
1169
|
+
recoveryStrategy,
|
|
1170
|
+
retryCount: this.getRetryCount(stepId),
|
|
1171
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1172
|
+
userMessage: buildUserMessage(category, error),
|
|
1173
|
+
technicalDetail: error.stack ?? error.message
|
|
1174
|
+
};
|
|
1175
|
+
this.errors.push(classified);
|
|
1176
|
+
return {
|
|
1177
|
+
strategy: recoveryStrategy,
|
|
1178
|
+
stepId,
|
|
1179
|
+
userGuidance: buildUserGuidance(category, error),
|
|
1180
|
+
platformGuidance: buildPlatformGuidance(category)
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Check whether the specified step can be retried.
|
|
1185
|
+
*/
|
|
1186
|
+
canRetry(stepId) {
|
|
1187
|
+
return this.getRetryCount(stepId) < MAX_RETRIES;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Record a retry attempt for a step.
|
|
1191
|
+
*/
|
|
1192
|
+
recordRetry(stepId) {
|
|
1193
|
+
const current = this.retryCounts.get(stepId) ?? 0;
|
|
1194
|
+
this.retryCounts.set(stepId, current + 1);
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Get the current retry count for a step.
|
|
1198
|
+
*/
|
|
1199
|
+
getRetryCount(stepId) {
|
|
1200
|
+
return this.retryCounts.get(stepId) ?? 0;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Check if the workflow should abort due to accumulated errors.
|
|
1204
|
+
*/
|
|
1205
|
+
shouldAbort() {
|
|
1206
|
+
return this.errors.some((e) => e.severity === "Fatal");
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Get a summary of all accumulated errors.
|
|
1210
|
+
*/
|
|
1211
|
+
getErrorSummary() {
|
|
1212
|
+
const fatalCount = this.errors.filter((e) => e.severity === "Fatal").length;
|
|
1213
|
+
const recoverableCount = this.errors.filter((e) => e.severity === "Recoverable").length;
|
|
1214
|
+
const warningCount = this.errors.filter((e) => e.severity === "Warning").length;
|
|
1215
|
+
return {
|
|
1216
|
+
totalErrors: this.errors.length,
|
|
1217
|
+
fatalCount,
|
|
1218
|
+
recoverableCount,
|
|
1219
|
+
warningCount,
|
|
1220
|
+
messages: this.errors.map((e) => e.userMessage)
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Determine the appropriate exit code based on workflow state and errors.
|
|
1225
|
+
*/
|
|
1226
|
+
getExitCode(workflow) {
|
|
1227
|
+
if (workflow.status === "cancelled") {
|
|
1228
|
+
return EXIT_CANCELLED;
|
|
1229
|
+
}
|
|
1230
|
+
if (workflow.status === "failed") {
|
|
1231
|
+
return EXIT_ERROR;
|
|
1232
|
+
}
|
|
1233
|
+
if (this.errors.some((e) => e.severity === "Fatal")) {
|
|
1234
|
+
return EXIT_ERROR;
|
|
1235
|
+
}
|
|
1236
|
+
return EXIT_SUCCESS;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Get all classified errors for inspection.
|
|
1240
|
+
*/
|
|
1241
|
+
getErrors() {
|
|
1242
|
+
return this.errors;
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
function classifyCategory(error) {
|
|
1246
|
+
if (error instanceof UserCancellationError || error.name === "UserCancellationError") {
|
|
1247
|
+
return "Cancellation";
|
|
1248
|
+
}
|
|
1249
|
+
const message = error.message.toLowerCase();
|
|
1250
|
+
const code = error.code;
|
|
1251
|
+
if (code === "EACCES" || code === "EPERM" || message.includes("permission denied")) {
|
|
1252
|
+
return "Permission";
|
|
1253
|
+
}
|
|
1254
|
+
if (code === "ENOSPC" || message.includes("disk full") || message.includes("no space")) {
|
|
1255
|
+
return "DiskSpace";
|
|
1256
|
+
}
|
|
1257
|
+
if (code === "ENOENT" || code === "ENOTDIR" || message.includes("invalid path") || message.includes("not a directory") || message.includes("does not exist")) {
|
|
1258
|
+
return "InvalidPath";
|
|
1259
|
+
}
|
|
1260
|
+
if (message.includes("node.js") || message.includes("node version") || message.includes("npm") || message.includes("minimum required")) {
|
|
1261
|
+
return "RuntimeVersion";
|
|
1262
|
+
}
|
|
1263
|
+
if (message.includes("validation") || message.includes("missing files") || message.includes("corrupt")) {
|
|
1264
|
+
return "ValidationFailure";
|
|
1265
|
+
}
|
|
1266
|
+
if (message.includes("integrity") || message.includes("hash") || message.includes("source directory")) {
|
|
1267
|
+
return "IntegrityError";
|
|
1268
|
+
}
|
|
1269
|
+
if (message.includes("conflict") || message.includes("overwrite")) {
|
|
1270
|
+
return "ConflictResolution";
|
|
1271
|
+
}
|
|
1272
|
+
return "InternalError";
|
|
1273
|
+
}
|
|
1274
|
+
function classifySeverity(category) {
|
|
1275
|
+
switch (category) {
|
|
1276
|
+
case "Cancellation":
|
|
1277
|
+
return "Fatal";
|
|
1278
|
+
case "DiskSpace":
|
|
1279
|
+
return "Fatal";
|
|
1280
|
+
case "IntegrityError":
|
|
1281
|
+
return "Fatal";
|
|
1282
|
+
case "RuntimeVersion":
|
|
1283
|
+
return "Fatal";
|
|
1284
|
+
case "Permission":
|
|
1285
|
+
return "Recoverable";
|
|
1286
|
+
case "InvalidPath":
|
|
1287
|
+
return "Recoverable";
|
|
1288
|
+
case "ConflictResolution":
|
|
1289
|
+
return "Recoverable";
|
|
1290
|
+
case "ValidationFailure":
|
|
1291
|
+
return "Fatal";
|
|
1292
|
+
case "InternalError":
|
|
1293
|
+
return "Fatal";
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function selectRecoveryStrategy(category, severity, canRetry) {
|
|
1297
|
+
if (category === "Cancellation") {
|
|
1298
|
+
return "AbortImmediately";
|
|
1299
|
+
}
|
|
1300
|
+
if (severity === "Fatal") {
|
|
1301
|
+
return "RollbackAndAbort";
|
|
1302
|
+
}
|
|
1303
|
+
if (severity === "Recoverable" && canRetry) {
|
|
1304
|
+
return "PromptUser";
|
|
1305
|
+
}
|
|
1306
|
+
if (severity === "Recoverable") {
|
|
1307
|
+
return "RollbackAndAbort";
|
|
1308
|
+
}
|
|
1309
|
+
return "SkipAndContinue";
|
|
1310
|
+
}
|
|
1311
|
+
function buildUserMessage(category, error) {
|
|
1312
|
+
switch (category) {
|
|
1313
|
+
case "Permission":
|
|
1314
|
+
return `Permission denied: ${error.message}`;
|
|
1315
|
+
case "DiskSpace":
|
|
1316
|
+
return `Insufficient disk space: ${error.message}`;
|
|
1317
|
+
case "InvalidPath":
|
|
1318
|
+
return `Invalid path: ${error.message}`;
|
|
1319
|
+
case "RuntimeVersion":
|
|
1320
|
+
return `Runtime requirement not met: ${error.message}`;
|
|
1321
|
+
case "ValidationFailure":
|
|
1322
|
+
return `Installation validation failed: ${error.message}`;
|
|
1323
|
+
case "IntegrityError":
|
|
1324
|
+
return `Source integrity error: ${error.message}`;
|
|
1325
|
+
case "ConflictResolution":
|
|
1326
|
+
return `Conflict resolution failed: ${error.message}`;
|
|
1327
|
+
case "Cancellation":
|
|
1328
|
+
return "Installation cancelled by user.";
|
|
1329
|
+
case "InternalError":
|
|
1330
|
+
return `Unexpected error: ${error.message}`;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function buildUserGuidance(category, error) {
|
|
1334
|
+
switch (category) {
|
|
1335
|
+
case "Permission":
|
|
1336
|
+
return "Run the installer with elevated permissions or choose a different target directory.";
|
|
1337
|
+
case "DiskSpace":
|
|
1338
|
+
return "Free up disk space and try again.";
|
|
1339
|
+
case "InvalidPath":
|
|
1340
|
+
return "Check that the target directory path is valid and the parent directory exists.";
|
|
1341
|
+
case "RuntimeVersion":
|
|
1342
|
+
return "Upgrade Node.js to version 18 or later. Visit https://nodejs.org/";
|
|
1343
|
+
case "ValidationFailure":
|
|
1344
|
+
return "Try running the installer again. If the problem persists, check file permissions.";
|
|
1345
|
+
case "IntegrityError":
|
|
1346
|
+
return "The AI-DLC library source may be corrupted. Re-clone the repository or re-install the package.";
|
|
1347
|
+
case "ConflictResolution":
|
|
1348
|
+
return "Use --force to overwrite existing files, or manually resolve conflicts.";
|
|
1349
|
+
case "Cancellation":
|
|
1350
|
+
return "Installation was cancelled. No changes were made.";
|
|
1351
|
+
case "InternalError":
|
|
1352
|
+
return "This is an unexpected error. Please report it with the --verbose output.";
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
function buildPlatformGuidance(category) {
|
|
1356
|
+
switch (category) {
|
|
1357
|
+
case "Permission":
|
|
1358
|
+
return process.platform === "win32" ? 'Right-click your terminal and select "Run as Administrator".' : "Prefix the command with sudo: sudo sixsevenai install";
|
|
1359
|
+
default:
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// src/platform/types.ts
|
|
1365
|
+
function createNormalizedPath(value) {
|
|
1366
|
+
return value;
|
|
1367
|
+
}
|
|
1368
|
+
var PlatformError = class extends Error {
|
|
1369
|
+
code;
|
|
1370
|
+
component;
|
|
1371
|
+
context;
|
|
1372
|
+
constructor(code, component, message, context) {
|
|
1373
|
+
super(message);
|
|
1374
|
+
this.name = "PlatformError";
|
|
1375
|
+
this.code = code;
|
|
1376
|
+
this.component = component;
|
|
1377
|
+
this.context = context;
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
function parseSemVer(versionString) {
|
|
1381
|
+
const cleaned = versionString.trim().replace(/^v/i, "");
|
|
1382
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(cleaned);
|
|
1383
|
+
if (!match) {
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
return {
|
|
1387
|
+
major: parseInt(match[1], 10),
|
|
1388
|
+
minor: parseInt(match[2], 10),
|
|
1389
|
+
patch: parseInt(match[3], 10),
|
|
1390
|
+
raw: versionString.trim()
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function semVerSatisfiesMinimum(version2, minimum) {
|
|
1394
|
+
if (version2.major !== minimum.major) {
|
|
1395
|
+
return version2.major > minimum.major;
|
|
1396
|
+
}
|
|
1397
|
+
if (version2.minor !== minimum.minor) {
|
|
1398
|
+
return version2.minor > minimum.minor;
|
|
1399
|
+
}
|
|
1400
|
+
return version2.patch >= minimum.patch;
|
|
1401
|
+
}
|
|
1402
|
+
function semVer(major, minor, patch) {
|
|
1403
|
+
return {
|
|
1404
|
+
major,
|
|
1405
|
+
minor,
|
|
1406
|
+
patch,
|
|
1407
|
+
raw: `${major}.${minor}.${patch}`
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// src/platform/detector.ts
|
|
1412
|
+
import * as os from "os";
|
|
1413
|
+
function resolveOsFamily(platform) {
|
|
1414
|
+
switch (platform) {
|
|
1415
|
+
case "win32":
|
|
1416
|
+
return "windows";
|
|
1417
|
+
case "darwin":
|
|
1418
|
+
return "macos";
|
|
1419
|
+
case "linux":
|
|
1420
|
+
return "linux";
|
|
1421
|
+
default:
|
|
1422
|
+
throw new PlatformError(
|
|
1423
|
+
"UNSUPPORTED_PLATFORM",
|
|
1424
|
+
"PlatformDetector",
|
|
1425
|
+
`Unsupported platform: "${platform}". Only Windows, macOS, and Linux are supported.`,
|
|
1426
|
+
{ platform }
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function resolveArchitecture(arch) {
|
|
1431
|
+
const archMap = {
|
|
1432
|
+
x64: "x64",
|
|
1433
|
+
arm64: "arm64",
|
|
1434
|
+
ia32: "x86",
|
|
1435
|
+
x86: "x86"
|
|
1436
|
+
};
|
|
1437
|
+
const name = archMap[arch];
|
|
1438
|
+
if (name) {
|
|
1439
|
+
return { name };
|
|
1440
|
+
}
|
|
1441
|
+
return { name: "x64" };
|
|
1442
|
+
}
|
|
1443
|
+
function resolveReleaseName(family, version2) {
|
|
1444
|
+
switch (family) {
|
|
1445
|
+
case "windows": {
|
|
1446
|
+
try {
|
|
1447
|
+
const osVersion = os.version();
|
|
1448
|
+
if (osVersion) {
|
|
1449
|
+
return osVersion;
|
|
1450
|
+
}
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
1453
|
+
return `Windows NT ${version2}`;
|
|
1454
|
+
}
|
|
1455
|
+
case "macos": {
|
|
1456
|
+
const majorMinor = version2.split(".").slice(0, 2).join(".");
|
|
1457
|
+
const macVersionNames = {
|
|
1458
|
+
"24": "Sequoia",
|
|
1459
|
+
"23": "Sonoma",
|
|
1460
|
+
"22": "Ventura",
|
|
1461
|
+
"21": "Monterey",
|
|
1462
|
+
"20": "Big Sur",
|
|
1463
|
+
"19": "Catalina"
|
|
1464
|
+
};
|
|
1465
|
+
const major = version2.split(".")[0];
|
|
1466
|
+
const name = macVersionNames[major];
|
|
1467
|
+
return name ? `macOS ${name} ${majorMinor}` : `macOS ${majorMinor}`;
|
|
1468
|
+
}
|
|
1469
|
+
case "linux": {
|
|
1470
|
+
try {
|
|
1471
|
+
const osVersion = os.version();
|
|
1472
|
+
if (osVersion) {
|
|
1473
|
+
return osVersion;
|
|
1474
|
+
}
|
|
1475
|
+
} catch {
|
|
1476
|
+
}
|
|
1477
|
+
return `Linux ${version2}`;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
function resolveHomeDirectory(family) {
|
|
1482
|
+
if (family === "windows") {
|
|
1483
|
+
const userProfile = process.env["USERPROFILE"];
|
|
1484
|
+
if (userProfile) {
|
|
1485
|
+
return { path: userProfile, source: "USERPROFILE" };
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
const home = process.env["HOME"];
|
|
1489
|
+
if (home) {
|
|
1490
|
+
return { path: home, source: "HOME" };
|
|
1491
|
+
}
|
|
1492
|
+
return { path: os.homedir(), source: "os.homedir" };
|
|
1493
|
+
}
|
|
1494
|
+
function resolveTempDirectory() {
|
|
1495
|
+
return { path: os.tmpdir() };
|
|
1496
|
+
}
|
|
1497
|
+
var PlatformDetector = class {
|
|
1498
|
+
/**
|
|
1499
|
+
* Detect platform information and return an immutable PlatformSnapshot.
|
|
1500
|
+
*
|
|
1501
|
+
* @throws PlatformError with code UNSUPPORTED_PLATFORM if the OS is not
|
|
1502
|
+
* win32, darwin, or linux.
|
|
1503
|
+
*/
|
|
1504
|
+
detect() {
|
|
1505
|
+
const family = resolveOsFamily(process.platform);
|
|
1506
|
+
const version2 = os.release();
|
|
1507
|
+
const release3 = resolveReleaseName(family, version2);
|
|
1508
|
+
const architecture = resolveArchitecture(process.arch);
|
|
1509
|
+
const operatingSystem = {
|
|
1510
|
+
family,
|
|
1511
|
+
version: version2,
|
|
1512
|
+
release: release3,
|
|
1513
|
+
architecture
|
|
1514
|
+
};
|
|
1515
|
+
const homeDirectory = resolveHomeDirectory(family);
|
|
1516
|
+
const tempDirectory = resolveTempDirectory();
|
|
1517
|
+
return {
|
|
1518
|
+
os: operatingSystem,
|
|
1519
|
+
homeDirectory,
|
|
1520
|
+
tempDirectory,
|
|
1521
|
+
detectedAt: /* @__PURE__ */ new Date()
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
// src/platform/paths.ts
|
|
1527
|
+
function hasDriveLetter(path7) {
|
|
1528
|
+
return /^[a-zA-Z]:[/\\]/.test(path7);
|
|
1529
|
+
}
|
|
1530
|
+
function isUncPath(path7) {
|
|
1531
|
+
return /^[/\\]{2}[^/\\]/.test(path7);
|
|
1532
|
+
}
|
|
1533
|
+
function resolveSegments(segments) {
|
|
1534
|
+
const resolved = [];
|
|
1535
|
+
for (const segment of segments) {
|
|
1536
|
+
if (segment === "." || segment === "") {
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
if (segment === "..") {
|
|
1540
|
+
if (resolved.length > 0) {
|
|
1541
|
+
resolved.pop();
|
|
1542
|
+
}
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
resolved.push(segment);
|
|
1546
|
+
}
|
|
1547
|
+
return resolved;
|
|
1548
|
+
}
|
|
1549
|
+
var PathNormalizer = class {
|
|
1550
|
+
/**
|
|
1551
|
+
* Convert a platform-native path to normalized internal form (forward slashes).
|
|
1552
|
+
*
|
|
1553
|
+
* Rules:
|
|
1554
|
+
* 1. Replace all backslashes with forward slashes
|
|
1555
|
+
* 2. Uppercase Windows drive letters (c:/ -> C:/)
|
|
1556
|
+
* 3. Preserve UNC path prefix (\\server\share -> //server/share)
|
|
1557
|
+
* 4. Resolve . and .. segments
|
|
1558
|
+
* 5. Strip trailing separator (except root paths like / or C:/)
|
|
1559
|
+
* 6. Collapse double separators (except UNC prefix)
|
|
1560
|
+
*
|
|
1561
|
+
* @throws PlatformError with code INVALID_PATH for empty input
|
|
1562
|
+
*/
|
|
1563
|
+
normalize(rawPath, os3) {
|
|
1564
|
+
if (!rawPath || rawPath.trim().length === 0) {
|
|
1565
|
+
throw new PlatformError(
|
|
1566
|
+
"INVALID_PATH",
|
|
1567
|
+
"PathNormalizer",
|
|
1568
|
+
"Path cannot be empty",
|
|
1569
|
+
{ rawPath }
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
let path7 = rawPath;
|
|
1573
|
+
path7 = path7.replace(/\\/g, "/");
|
|
1574
|
+
let uncPrefix = "";
|
|
1575
|
+
if (isUncPath(rawPath)) {
|
|
1576
|
+
uncPrefix = "//";
|
|
1577
|
+
path7 = path7.slice(2);
|
|
1578
|
+
}
|
|
1579
|
+
if (os3.family === "windows" && hasDriveLetter(path7)) {
|
|
1580
|
+
path7 = path7[0].toUpperCase() + path7.slice(1);
|
|
1581
|
+
}
|
|
1582
|
+
const segments = path7.split("/");
|
|
1583
|
+
let root = "";
|
|
1584
|
+
if (uncPrefix) {
|
|
1585
|
+
const server = segments[0];
|
|
1586
|
+
const share = segments[1];
|
|
1587
|
+
root = `${uncPrefix}${server}/${share}`;
|
|
1588
|
+
segments.splice(0, 2);
|
|
1589
|
+
} else if (hasDriveLetter(path7)) {
|
|
1590
|
+
root = segments[0] + "/";
|
|
1591
|
+
segments.splice(0, 1);
|
|
1592
|
+
} else if (path7.startsWith("/")) {
|
|
1593
|
+
root = "/";
|
|
1594
|
+
segments.splice(0, 1);
|
|
1595
|
+
}
|
|
1596
|
+
const resolved = resolveSegments(segments);
|
|
1597
|
+
let normalized = root + resolved.join("/");
|
|
1598
|
+
if (!uncPrefix) {
|
|
1599
|
+
normalized = normalized.replace(/\/{2,}/g, "/");
|
|
1600
|
+
}
|
|
1601
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
1602
|
+
const isRootPath = normalized === "/" || /^[A-Z]:\/$/.test(normalized) || /^\/\/[^/]+\/[^/]+\/?$/.test(normalized);
|
|
1603
|
+
if (!isRootPath) {
|
|
1604
|
+
normalized = normalized.replace(/\/+$/, "");
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return createNormalizedPath(normalized);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Convert a normalized path back to platform-native form for OS API calls.
|
|
1611
|
+
* On Windows, replaces forward slashes with backslashes.
|
|
1612
|
+
* On Unix, returns as-is.
|
|
1613
|
+
*/
|
|
1614
|
+
toNative(path7, os3) {
|
|
1615
|
+
if (os3.family === "windows") {
|
|
1616
|
+
return path7.replace(/\//g, "\\");
|
|
1617
|
+
}
|
|
1618
|
+
return path7;
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Replace home directory tokens (~, $HOME, %USERPROFILE%) with the
|
|
1622
|
+
* actual home directory path.
|
|
1623
|
+
*
|
|
1624
|
+
* Token expansion happens before normalization:
|
|
1625
|
+
* - ~ or ~/ at the start of the path
|
|
1626
|
+
* - $HOME at the start of the path
|
|
1627
|
+
* - %USERPROFILE% at the start of the path
|
|
1628
|
+
*
|
|
1629
|
+
* @throws PlatformError with code INVALID_PATH for empty input
|
|
1630
|
+
*/
|
|
1631
|
+
resolveHome(path7, homeDir) {
|
|
1632
|
+
if (!path7 || path7.trim().length === 0) {
|
|
1633
|
+
throw new PlatformError(
|
|
1634
|
+
"INVALID_PATH",
|
|
1635
|
+
"PathNormalizer",
|
|
1636
|
+
"Path cannot be empty",
|
|
1637
|
+
{ path: path7 }
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
let resolved = path7;
|
|
1641
|
+
const normalizedHome = homeDir.path.replace(/\\/g, "/");
|
|
1642
|
+
if (resolved === "~") {
|
|
1643
|
+
resolved = normalizedHome;
|
|
1644
|
+
} else if (resolved.startsWith("~/") || resolved.startsWith("~\\")) {
|
|
1645
|
+
resolved = normalizedHome + "/" + resolved.slice(2);
|
|
1646
|
+
}
|
|
1647
|
+
if (resolved === "$HOME") {
|
|
1648
|
+
resolved = normalizedHome;
|
|
1649
|
+
} else if (resolved.startsWith("$HOME/") || resolved.startsWith("$HOME\\")) {
|
|
1650
|
+
resolved = normalizedHome + "/" + resolved.slice(6);
|
|
1651
|
+
}
|
|
1652
|
+
if (resolved === "%USERPROFILE%") {
|
|
1653
|
+
resolved = normalizedHome;
|
|
1654
|
+
} else if (resolved.startsWith("%USERPROFILE%/") || resolved.startsWith("%USERPROFILE%\\")) {
|
|
1655
|
+
resolved = normalizedHome + "/" + resolved.slice(14);
|
|
1656
|
+
}
|
|
1657
|
+
const family = normalizedHome.match(/^[A-Z]:/i) ? "windows" : "linux";
|
|
1658
|
+
const os3 = {
|
|
1659
|
+
family,
|
|
1660
|
+
version: "",
|
|
1661
|
+
release: "",
|
|
1662
|
+
architecture: { name: "x64" }
|
|
1663
|
+
};
|
|
1664
|
+
return this.normalize(resolved, os3);
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Convert a relative path to absolute using the given base path.
|
|
1668
|
+
* If the path is already absolute, returns it unchanged.
|
|
1669
|
+
*/
|
|
1670
|
+
toAbsolute(path7, basePath) {
|
|
1671
|
+
if (this.isAbsolute(path7)) {
|
|
1672
|
+
return path7;
|
|
1673
|
+
}
|
|
1674
|
+
return createNormalizedPath(
|
|
1675
|
+
this.stripTrailing(basePath) + "/" + path7
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Convert an absolute path to relative from the given base path.
|
|
1680
|
+
* If the path is not under the base, returns the original absolute path.
|
|
1681
|
+
*/
|
|
1682
|
+
toRelative(path7, basePath) {
|
|
1683
|
+
const normalBase = this.stripTrailing(basePath);
|
|
1684
|
+
const normalPath = this.stripTrailing(path7);
|
|
1685
|
+
if (!normalPath.startsWith(normalBase + "/") && normalPath !== normalBase) {
|
|
1686
|
+
const pathParts = normalPath.split("/");
|
|
1687
|
+
const baseParts = normalBase.split("/");
|
|
1688
|
+
let commonLength = 0;
|
|
1689
|
+
while (commonLength < pathParts.length && commonLength < baseParts.length && pathParts[commonLength] === baseParts[commonLength]) {
|
|
1690
|
+
commonLength++;
|
|
1691
|
+
}
|
|
1692
|
+
const upCount = baseParts.length - commonLength;
|
|
1693
|
+
const upSegments = Array(upCount).fill("..");
|
|
1694
|
+
const downSegments = pathParts.slice(commonLength);
|
|
1695
|
+
const relativeParts = [...upSegments, ...downSegments];
|
|
1696
|
+
return createNormalizedPath(
|
|
1697
|
+
relativeParts.length > 0 ? relativeParts.join("/") : "."
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
if (normalPath === normalBase) {
|
|
1701
|
+
return createNormalizedPath(".");
|
|
1702
|
+
}
|
|
1703
|
+
return createNormalizedPath(normalPath.slice(normalBase.length + 1));
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Join path segments, normalizing the result.
|
|
1707
|
+
* The base path is used as the starting point.
|
|
1708
|
+
*/
|
|
1709
|
+
join(base, ...segments) {
|
|
1710
|
+
const allSegments = [base, ...segments].filter(Boolean);
|
|
1711
|
+
const joined = allSegments.join("/");
|
|
1712
|
+
const isWindows = /^[A-Z]:/i.test(base);
|
|
1713
|
+
const os3 = {
|
|
1714
|
+
family: isWindows ? "windows" : "linux",
|
|
1715
|
+
version: "",
|
|
1716
|
+
release: "",
|
|
1717
|
+
architecture: { name: "x64" }
|
|
1718
|
+
};
|
|
1719
|
+
return this.normalize(joined, os3);
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Return the parent directory path, or null for root paths.
|
|
1723
|
+
*/
|
|
1724
|
+
parent(path7) {
|
|
1725
|
+
const stripped = this.stripTrailing(path7);
|
|
1726
|
+
if (stripped === "/" || /^[A-Z]:\/?$/i.test(stripped)) {
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
if (/^\/\/[^/]+\/[^/]+\/?$/.test(stripped)) {
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
const lastSlash = stripped.lastIndexOf("/");
|
|
1733
|
+
if (lastSlash === -1) {
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
if (lastSlash === 0) {
|
|
1737
|
+
return createNormalizedPath("/");
|
|
1738
|
+
}
|
|
1739
|
+
const parentStr = stripped.slice(0, lastSlash);
|
|
1740
|
+
if (/^[A-Z]:$/i.test(parentStr)) {
|
|
1741
|
+
return createNormalizedPath(parentStr + "/");
|
|
1742
|
+
}
|
|
1743
|
+
return createNormalizedPath(parentStr);
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Extract file extension (without leading dot), or null if none.
|
|
1747
|
+
*/
|
|
1748
|
+
extension(path7) {
|
|
1749
|
+
const stripped = this.stripTrailing(path7);
|
|
1750
|
+
const lastSlash = stripped.lastIndexOf("/");
|
|
1751
|
+
const filename = lastSlash >= 0 ? stripped.slice(lastSlash + 1) : stripped;
|
|
1752
|
+
if (filename.startsWith(".") && filename.indexOf(".", 1) === -1) {
|
|
1753
|
+
return null;
|
|
1754
|
+
}
|
|
1755
|
+
const lastDot = filename.lastIndexOf(".");
|
|
1756
|
+
if (lastDot <= 0) {
|
|
1757
|
+
return null;
|
|
1758
|
+
}
|
|
1759
|
+
return filename.slice(lastDot + 1);
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Get the default .claude directory path for the given home directory.
|
|
1763
|
+
*/
|
|
1764
|
+
getDefaultClaudeDir(homeDir, os3) {
|
|
1765
|
+
const homePath = this.normalize(homeDir.path, os3);
|
|
1766
|
+
return this.join(homePath, ".claude");
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Check if a normalized path is absolute.
|
|
1770
|
+
*/
|
|
1771
|
+
isAbsolute(path7) {
|
|
1772
|
+
return path7.startsWith("/") || /^[A-Z]:\//i.test(path7) || path7.startsWith("//");
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Strip trailing slash from a path (preserving root paths).
|
|
1776
|
+
*/
|
|
1777
|
+
stripTrailing(path7) {
|
|
1778
|
+
if (path7 === "/" || /^[A-Z]:\/$/.test(path7) || /^\/\/[^/]+\/[^/]+\/$/.test(path7)) {
|
|
1779
|
+
return path7;
|
|
1780
|
+
}
|
|
1781
|
+
return path7.replace(/\/+$/, "");
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
// src/platform/permissions.ts
|
|
1786
|
+
import * as fs2 from "fs";
|
|
1787
|
+
import { execSync } from "child_process";
|
|
1788
|
+
async function statPath(nativePath) {
|
|
1789
|
+
try {
|
|
1790
|
+
const stat5 = await fs2.promises.stat(nativePath);
|
|
1791
|
+
return { exists: true, isDirectory: stat5.isDirectory() };
|
|
1792
|
+
} catch {
|
|
1793
|
+
return { exists: false, isDirectory: false };
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
async function checkPermission(nativePath, flag) {
|
|
1797
|
+
try {
|
|
1798
|
+
await fs2.promises.access(nativePath, flag);
|
|
1799
|
+
return true;
|
|
1800
|
+
} catch {
|
|
1801
|
+
return false;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
function buildElevationInstructions(platform, targetPath) {
|
|
1805
|
+
const family = platform.os.family;
|
|
1806
|
+
switch (family) {
|
|
1807
|
+
case "windows":
|
|
1808
|
+
return {
|
|
1809
|
+
platform: "windows",
|
|
1810
|
+
command: "Run terminal as Administrator",
|
|
1811
|
+
description: "Right-click your terminal and select 'Run as Administrator', then re-run the command.",
|
|
1812
|
+
shellSpecificHint: "In PowerShell: Start-Process pwsh -Verb RunAs"
|
|
1813
|
+
};
|
|
1814
|
+
case "macos":
|
|
1815
|
+
return {
|
|
1816
|
+
platform: "macos",
|
|
1817
|
+
command: `sudo sixsevenai install --target "${targetPath}"`,
|
|
1818
|
+
description: "Prefix the command with sudo to run with root privileges.",
|
|
1819
|
+
shellSpecificHint: null
|
|
1820
|
+
};
|
|
1821
|
+
case "linux":
|
|
1822
|
+
return {
|
|
1823
|
+
platform: "linux",
|
|
1824
|
+
command: `sudo sixsevenai install --target "${targetPath}"`,
|
|
1825
|
+
description: "Prefix the command with sudo to run with root privileges.",
|
|
1826
|
+
shellSpecificHint: null
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
function findNearestExistingParent(nativePath) {
|
|
1831
|
+
let current = nativePath;
|
|
1832
|
+
while (true) {
|
|
1833
|
+
try {
|
|
1834
|
+
const stat5 = fs2.statSync(current);
|
|
1835
|
+
if (stat5.isDirectory()) {
|
|
1836
|
+
return current;
|
|
1837
|
+
}
|
|
1838
|
+
} catch {
|
|
1839
|
+
}
|
|
1840
|
+
const parent = getParentNative(current);
|
|
1841
|
+
if (!parent || parent === current) {
|
|
1842
|
+
return current;
|
|
1843
|
+
}
|
|
1844
|
+
current = parent;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
function getParentNative(nativePath) {
|
|
1848
|
+
const normalized = nativePath.replace(/\\/g, "/");
|
|
1849
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
1850
|
+
if (lastSlash <= 0) {
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
if (/^[A-Z]:\/?$/i.test(normalized)) {
|
|
1854
|
+
return null;
|
|
1855
|
+
}
|
|
1856
|
+
const parent = normalized.slice(0, lastSlash);
|
|
1857
|
+
if (/^[A-Z]:$/i.test(parent)) {
|
|
1858
|
+
return parent + "/";
|
|
1859
|
+
}
|
|
1860
|
+
return parent || "/";
|
|
1861
|
+
}
|
|
1862
|
+
var PermissionChecker = class {
|
|
1863
|
+
pathNormalizer = new PathNormalizer();
|
|
1864
|
+
/**
|
|
1865
|
+
* Check access to a path with the specified access mode.
|
|
1866
|
+
* Never throws -- returns a PermissionResult with appropriate flags.
|
|
1867
|
+
*/
|
|
1868
|
+
async checkAccess(path7, desiredAccess, platform) {
|
|
1869
|
+
const nativePath = this.pathNormalizer.toNative(path7, platform.os);
|
|
1870
|
+
const now = /* @__PURE__ */ new Date();
|
|
1871
|
+
try {
|
|
1872
|
+
const { exists, isDirectory } = await statPath(nativePath);
|
|
1873
|
+
if (!exists) {
|
|
1874
|
+
return {
|
|
1875
|
+
targetPath: path7,
|
|
1876
|
+
canRead: false,
|
|
1877
|
+
canWrite: false,
|
|
1878
|
+
canExecute: false,
|
|
1879
|
+
exists: false,
|
|
1880
|
+
isDirectory: false,
|
|
1881
|
+
requiresElevation: false,
|
|
1882
|
+
elevationInstructions: null,
|
|
1883
|
+
checkedAt: now
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
const canRead = await checkPermission(nativePath, fs2.constants.R_OK);
|
|
1887
|
+
const canWrite = await checkPermission(nativePath, fs2.constants.W_OK);
|
|
1888
|
+
const canExecute = await checkPermission(nativePath, fs2.constants.X_OK);
|
|
1889
|
+
let needsElevation = false;
|
|
1890
|
+
switch (desiredAccess) {
|
|
1891
|
+
case "read":
|
|
1892
|
+
needsElevation = !canRead;
|
|
1893
|
+
break;
|
|
1894
|
+
case "write":
|
|
1895
|
+
needsElevation = !canWrite;
|
|
1896
|
+
break;
|
|
1897
|
+
case "execute":
|
|
1898
|
+
needsElevation = !canExecute;
|
|
1899
|
+
break;
|
|
1900
|
+
case "readwrite":
|
|
1901
|
+
needsElevation = !canRead || !canWrite;
|
|
1902
|
+
break;
|
|
1903
|
+
}
|
|
1904
|
+
const elevationInstructions = needsElevation ? buildElevationInstructions(platform, nativePath) : null;
|
|
1905
|
+
return {
|
|
1906
|
+
targetPath: path7,
|
|
1907
|
+
canRead,
|
|
1908
|
+
canWrite,
|
|
1909
|
+
canExecute,
|
|
1910
|
+
exists,
|
|
1911
|
+
isDirectory,
|
|
1912
|
+
requiresElevation: needsElevation,
|
|
1913
|
+
elevationInstructions,
|
|
1914
|
+
checkedAt: now
|
|
1915
|
+
};
|
|
1916
|
+
} catch {
|
|
1917
|
+
return {
|
|
1918
|
+
targetPath: path7,
|
|
1919
|
+
canRead: false,
|
|
1920
|
+
canWrite: false,
|
|
1921
|
+
canExecute: false,
|
|
1922
|
+
exists: false,
|
|
1923
|
+
isDirectory: false,
|
|
1924
|
+
requiresElevation: false,
|
|
1925
|
+
elevationInstructions: null,
|
|
1926
|
+
checkedAt: now
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Convenience method: check write access to a path.
|
|
1932
|
+
*/
|
|
1933
|
+
async checkWriteAccess(path7, platform) {
|
|
1934
|
+
return this.checkAccess(path7, "write", platform);
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Check whether a directory can be created at the given path.
|
|
1938
|
+
* If the path does not exist, checks permissions on the nearest
|
|
1939
|
+
* existing parent directory.
|
|
1940
|
+
*/
|
|
1941
|
+
async checkDirectoryCreatable(path7, platform) {
|
|
1942
|
+
const nativePath = this.pathNormalizer.toNative(path7, platform.os);
|
|
1943
|
+
const now = /* @__PURE__ */ new Date();
|
|
1944
|
+
try {
|
|
1945
|
+
const { exists, isDirectory } = await statPath(nativePath);
|
|
1946
|
+
if (exists) {
|
|
1947
|
+
if (isDirectory) {
|
|
1948
|
+
return this.checkAccess(path7, "write", platform);
|
|
1949
|
+
}
|
|
1950
|
+
return {
|
|
1951
|
+
targetPath: path7,
|
|
1952
|
+
canRead: false,
|
|
1953
|
+
canWrite: false,
|
|
1954
|
+
canExecute: false,
|
|
1955
|
+
exists: true,
|
|
1956
|
+
isDirectory: false,
|
|
1957
|
+
requiresElevation: false,
|
|
1958
|
+
elevationInstructions: null,
|
|
1959
|
+
checkedAt: now
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
const nearestParent = findNearestExistingParent(nativePath);
|
|
1963
|
+
const canWriteParent = await checkPermission(
|
|
1964
|
+
nearestParent,
|
|
1965
|
+
fs2.constants.W_OK
|
|
1966
|
+
);
|
|
1967
|
+
const needsElevation = !canWriteParent;
|
|
1968
|
+
const elevationInstructions = needsElevation ? buildElevationInstructions(platform, nativePath) : null;
|
|
1969
|
+
return {
|
|
1970
|
+
targetPath: path7,
|
|
1971
|
+
canRead: false,
|
|
1972
|
+
canWrite: canWriteParent,
|
|
1973
|
+
canExecute: false,
|
|
1974
|
+
exists: false,
|
|
1975
|
+
isDirectory: false,
|
|
1976
|
+
requiresElevation: needsElevation,
|
|
1977
|
+
elevationInstructions,
|
|
1978
|
+
checkedAt: now
|
|
1979
|
+
};
|
|
1980
|
+
} catch {
|
|
1981
|
+
return {
|
|
1982
|
+
targetPath: path7,
|
|
1983
|
+
canRead: false,
|
|
1984
|
+
canWrite: false,
|
|
1985
|
+
canExecute: false,
|
|
1986
|
+
exists: false,
|
|
1987
|
+
isDirectory: false,
|
|
1988
|
+
requiresElevation: false,
|
|
1989
|
+
elevationInstructions: null,
|
|
1990
|
+
checkedAt: now
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Detect whether the current process is running with elevated privileges.
|
|
1996
|
+
*
|
|
1997
|
+
* - Unix: checks process.getuid() === 0
|
|
1998
|
+
* - Windows: executes `net session` via child_process.execSync
|
|
1999
|
+
*
|
|
2000
|
+
* Never throws. Returns false on detection failure.
|
|
2001
|
+
*/
|
|
2002
|
+
async detectElevation(platform) {
|
|
2003
|
+
try {
|
|
2004
|
+
if (platform.os.family === "windows") {
|
|
2005
|
+
return this.detectWindowsElevation();
|
|
2006
|
+
}
|
|
2007
|
+
return this.detectUnixElevation();
|
|
2008
|
+
} catch {
|
|
2009
|
+
return false;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Detect whether symbolic links can be created on the current platform.
|
|
2014
|
+
*
|
|
2015
|
+
* - Unix: always supported (no restrictions)
|
|
2016
|
+
* - Windows: depends on Developer Mode and/or elevation status
|
|
2017
|
+
*/
|
|
2018
|
+
async checkSymlinkCapability(platform) {
|
|
2019
|
+
if (platform.os.family !== "windows") {
|
|
2020
|
+
return {
|
|
2021
|
+
supported: true,
|
|
2022
|
+
requiresDeveloperMode: false,
|
|
2023
|
+
requiresElevation: false,
|
|
2024
|
+
instructions: null
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
const isElevated = await this.detectElevation(platform);
|
|
2028
|
+
if (isElevated) {
|
|
2029
|
+
return {
|
|
2030
|
+
supported: true,
|
|
2031
|
+
requiresDeveloperMode: false,
|
|
2032
|
+
requiresElevation: false,
|
|
2033
|
+
instructions: null
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
const hasDeveloperMode = this.checkWindowsDeveloperMode();
|
|
2037
|
+
if (hasDeveloperMode) {
|
|
2038
|
+
return {
|
|
2039
|
+
supported: true,
|
|
2040
|
+
requiresDeveloperMode: false,
|
|
2041
|
+
requiresElevation: false,
|
|
2042
|
+
instructions: null
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
supported: false,
|
|
2047
|
+
requiresDeveloperMode: true,
|
|
2048
|
+
requiresElevation: true,
|
|
2049
|
+
instructions: "Enable Developer Mode in Windows Settings > For developers, or run the installer as Administrator."
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Detect Windows elevation by running `net session`.
|
|
2054
|
+
* If it succeeds, the process is elevated. If it fails with exit code 5
|
|
2055
|
+
* (Access denied), the process is not elevated.
|
|
2056
|
+
*/
|
|
2057
|
+
detectWindowsElevation() {
|
|
2058
|
+
try {
|
|
2059
|
+
execSync("net session", {
|
|
2060
|
+
stdio: "pipe",
|
|
2061
|
+
timeout: 5e3
|
|
2062
|
+
});
|
|
2063
|
+
return true;
|
|
2064
|
+
} catch {
|
|
2065
|
+
return false;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Detect Unix elevation by checking the effective user ID.
|
|
2070
|
+
*/
|
|
2071
|
+
detectUnixElevation() {
|
|
2072
|
+
if (typeof process.getuid === "function") {
|
|
2073
|
+
return process.getuid() === 0;
|
|
2074
|
+
}
|
|
2075
|
+
return false;
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Check if Windows Developer Mode is enabled by reading the registry.
|
|
2079
|
+
*/
|
|
2080
|
+
checkWindowsDeveloperMode() {
|
|
2081
|
+
try {
|
|
2082
|
+
const result = execSync(
|
|
2083
|
+
'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock" /v AllowDevelopmentWithoutDevLicense',
|
|
2084
|
+
{ stdio: "pipe", timeout: 5e3 }
|
|
2085
|
+
).toString();
|
|
2086
|
+
return result.includes("0x1");
|
|
2087
|
+
} catch {
|
|
2088
|
+
return false;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
|
|
2093
|
+
// src/platform/terminal.ts
|
|
2094
|
+
import * as os2 from "os";
|
|
2095
|
+
function detectEmulator() {
|
|
2096
|
+
const env = process.env;
|
|
2097
|
+
if (env["WT_SESSION"]) {
|
|
2098
|
+
return {
|
|
2099
|
+
name: "Windows Terminal",
|
|
2100
|
+
version: env["WT_SESSION"] ?? null,
|
|
2101
|
+
isKnown: true
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
if (env["TERM_PROGRAM"] === "vscode") {
|
|
2105
|
+
return {
|
|
2106
|
+
name: "VS Code Terminal",
|
|
2107
|
+
version: env["TERM_PROGRAM_VERSION"] ?? null,
|
|
2108
|
+
isKnown: true
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
if (env["TERM_PROGRAM"] === "iTerm.app") {
|
|
2112
|
+
return {
|
|
2113
|
+
name: "iTerm2",
|
|
2114
|
+
version: env["TERM_PROGRAM_VERSION"] ?? null,
|
|
2115
|
+
isKnown: true
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
if (env["TERM_PROGRAM"] === "Apple_Terminal") {
|
|
2119
|
+
return {
|
|
2120
|
+
name: "Apple Terminal",
|
|
2121
|
+
version: env["TERM_PROGRAM_VERSION"] ?? null,
|
|
2122
|
+
isKnown: true
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
if (env["ConEmuPID"] || env["ConEmuANSI"]) {
|
|
2126
|
+
return {
|
|
2127
|
+
name: "ConEmu",
|
|
2128
|
+
version: null,
|
|
2129
|
+
isKnown: true
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
if (env["GNOME_TERMINAL_SERVICE"] || env["GNOME_TERMINAL_SCREEN"]) {
|
|
2133
|
+
return {
|
|
2134
|
+
name: "GNOME Terminal",
|
|
2135
|
+
version: null,
|
|
2136
|
+
isKnown: true
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
if (env["TERM_PROGRAM"] === "Hyper") {
|
|
2140
|
+
return {
|
|
2141
|
+
name: "Hyper",
|
|
2142
|
+
version: env["TERM_PROGRAM_VERSION"] ?? null,
|
|
2143
|
+
isKnown: true
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
if (env["TERM_PROGRAM"] === "Alacritty" || env["ALACRITTY_LOG"]) {
|
|
2147
|
+
return {
|
|
2148
|
+
name: "Alacritty",
|
|
2149
|
+
version: null,
|
|
2150
|
+
isKnown: true
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
if (env["TERM"] === "xterm-kitty" || env["KITTY_WINDOW_ID"]) {
|
|
2154
|
+
return {
|
|
2155
|
+
name: "Kitty",
|
|
2156
|
+
version: null,
|
|
2157
|
+
isKnown: true
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
if (env["TERM_PROGRAM"] === "WezTerm") {
|
|
2161
|
+
return {
|
|
2162
|
+
name: "WezTerm",
|
|
2163
|
+
version: env["TERM_PROGRAM_VERSION"] ?? null,
|
|
2164
|
+
isKnown: true
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
if (env["TERM_PROGRAM"]) {
|
|
2168
|
+
return {
|
|
2169
|
+
name: env["TERM_PROGRAM"],
|
|
2170
|
+
version: env["TERM_PROGRAM_VERSION"] ?? null,
|
|
2171
|
+
isKnown: false
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
if (env["TERM"] && env["TERM"] !== "dumb") {
|
|
2175
|
+
return {
|
|
2176
|
+
name: env["TERM"],
|
|
2177
|
+
version: null,
|
|
2178
|
+
isKnown: false
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
return {
|
|
2182
|
+
name: null,
|
|
2183
|
+
version: null,
|
|
2184
|
+
isKnown: false
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
function parseForcedColor(value) {
|
|
2188
|
+
switch (value) {
|
|
2189
|
+
case "0":
|
|
2190
|
+
return 0 /* None */;
|
|
2191
|
+
case "1":
|
|
2192
|
+
case "true":
|
|
2193
|
+
case "":
|
|
2194
|
+
return 1 /* Basic */;
|
|
2195
|
+
case "2":
|
|
2196
|
+
return 2 /* Extended */;
|
|
2197
|
+
case "3":
|
|
2198
|
+
return 3 /* TrueColor */;
|
|
2199
|
+
default:
|
|
2200
|
+
return null;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
var TerminalDetector = class {
|
|
2204
|
+
/**
|
|
2205
|
+
* Detect all terminal capabilities and return a complete profile.
|
|
2206
|
+
*/
|
|
2207
|
+
detectProfile() {
|
|
2208
|
+
return {
|
|
2209
|
+
colorSupport: this.detectColorSupport(),
|
|
2210
|
+
dimensions: this.detectDimensions(),
|
|
2211
|
+
unicodeSupport: this.detectUnicodeSupport(),
|
|
2212
|
+
interactivity: this.detectInteractivity(),
|
|
2213
|
+
emulator: detectEmulator(),
|
|
2214
|
+
detectedAt: /* @__PURE__ */ new Date()
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Detect the color support level of the current terminal.
|
|
2219
|
+
*
|
|
2220
|
+
* Priority chain:
|
|
2221
|
+
* 1. NO_COLOR env var set -> None
|
|
2222
|
+
* 2. FORCE_COLOR env var set -> forced level
|
|
2223
|
+
* 3. stdout is not a TTY -> None
|
|
2224
|
+
* 4. COLORTERM = "truecolor" or "24bit" -> TrueColor
|
|
2225
|
+
* 5. Known terminal emulator -> emulator-specific level
|
|
2226
|
+
* 6. TERM = "xterm-256color" -> Extended
|
|
2227
|
+
* 7. TERM contains "color" -> Basic
|
|
2228
|
+
* 8. Windows version >= 10.0.14393 -> Extended minimum
|
|
2229
|
+
* 9. Default -> Basic
|
|
2230
|
+
*/
|
|
2231
|
+
detectColorSupport() {
|
|
2232
|
+
const env = process.env;
|
|
2233
|
+
if ("NO_COLOR" in env) {
|
|
2234
|
+
return 0 /* None */;
|
|
2235
|
+
}
|
|
2236
|
+
if ("FORCE_COLOR" in env) {
|
|
2237
|
+
const forced = parseForcedColor(env["FORCE_COLOR"] ?? "");
|
|
2238
|
+
if (forced !== null) {
|
|
2239
|
+
return forced;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
if (!process.stdout.isTTY) {
|
|
2243
|
+
return 0 /* None */;
|
|
2244
|
+
}
|
|
2245
|
+
const colorTerm = env["COLORTERM"];
|
|
2246
|
+
if (colorTerm === "truecolor" || colorTerm === "24bit") {
|
|
2247
|
+
return 3 /* TrueColor */;
|
|
2248
|
+
}
|
|
2249
|
+
const termProgram = env["TERM_PROGRAM"];
|
|
2250
|
+
if (env["WT_SESSION"]) {
|
|
2251
|
+
return 3 /* TrueColor */;
|
|
2252
|
+
}
|
|
2253
|
+
if (termProgram === "vscode") {
|
|
2254
|
+
return 3 /* TrueColor */;
|
|
2255
|
+
}
|
|
2256
|
+
if (termProgram === "iTerm.app") {
|
|
2257
|
+
return 3 /* TrueColor */;
|
|
2258
|
+
}
|
|
2259
|
+
if (termProgram === "Apple_Terminal") {
|
|
2260
|
+
return 2 /* Extended */;
|
|
2261
|
+
}
|
|
2262
|
+
if (env["ConEmuANSI"] === "ON") {
|
|
2263
|
+
return 2 /* Extended */;
|
|
2264
|
+
}
|
|
2265
|
+
if (env["GNOME_TERMINAL_SERVICE"]) {
|
|
2266
|
+
return 2 /* Extended */;
|
|
2267
|
+
}
|
|
2268
|
+
if (env["TERM"] === "xterm-kitty" || env["KITTY_WINDOW_ID"] || termProgram === "Alacritty" || termProgram === "WezTerm" || termProgram === "Hyper") {
|
|
2269
|
+
return 3 /* TrueColor */;
|
|
2270
|
+
}
|
|
2271
|
+
const term = env["TERM"];
|
|
2272
|
+
if (term === "xterm-256color" || term === "screen-256color") {
|
|
2273
|
+
return 2 /* Extended */;
|
|
2274
|
+
}
|
|
2275
|
+
if (term && /color/i.test(term)) {
|
|
2276
|
+
return 1 /* Basic */;
|
|
2277
|
+
}
|
|
2278
|
+
if (process.platform === "win32") {
|
|
2279
|
+
const osRelease = os2.release();
|
|
2280
|
+
const versionParts = osRelease.split(".");
|
|
2281
|
+
const major = parseInt(versionParts[0], 10);
|
|
2282
|
+
const build = parseInt(versionParts[2], 10);
|
|
2283
|
+
if (major >= 10 && build >= 14393) {
|
|
2284
|
+
return 2 /* Extended */;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
return 1 /* Basic */;
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Detect terminal dimensions (columns x rows).
|
|
2291
|
+
* Falls back to 80x24 if detection fails.
|
|
2292
|
+
*/
|
|
2293
|
+
detectDimensions() {
|
|
2294
|
+
const columns = process.stdout.columns;
|
|
2295
|
+
const rows = process.stdout.rows;
|
|
2296
|
+
return {
|
|
2297
|
+
columns: columns && columns > 0 ? columns : 80,
|
|
2298
|
+
rows: rows && rows > 0 ? rows : 24
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Subscribe to terminal resize events.
|
|
2303
|
+
* Returns a Disposable to unsubscribe.
|
|
2304
|
+
*/
|
|
2305
|
+
onResize(callback) {
|
|
2306
|
+
const handler = () => {
|
|
2307
|
+
callback(this.detectDimensions());
|
|
2308
|
+
};
|
|
2309
|
+
process.stdout.on("resize", handler);
|
|
2310
|
+
return {
|
|
2311
|
+
dispose: () => {
|
|
2312
|
+
process.stdout.removeListener("resize", handler);
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Detect Unicode support in the terminal.
|
|
2318
|
+
*
|
|
2319
|
+
* Detection methods:
|
|
2320
|
+
* 1. Check LANG, LC_ALL, LC_CTYPE for UTF-8 reference
|
|
2321
|
+
* 2. Check for modern terminal emulators (WT_SESSION, TERM_PROGRAM)
|
|
2322
|
+
* 3. On Windows, check if running in a modern terminal
|
|
2323
|
+
* 4. Default to true on macOS/Linux, false on Windows CMD
|
|
2324
|
+
*/
|
|
2325
|
+
detectUnicodeSupport() {
|
|
2326
|
+
const env = process.env;
|
|
2327
|
+
const localeVars = [env["LC_ALL"], env["LC_CTYPE"], env["LANG"]];
|
|
2328
|
+
for (const localeVar of localeVars) {
|
|
2329
|
+
if (localeVar && /utf-?8/i.test(localeVar)) {
|
|
2330
|
+
return { supported: true, detectionMethod: "locale" };
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
if (env["WT_SESSION"] || env["TERM_PROGRAM"] === "vscode" || env["TERM_PROGRAM"] === "iTerm.app" || env["TERM_PROGRAM"] === "Hyper" || env["TERM_PROGRAM"] === "Alacritty" || env["TERM_PROGRAM"] === "WezTerm" || env["KITTY_WINDOW_ID"] || env["TERM"] === "xterm-kitty") {
|
|
2334
|
+
return { supported: true, detectionMethod: "terminal" };
|
|
2335
|
+
}
|
|
2336
|
+
if (env["ConEmuANSI"] === "ON") {
|
|
2337
|
+
return { supported: true, detectionMethod: "env" };
|
|
2338
|
+
}
|
|
2339
|
+
if (process.platform === "darwin") {
|
|
2340
|
+
return { supported: true, detectionMethod: "default" };
|
|
2341
|
+
}
|
|
2342
|
+
if (process.platform === "linux") {
|
|
2343
|
+
return { supported: true, detectionMethod: "default" };
|
|
2344
|
+
}
|
|
2345
|
+
if (process.platform === "win32") {
|
|
2346
|
+
return { supported: false, detectionMethod: "default" };
|
|
2347
|
+
}
|
|
2348
|
+
return { supported: true, detectionMethod: "default" };
|
|
2349
|
+
}
|
|
2350
|
+
/**
|
|
2351
|
+
* Detect terminal interactivity mode.
|
|
2352
|
+
*/
|
|
2353
|
+
detectInteractivity() {
|
|
2354
|
+
const isTTY = !!process.stdout.isTTY;
|
|
2355
|
+
const isInteractive = !!process.stdin.isTTY;
|
|
2356
|
+
const supportsRawMode = isInteractive && typeof process.stdin.setRawMode === "function";
|
|
2357
|
+
return {
|
|
2358
|
+
isInteractive,
|
|
2359
|
+
isTTY,
|
|
2360
|
+
supportsRawMode
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
|
|
2365
|
+
// src/platform/shell.ts
|
|
2366
|
+
import * as fs3 from "fs";
|
|
2367
|
+
function resolveShellType(env, platform) {
|
|
2368
|
+
if (env["PSModulePath"]) {
|
|
2369
|
+
const psPath = env["PSModulePath"] ?? "";
|
|
2370
|
+
const isPwshCore = psPath.includes("PowerShell") && !psPath.includes("WindowsPowerShell");
|
|
2371
|
+
if (platform === "windows") {
|
|
2372
|
+
return {
|
|
2373
|
+
type: "powershell",
|
|
2374
|
+
executablePath: isPwshCore ? "pwsh.exe" : "powershell.exe"
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
return {
|
|
2378
|
+
type: "powershell",
|
|
2379
|
+
executablePath: "pwsh"
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
const shell = env["SHELL"];
|
|
2383
|
+
if (shell) {
|
|
2384
|
+
const shellName = shell.split("/").pop()?.toLowerCase() ?? "";
|
|
2385
|
+
if (shellName.includes("zsh")) {
|
|
2386
|
+
return { type: "zsh", executablePath: shell };
|
|
2387
|
+
}
|
|
2388
|
+
if (shellName.includes("bash")) {
|
|
2389
|
+
return { type: "bash", executablePath: shell };
|
|
2390
|
+
}
|
|
2391
|
+
if (shellName.includes("fish")) {
|
|
2392
|
+
return { type: "fish", executablePath: shell };
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
if (env["MSYSTEM"]) {
|
|
2396
|
+
const bashPath = env["SHELL"] ?? "bash";
|
|
2397
|
+
return { type: "bash", executablePath: bashPath };
|
|
2398
|
+
}
|
|
2399
|
+
if (platform === "windows") {
|
|
2400
|
+
const comspec = env["ComSpec"];
|
|
2401
|
+
if (comspec && comspec.toLowerCase().endsWith("cmd.exe")) {
|
|
2402
|
+
return { type: "cmd", executablePath: comspec };
|
|
2403
|
+
}
|
|
2404
|
+
return { type: "powershell", executablePath: "pwsh.exe" };
|
|
2405
|
+
}
|
|
2406
|
+
if (platform === "macos") {
|
|
2407
|
+
return { type: "zsh", executablePath: "/bin/zsh" };
|
|
2408
|
+
}
|
|
2409
|
+
return { type: "bash", executablePath: "/bin/bash" };
|
|
2410
|
+
}
|
|
2411
|
+
function resolveProfilePath(shellType, platform, homePath) {
|
|
2412
|
+
const home = homePath.replace(/\\/g, "/");
|
|
2413
|
+
switch (shellType) {
|
|
2414
|
+
case "powershell": {
|
|
2415
|
+
if (platform === "windows") {
|
|
2416
|
+
return `${home}/Documents/PowerShell/Microsoft.PowerShell_profile.ps1`;
|
|
2417
|
+
}
|
|
2418
|
+
return `${home}/.config/powershell/Microsoft.PowerShell_profile.ps1`;
|
|
2419
|
+
}
|
|
2420
|
+
case "bash": {
|
|
2421
|
+
if (platform === "macos") {
|
|
2422
|
+
return `${home}/.bash_profile`;
|
|
2423
|
+
}
|
|
2424
|
+
return `${home}/.bashrc`;
|
|
2425
|
+
}
|
|
2426
|
+
case "zsh":
|
|
2427
|
+
return `${home}/.zshrc`;
|
|
2428
|
+
case "fish":
|
|
2429
|
+
return `${home}/.config/fish/config.fish`;
|
|
2430
|
+
case "cmd":
|
|
2431
|
+
return null;
|
|
2432
|
+
case "unknown":
|
|
2433
|
+
return null;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
function buildPathModificationCommand(shellType, directory, profilePath) {
|
|
2437
|
+
switch (shellType) {
|
|
2438
|
+
case "powershell":
|
|
2439
|
+
return `$env:PATH = "${directory}" + [System.IO.Path]::PathSeparator + $env:PATH`;
|
|
2440
|
+
case "bash":
|
|
2441
|
+
case "zsh":
|
|
2442
|
+
return `export PATH="${directory}:$PATH"`;
|
|
2443
|
+
case "fish":
|
|
2444
|
+
return `set -gx PATH "${directory}" $PATH`;
|
|
2445
|
+
case "cmd":
|
|
2446
|
+
return `set PATH=${directory};%PATH%`;
|
|
2447
|
+
case "unknown":
|
|
2448
|
+
return null;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function detectShellVersion(shellType, env) {
|
|
2452
|
+
switch (shellType) {
|
|
2453
|
+
case "bash": {
|
|
2454
|
+
return env["BASH_VERSION"] ?? null;
|
|
2455
|
+
}
|
|
2456
|
+
case "zsh": {
|
|
2457
|
+
return env["ZSH_VERSION"] ?? null;
|
|
2458
|
+
}
|
|
2459
|
+
case "fish": {
|
|
2460
|
+
return env["FISH_VERSION"] ?? null;
|
|
2461
|
+
}
|
|
2462
|
+
case "powershell": {
|
|
2463
|
+
return null;
|
|
2464
|
+
}
|
|
2465
|
+
default:
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
var ShellDetector = class {
|
|
2470
|
+
pathNormalizer = new PathNormalizer();
|
|
2471
|
+
/**
|
|
2472
|
+
* Detect the active shell and its profile configuration.
|
|
2473
|
+
*/
|
|
2474
|
+
async detectShell(platform) {
|
|
2475
|
+
const osFamily = platform.os.family;
|
|
2476
|
+
const env = process.env;
|
|
2477
|
+
const { type, executablePath } = resolveShellType(env, osFamily);
|
|
2478
|
+
const rawProfilePath = resolveProfilePath(
|
|
2479
|
+
type,
|
|
2480
|
+
osFamily,
|
|
2481
|
+
platform.homeDirectory.path
|
|
2482
|
+
);
|
|
2483
|
+
let profilePath = null;
|
|
2484
|
+
let profileExists = false;
|
|
2485
|
+
if (rawProfilePath) {
|
|
2486
|
+
profilePath = this.pathNormalizer.normalize(rawProfilePath, platform.os);
|
|
2487
|
+
const nativePath = this.pathNormalizer.toNative(profilePath, platform.os);
|
|
2488
|
+
try {
|
|
2489
|
+
profileExists = fs3.existsSync(nativePath);
|
|
2490
|
+
} catch {
|
|
2491
|
+
profileExists = false;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
const version2 = detectShellVersion(type, env);
|
|
2495
|
+
const pathModCommand = buildPathModificationCommand(
|
|
2496
|
+
type,
|
|
2497
|
+
"{directory}",
|
|
2498
|
+
rawProfilePath
|
|
2499
|
+
);
|
|
2500
|
+
const pathSeparator = osFamily === "windows" ? ";" : ":";
|
|
2501
|
+
return {
|
|
2502
|
+
type,
|
|
2503
|
+
version: version2,
|
|
2504
|
+
executablePath,
|
|
2505
|
+
profilePath,
|
|
2506
|
+
profileExists,
|
|
2507
|
+
pathSeparator,
|
|
2508
|
+
pathModificationCommand: pathModCommand
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Generate a shell-specific command to add a directory to PATH.
|
|
2513
|
+
* The returned command can be run immediately in the current session.
|
|
2514
|
+
*/
|
|
2515
|
+
getPathModificationCommand(shell, directory) {
|
|
2516
|
+
const nativeDir = shell.pathSeparator === ";" ? directory.replace(/\//g, "\\") : directory;
|
|
2517
|
+
const command = buildPathModificationCommand(
|
|
2518
|
+
shell.type,
|
|
2519
|
+
nativeDir,
|
|
2520
|
+
shell.profilePath
|
|
2521
|
+
);
|
|
2522
|
+
return command ?? `# Unable to generate PATH command for ${shell.type} shell`;
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Generate a command to open the shell's profile file for editing.
|
|
2526
|
+
* Returns null if the shell has no profile file.
|
|
2527
|
+
*/
|
|
2528
|
+
getProfileEditCommand(shell) {
|
|
2529
|
+
if (!shell.profilePath) {
|
|
2530
|
+
return null;
|
|
2531
|
+
}
|
|
2532
|
+
const profileStr = shell.profilePath;
|
|
2533
|
+
switch (shell.type) {
|
|
2534
|
+
case "powershell":
|
|
2535
|
+
return `code "${profileStr}" # or notepad "${profileStr}"`;
|
|
2536
|
+
case "bash":
|
|
2537
|
+
case "zsh":
|
|
2538
|
+
case "fish":
|
|
2539
|
+
return `${process.env["EDITOR"] ?? "nano"} "${profileStr}"`;
|
|
2540
|
+
case "cmd":
|
|
2541
|
+
return null;
|
|
2542
|
+
case "unknown":
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
|
|
2548
|
+
// src/platform/runtime.ts
|
|
2549
|
+
import { execSync as execSync2 } from "child_process";
|
|
2550
|
+
var MINIMUM_NODE_VERSION = semVer(18, 0, 0);
|
|
2551
|
+
var MINIMUM_NPM_VERSION = semVer(9, 0, 0);
|
|
2552
|
+
var COMMAND_TIMEOUT_MS = 1e4;
|
|
2553
|
+
function execCommand(command) {
|
|
2554
|
+
try {
|
|
2555
|
+
const result = execSync2(command, {
|
|
2556
|
+
encoding: "utf-8",
|
|
2557
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2558
|
+
timeout: COMMAND_TIMEOUT_MS
|
|
2559
|
+
});
|
|
2560
|
+
return result.trim();
|
|
2561
|
+
} catch {
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
function resolveExecutablePath(toolName) {
|
|
2566
|
+
const isWindows = process.platform === "win32";
|
|
2567
|
+
const command = isWindows ? `where ${toolName}` : `which ${toolName}`;
|
|
2568
|
+
const result = execCommand(command);
|
|
2569
|
+
if (!result) return null;
|
|
2570
|
+
return result.split(/\r?\n/)[0].trim();
|
|
2571
|
+
}
|
|
2572
|
+
var RuntimeValidator = class {
|
|
2573
|
+
/**
|
|
2574
|
+
* Run all runtime validation checks and return a complete RuntimeReport.
|
|
2575
|
+
*/
|
|
2576
|
+
async validate(minimumNode, minimumNpm) {
|
|
2577
|
+
const errors = [];
|
|
2578
|
+
const node = this.checkNodeVersion(minimumNode);
|
|
2579
|
+
if (!node.meetsMinimum) {
|
|
2580
|
+
errors.push({
|
|
2581
|
+
tool: "node",
|
|
2582
|
+
errorType: "version_too_low",
|
|
2583
|
+
message: `Node.js ${node.version.raw} is below the minimum required ${minimumNode.raw}.`,
|
|
2584
|
+
fix: `Upgrade Node.js to ${minimumNode.raw} or later. Visit https://nodejs.org/ or use nvm: nvm install ${minimumNode.major}`
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
let npm = null;
|
|
2588
|
+
try {
|
|
2589
|
+
npm = await this.checkNpmVersion(minimumNpm);
|
|
2590
|
+
if (!npm.meetsMinimum) {
|
|
2591
|
+
errors.push({
|
|
2592
|
+
tool: "npm",
|
|
2593
|
+
errorType: "version_too_low",
|
|
2594
|
+
message: `npm ${npm.version.raw} is below the minimum required ${minimumNpm.raw}.`,
|
|
2595
|
+
fix: `Upgrade npm: npm install -g npm@latest`
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
} catch {
|
|
2599
|
+
errors.push({
|
|
2600
|
+
tool: "npm",
|
|
2601
|
+
errorType: "not_found",
|
|
2602
|
+
message: "npm is not installed or not available on PATH.",
|
|
2603
|
+
fix: "Install Node.js (which includes npm) from https://nodejs.org/ or install npm separately."
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
let git = null;
|
|
2607
|
+
try {
|
|
2608
|
+
const gitResult = await this.checkGitVersion();
|
|
2609
|
+
git = gitResult;
|
|
2610
|
+
} catch {
|
|
2611
|
+
}
|
|
2612
|
+
let globalBinDir = null;
|
|
2613
|
+
if (npm) {
|
|
2614
|
+
try {
|
|
2615
|
+
globalBinDir = await this.resolveGlobalBinDir({
|
|
2616
|
+
type: "unknown",
|
|
2617
|
+
version: null,
|
|
2618
|
+
executablePath: "",
|
|
2619
|
+
profilePath: null,
|
|
2620
|
+
profileExists: false,
|
|
2621
|
+
pathSeparator: process.platform === "win32" ? ";" : ":",
|
|
2622
|
+
pathModificationCommand: null
|
|
2623
|
+
});
|
|
2624
|
+
if (!globalBinDir.isOnPath) {
|
|
2625
|
+
errors.push({
|
|
2626
|
+
tool: "npm",
|
|
2627
|
+
errorType: "not_on_path",
|
|
2628
|
+
message: `npm global bin directory (${globalBinDir.path}) is not on PATH.`,
|
|
2629
|
+
fix: globalBinDir.pathFixCommand ?? `Add ${globalBinDir.path} to your PATH environment variable.`
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
} catch {
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
const allRequirementsMet = node.meetsMinimum && (npm?.meetsMinimum ?? false) && (globalBinDir?.isOnPath ?? false);
|
|
2636
|
+
return {
|
|
2637
|
+
node,
|
|
2638
|
+
npm,
|
|
2639
|
+
git,
|
|
2640
|
+
globalBinDir,
|
|
2641
|
+
allRequirementsMet,
|
|
2642
|
+
errors,
|
|
2643
|
+
validatedAt: /* @__PURE__ */ new Date()
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Check Node.js version against the minimum requirement.
|
|
2648
|
+
* This is synchronous since we read from process.version.
|
|
2649
|
+
*/
|
|
2650
|
+
checkNodeVersion(minimum) {
|
|
2651
|
+
const rawVersion = process.version;
|
|
2652
|
+
const version2 = parseSemVer(rawVersion);
|
|
2653
|
+
if (!version2) {
|
|
2654
|
+
return {
|
|
2655
|
+
tool: "node",
|
|
2656
|
+
version: semVer(0, 0, 0),
|
|
2657
|
+
executablePath: process.execPath,
|
|
2658
|
+
meetsMinimum: false,
|
|
2659
|
+
minimumRequired: minimum
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
return {
|
|
2663
|
+
tool: "node",
|
|
2664
|
+
version: version2,
|
|
2665
|
+
executablePath: process.execPath,
|
|
2666
|
+
meetsMinimum: semVerSatisfiesMinimum(version2, minimum),
|
|
2667
|
+
minimumRequired: minimum
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Check npm version against the minimum requirement.
|
|
2672
|
+
* Executes `npm --version` via child_process.
|
|
2673
|
+
*/
|
|
2674
|
+
async checkNpmVersion(minimum) {
|
|
2675
|
+
const rawVersion = execCommand("npm --version");
|
|
2676
|
+
if (!rawVersion) {
|
|
2677
|
+
throw new Error("npm is not available");
|
|
2678
|
+
}
|
|
2679
|
+
const version2 = parseSemVer(rawVersion);
|
|
2680
|
+
if (!version2) {
|
|
2681
|
+
throw new Error(`Unable to parse npm version: ${rawVersion}`);
|
|
2682
|
+
}
|
|
2683
|
+
const executablePath = resolveExecutablePath("npm") ?? "npm";
|
|
2684
|
+
return {
|
|
2685
|
+
tool: "npm",
|
|
2686
|
+
version: version2,
|
|
2687
|
+
executablePath,
|
|
2688
|
+
meetsMinimum: semVerSatisfiesMinimum(version2, minimum),
|
|
2689
|
+
minimumRequired: minimum
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Check git version. Git is not strictly required but is reported.
|
|
2694
|
+
*/
|
|
2695
|
+
async checkGitVersion() {
|
|
2696
|
+
const rawVersion = execCommand("git --version");
|
|
2697
|
+
if (!rawVersion) {
|
|
2698
|
+
throw new Error("git is not available");
|
|
2699
|
+
}
|
|
2700
|
+
const versionMatch = /(\d+\.\d+\.\d+)/.exec(rawVersion);
|
|
2701
|
+
if (!versionMatch) {
|
|
2702
|
+
throw new Error(`Unable to parse git version: ${rawVersion}`);
|
|
2703
|
+
}
|
|
2704
|
+
const version2 = parseSemVer(versionMatch[1]);
|
|
2705
|
+
if (!version2) {
|
|
2706
|
+
throw new Error(`Unable to parse git version: ${versionMatch[1]}`);
|
|
2707
|
+
}
|
|
2708
|
+
const executablePath = resolveExecutablePath("git") ?? "git";
|
|
2709
|
+
return {
|
|
2710
|
+
tool: "git",
|
|
2711
|
+
version: version2,
|
|
2712
|
+
executablePath,
|
|
2713
|
+
meetsMinimum: true,
|
|
2714
|
+
minimumRequired: semVer(0, 0, 0)
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Resolve the npm global bin directory and check if it is on PATH.
|
|
2719
|
+
*
|
|
2720
|
+
* On Unix: `npm config get prefix` returns the prefix, bin dir is `{prefix}/bin`
|
|
2721
|
+
* On Windows: `npm config get prefix` returns the prefix, which IS the bin dir
|
|
2722
|
+
*/
|
|
2723
|
+
async resolveGlobalBinDir(shell) {
|
|
2724
|
+
const prefix = execCommand("npm config get prefix");
|
|
2725
|
+
if (!prefix) {
|
|
2726
|
+
throw new Error("Unable to resolve npm global bin directory");
|
|
2727
|
+
}
|
|
2728
|
+
const isWindows = process.platform === "win32";
|
|
2729
|
+
const binDir = isWindows ? prefix.replace(/\\/g, "/") : prefix.replace(/\\/g, "/") + "/bin";
|
|
2730
|
+
const normalizedBinDir = createNormalizedPath(binDir);
|
|
2731
|
+
const pathEnv = process.env["PATH"] ?? process.env["Path"] ?? "";
|
|
2732
|
+
const pathDirs = pathEnv.split(shell.pathSeparator);
|
|
2733
|
+
const isOnPath = pathDirs.some((dir) => {
|
|
2734
|
+
const normalizedDir = dir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2735
|
+
return normalizedDir.toLowerCase() === binDir.toLowerCase() || normalizedDir.toLowerCase() === binDir.replace(/\/bin$/, "").toLowerCase();
|
|
2736
|
+
});
|
|
2737
|
+
let pathFixCommand = null;
|
|
2738
|
+
if (!isOnPath) {
|
|
2739
|
+
if (isWindows) {
|
|
2740
|
+
pathFixCommand = `$env:PATH = "${binDir.replace(/\//g, "\\")}" + ";" + $env:PATH`;
|
|
2741
|
+
} else {
|
|
2742
|
+
pathFixCommand = `export PATH="${binDir}:$PATH"`;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
return {
|
|
2746
|
+
path: normalizedBinDir,
|
|
2747
|
+
isOnPath,
|
|
2748
|
+
pathFixCommand
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Check if an arbitrary tool is available on PATH.
|
|
2753
|
+
*/
|
|
2754
|
+
async checkToolAvailable(toolName) {
|
|
2755
|
+
const executablePath = resolveExecutablePath(toolName);
|
|
2756
|
+
return {
|
|
2757
|
+
available: executablePath !== null,
|
|
2758
|
+
path: executablePath
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
};
|
|
2762
|
+
|
|
2763
|
+
// src/platform/facade.ts
|
|
2764
|
+
function safeTerminalProfile() {
|
|
2765
|
+
return {
|
|
2766
|
+
colorSupport: 0 /* None */,
|
|
2767
|
+
dimensions: { columns: 80, rows: 24 },
|
|
2768
|
+
unicodeSupport: { supported: false, detectionMethod: "default" },
|
|
2769
|
+
interactivity: { isInteractive: false, isTTY: false, supportsRawMode: false },
|
|
2770
|
+
emulator: { name: null, version: null, isKnown: false },
|
|
2771
|
+
detectedAt: /* @__PURE__ */ new Date()
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
function safeShellEnvironment() {
|
|
2775
|
+
return {
|
|
2776
|
+
type: "unknown",
|
|
2777
|
+
version: null,
|
|
2778
|
+
executablePath: "",
|
|
2779
|
+
profilePath: null,
|
|
2780
|
+
profileExists: false,
|
|
2781
|
+
pathSeparator: process.platform === "win32" ? ";" : ":",
|
|
2782
|
+
pathModificationCommand: null
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
function safeRuntimeReport() {
|
|
2786
|
+
return {
|
|
2787
|
+
node: {
|
|
2788
|
+
tool: "node",
|
|
2789
|
+
version: semVer(0, 0, 0),
|
|
2790
|
+
executablePath: process.execPath,
|
|
2791
|
+
meetsMinimum: false,
|
|
2792
|
+
minimumRequired: MINIMUM_NODE_VERSION
|
|
2793
|
+
},
|
|
2794
|
+
npm: null,
|
|
2795
|
+
git: null,
|
|
2796
|
+
globalBinDir: null,
|
|
2797
|
+
allRequirementsMet: false,
|
|
2798
|
+
errors: [
|
|
2799
|
+
{
|
|
2800
|
+
tool: "runtime",
|
|
2801
|
+
errorType: "not_found",
|
|
2802
|
+
message: "Runtime validation failed unexpectedly.",
|
|
2803
|
+
fix: "Ensure Node.js >= 18.0.0 and npm >= 9.0.0 are installed."
|
|
2804
|
+
}
|
|
2805
|
+
],
|
|
2806
|
+
validatedAt: /* @__PURE__ */ new Date()
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
var PlatformFacade = class {
|
|
2810
|
+
platformDetector;
|
|
2811
|
+
pathNormalizer;
|
|
2812
|
+
permissionChecker;
|
|
2813
|
+
terminalDetector;
|
|
2814
|
+
shellDetector;
|
|
2815
|
+
runtimeValidator;
|
|
2816
|
+
constructor() {
|
|
2817
|
+
this.platformDetector = new PlatformDetector();
|
|
2818
|
+
this.pathNormalizer = new PathNormalizer();
|
|
2819
|
+
this.permissionChecker = new PermissionChecker();
|
|
2820
|
+
this.terminalDetector = new TerminalDetector();
|
|
2821
|
+
this.shellDetector = new ShellDetector();
|
|
2822
|
+
this.runtimeValidator = new RuntimeValidator();
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Detect all platform capabilities in a single coordinated call.
|
|
2826
|
+
*
|
|
2827
|
+
* Detection flow:
|
|
2828
|
+
* 1. PlatformDetector.detect() -- FATAL on failure
|
|
2829
|
+
* 2. TerminalDetector.detectProfile() -- parallel with step 3
|
|
2830
|
+
* 3. ShellDetector.detectShell(snapshot) -- parallel with step 2
|
|
2831
|
+
* 4. RuntimeValidator.validate() -- depends on step 3
|
|
2832
|
+
* 5. Assemble PlatformEnvironment
|
|
2833
|
+
*
|
|
2834
|
+
* @throws PlatformError with code UNSUPPORTED_PLATFORM if OS detection fails
|
|
2835
|
+
*/
|
|
2836
|
+
async detectAll() {
|
|
2837
|
+
const platform = this.platformDetector.detect();
|
|
2838
|
+
const [terminal, shell] = await Promise.all([
|
|
2839
|
+
this.safeDetectTerminal(),
|
|
2840
|
+
this.safeDetectShell(platform)
|
|
2841
|
+
]);
|
|
2842
|
+
const runtime = await this.safeValidateRuntime();
|
|
2843
|
+
const permissionChecker = this.permissionChecker;
|
|
2844
|
+
const pathNormalizer = this.pathNormalizer;
|
|
2845
|
+
return {
|
|
2846
|
+
platform,
|
|
2847
|
+
terminal,
|
|
2848
|
+
shell,
|
|
2849
|
+
runtime,
|
|
2850
|
+
permissions: {
|
|
2851
|
+
checkWriteAccess: (path7) => permissionChecker.checkWriteAccess(path7, platform),
|
|
2852
|
+
checkDirectoryCreatable: (path7) => permissionChecker.checkDirectoryCreatable(path7, platform)
|
|
2853
|
+
},
|
|
2854
|
+
paths: pathNormalizer
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
/**
|
|
2858
|
+
* Safely detect terminal profile. Returns safe defaults on failure.
|
|
2859
|
+
*/
|
|
2860
|
+
async safeDetectTerminal() {
|
|
2861
|
+
try {
|
|
2862
|
+
return this.terminalDetector.detectProfile();
|
|
2863
|
+
} catch {
|
|
2864
|
+
return safeTerminalProfile();
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
/**
|
|
2868
|
+
* Safely detect shell environment. Returns safe defaults on failure.
|
|
2869
|
+
*/
|
|
2870
|
+
async safeDetectShell(platform) {
|
|
2871
|
+
try {
|
|
2872
|
+
return await this.shellDetector.detectShell(platform);
|
|
2873
|
+
} catch {
|
|
2874
|
+
return safeShellEnvironment();
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
/**
|
|
2878
|
+
* Safely validate runtime. Returns safe defaults on failure.
|
|
2879
|
+
*/
|
|
2880
|
+
async safeValidateRuntime() {
|
|
2881
|
+
try {
|
|
2882
|
+
return await this.runtimeValidator.validate(
|
|
2883
|
+
MINIMUM_NODE_VERSION,
|
|
2884
|
+
MINIMUM_NPM_VERSION
|
|
2885
|
+
);
|
|
2886
|
+
} catch {
|
|
2887
|
+
return safeRuntimeReport();
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
};
|
|
2891
|
+
|
|
2892
|
+
// src/engine/manifest.ts
|
|
2893
|
+
import * as fs4 from "fs/promises";
|
|
2894
|
+
import * as path2 from "path";
|
|
2895
|
+
var CATEGORY_RULES = [
|
|
2896
|
+
{ pattern: /^agents[/\\]/, type: "agents" },
|
|
2897
|
+
{ pattern: /^skills[/\\]/, type: "skills" },
|
|
2898
|
+
{ pattern: /^commands[/\\]/, type: "commands" },
|
|
2899
|
+
{ pattern: /^cli[/\\]/, type: "cli" },
|
|
2900
|
+
{ pattern: /^typescript[/\\]/, type: "cli" },
|
|
2901
|
+
{ pattern: /^workflows[/\\]/, type: "workflows" },
|
|
2902
|
+
{ pattern: /^metadata[/\\]/, type: "metadata" }
|
|
2903
|
+
];
|
|
2904
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
2905
|
+
"node_modules",
|
|
2906
|
+
".git",
|
|
2907
|
+
"dist",
|
|
2908
|
+
"build",
|
|
2909
|
+
"out",
|
|
2910
|
+
".next",
|
|
2911
|
+
"__pycache__",
|
|
2912
|
+
"coverage"
|
|
2913
|
+
]);
|
|
2914
|
+
function classifyComponent(relativePath) {
|
|
2915
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
2916
|
+
for (const rule of CATEGORY_RULES) {
|
|
2917
|
+
if (rule.pattern.test(normalized)) {
|
|
2918
|
+
return rule.type;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
return "metadata";
|
|
2922
|
+
}
|
|
2923
|
+
async function walkDirectory(rootDir, currentDir) {
|
|
2924
|
+
const entries = [];
|
|
2925
|
+
let dirEntries;
|
|
2926
|
+
try {
|
|
2927
|
+
dirEntries = await fs4.readdir(currentDir, { withFileTypes: true });
|
|
2928
|
+
} catch {
|
|
2929
|
+
return entries;
|
|
2930
|
+
}
|
|
2931
|
+
for (const dirEntry of dirEntries) {
|
|
2932
|
+
const fullPath = path2.join(currentDir, dirEntry.name);
|
|
2933
|
+
if (dirEntry.isDirectory()) {
|
|
2934
|
+
if (IGNORED_DIRECTORIES.has(dirEntry.name)) {
|
|
2935
|
+
continue;
|
|
2936
|
+
}
|
|
2937
|
+
const subEntries = await walkDirectory(rootDir, fullPath);
|
|
2938
|
+
entries.push(...subEntries);
|
|
2939
|
+
} else if (dirEntry.isFile()) {
|
|
2940
|
+
const relativePath = path2.relative(rootDir, fullPath);
|
|
2941
|
+
const componentType = classifyComponent(relativePath);
|
|
2942
|
+
try {
|
|
2943
|
+
const stat5 = await fs4.stat(fullPath);
|
|
2944
|
+
entries.push({
|
|
2945
|
+
relativePath,
|
|
2946
|
+
fileName: dirEntry.name,
|
|
2947
|
+
componentType,
|
|
2948
|
+
sizeBytes: stat5.size
|
|
2949
|
+
});
|
|
2950
|
+
} catch {
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
return entries;
|
|
2956
|
+
}
|
|
2957
|
+
async function scanSourceDirectory(sourceDirectory) {
|
|
2958
|
+
try {
|
|
2959
|
+
const stat5 = await fs4.stat(sourceDirectory);
|
|
2960
|
+
if (!stat5.isDirectory()) {
|
|
2961
|
+
throw new Error(
|
|
2962
|
+
`Source path is not a directory: ${sourceDirectory}`
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2965
|
+
} catch (error) {
|
|
2966
|
+
if (error instanceof Error && error.message.startsWith("Source path")) {
|
|
2967
|
+
throw error;
|
|
2968
|
+
}
|
|
2969
|
+
throw new Error(
|
|
2970
|
+
`Source directory does not exist or is not accessible: ${sourceDirectory}`
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
const entries = await walkDirectory(sourceDirectory, sourceDirectory);
|
|
2974
|
+
const countsByType = countComponentsByType(entries);
|
|
2975
|
+
return {
|
|
2976
|
+
entries,
|
|
2977
|
+
totalCount: entries.length,
|
|
2978
|
+
countsByType
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
function countComponentsByType(entries) {
|
|
2982
|
+
const counts = {
|
|
2983
|
+
agents: 0,
|
|
2984
|
+
skills: 0,
|
|
2985
|
+
commands: 0,
|
|
2986
|
+
cli: 0,
|
|
2987
|
+
workflows: 0,
|
|
2988
|
+
metadata: 0
|
|
2989
|
+
};
|
|
2990
|
+
for (const entry of entries) {
|
|
2991
|
+
counts[entry.componentType]++;
|
|
2992
|
+
}
|
|
2993
|
+
return counts;
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
// src/engine/conflict-detector.ts
|
|
2997
|
+
import * as fs5 from "fs/promises";
|
|
2998
|
+
import * as crypto from "crypto";
|
|
2999
|
+
import * as nodePath from "path";
|
|
3000
|
+
async function computeFileHash(filePath) {
|
|
3001
|
+
const content = await fs5.readFile(filePath);
|
|
3002
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
3003
|
+
}
|
|
3004
|
+
async function directoryExists(dirPath) {
|
|
3005
|
+
try {
|
|
3006
|
+
const stat5 = await fs5.stat(dirPath);
|
|
3007
|
+
return stat5.isDirectory();
|
|
3008
|
+
} catch {
|
|
3009
|
+
return false;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
async function fileExists(filePath) {
|
|
3013
|
+
try {
|
|
3014
|
+
const stat5 = await fs5.stat(filePath);
|
|
3015
|
+
return stat5.isFile();
|
|
3016
|
+
} catch {
|
|
3017
|
+
return false;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
async function detectConflicts(manifest, sourceDirectory, targetDirectory) {
|
|
3021
|
+
const isExistingInstallation = await directoryExists(targetDirectory);
|
|
3022
|
+
const files = [];
|
|
3023
|
+
const existingFiles = [];
|
|
3024
|
+
const modifiedFiles = [];
|
|
3025
|
+
let newCount = 0;
|
|
3026
|
+
let identicalCount = 0;
|
|
3027
|
+
let modifiedCount = 0;
|
|
3028
|
+
for (const entry of manifest.entries) {
|
|
3029
|
+
const sourcePath = nodePath.join(sourceDirectory, entry.relativePath);
|
|
3030
|
+
const targetPath = nodePath.join(targetDirectory, entry.relativePath);
|
|
3031
|
+
const targetFileExists = await fileExists(targetPath);
|
|
3032
|
+
if (!targetFileExists) {
|
|
3033
|
+
const sourceHash2 = await computeFileHash(sourcePath);
|
|
3034
|
+
files.push({
|
|
3035
|
+
relativePath: entry.relativePath,
|
|
3036
|
+
status: "new",
|
|
3037
|
+
sourceHash: sourceHash2,
|
|
3038
|
+
targetHash: null
|
|
3039
|
+
});
|
|
3040
|
+
newCount++;
|
|
3041
|
+
continue;
|
|
3042
|
+
}
|
|
3043
|
+
existingFiles.push(entry.relativePath);
|
|
3044
|
+
const [sourceHash, targetHash] = await Promise.all([
|
|
3045
|
+
computeFileHash(sourcePath),
|
|
3046
|
+
computeFileHash(targetPath)
|
|
3047
|
+
]);
|
|
3048
|
+
if (sourceHash === targetHash) {
|
|
3049
|
+
files.push({
|
|
3050
|
+
relativePath: entry.relativePath,
|
|
3051
|
+
status: "identical",
|
|
3052
|
+
sourceHash,
|
|
3053
|
+
targetHash
|
|
3054
|
+
});
|
|
3055
|
+
identicalCount++;
|
|
3056
|
+
} else {
|
|
3057
|
+
files.push({
|
|
3058
|
+
relativePath: entry.relativePath,
|
|
3059
|
+
status: "modified",
|
|
3060
|
+
sourceHash,
|
|
3061
|
+
targetHash
|
|
3062
|
+
});
|
|
3063
|
+
modifiedFiles.push(entry.relativePath);
|
|
3064
|
+
modifiedCount++;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
return {
|
|
3068
|
+
files,
|
|
3069
|
+
existingFiles,
|
|
3070
|
+
modifiedFiles,
|
|
3071
|
+
newCount,
|
|
3072
|
+
identicalCount,
|
|
3073
|
+
modifiedCount,
|
|
3074
|
+
hasConflicts: modifiedCount > 0,
|
|
3075
|
+
isExistingInstallation
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
// src/engine/file-copier.ts
|
|
3080
|
+
import * as fs6 from "fs/promises";
|
|
3081
|
+
import * as path3 from "path";
|
|
3082
|
+
var Semaphore = class {
|
|
3083
|
+
permits;
|
|
3084
|
+
queue = [];
|
|
3085
|
+
constructor(maxConcurrent) {
|
|
3086
|
+
this.permits = maxConcurrent;
|
|
3087
|
+
}
|
|
3088
|
+
/** Acquire a permit. Resolves immediately if available, else waits. */
|
|
3089
|
+
async acquire() {
|
|
3090
|
+
if (this.permits > 0) {
|
|
3091
|
+
this.permits--;
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
return new Promise((resolve3) => this.queue.push(resolve3));
|
|
3095
|
+
}
|
|
3096
|
+
/** Release a permit. Wakes the next waiter if any. */
|
|
3097
|
+
release() {
|
|
3098
|
+
const next = this.queue.shift();
|
|
3099
|
+
if (next) {
|
|
3100
|
+
next();
|
|
3101
|
+
} else {
|
|
3102
|
+
this.permits++;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
};
|
|
3106
|
+
async function ensureDirectories(files) {
|
|
3107
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
3108
|
+
for (const file of files) {
|
|
3109
|
+
const dir = path3.dirname(file.destinationPath);
|
|
3110
|
+
dirs.add(dir);
|
|
3111
|
+
}
|
|
3112
|
+
const sorted = Array.from(dirs).sort((a, b) => a.length - b.length);
|
|
3113
|
+
for (const dir of sorted) {
|
|
3114
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
async function copySingleFile(sourcePath, destinationPath) {
|
|
3118
|
+
await fs6.copyFile(sourcePath, destinationPath);
|
|
3119
|
+
}
|
|
3120
|
+
var DEFAULT_CONCURRENCY = 10;
|
|
3121
|
+
var MIN_CONCURRENCY = 1;
|
|
3122
|
+
var MAX_CONCURRENCY = 64;
|
|
3123
|
+
async function copyFiles(files, options = {}) {
|
|
3124
|
+
const startTime = Date.now();
|
|
3125
|
+
const concurrency = Math.max(
|
|
3126
|
+
MIN_CONCURRENCY,
|
|
3127
|
+
Math.min(MAX_CONCURRENCY, options.concurrency ?? DEFAULT_CONCURRENCY)
|
|
3128
|
+
);
|
|
3129
|
+
const onProgress = options.onProgress ?? (() => {
|
|
3130
|
+
});
|
|
3131
|
+
const total = files.length;
|
|
3132
|
+
if (total === 0) {
|
|
3133
|
+
return buildResult(true, 0, 0, [], [], startTime, {});
|
|
3134
|
+
}
|
|
3135
|
+
await ensureDirectories(files);
|
|
3136
|
+
const errors = [];
|
|
3137
|
+
const warnings = [];
|
|
3138
|
+
const componentCounts = {
|
|
3139
|
+
agents: 0,
|
|
3140
|
+
skills: 0,
|
|
3141
|
+
commands: 0,
|
|
3142
|
+
cli: 0,
|
|
3143
|
+
workflows: 0,
|
|
3144
|
+
metadata: 0
|
|
3145
|
+
};
|
|
3146
|
+
let copiedCount = 0;
|
|
3147
|
+
let skippedCount = 0;
|
|
3148
|
+
let completedCount = 0;
|
|
3149
|
+
let hasFailed = false;
|
|
3150
|
+
const semaphore = new Semaphore(concurrency);
|
|
3151
|
+
const tasks = files.map(async (file) => {
|
|
3152
|
+
await semaphore.acquire();
|
|
3153
|
+
try {
|
|
3154
|
+
if (hasFailed) {
|
|
3155
|
+
skippedCount++;
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
await copySingleFile(file.sourcePath, file.destinationPath);
|
|
3159
|
+
copiedCount++;
|
|
3160
|
+
componentCounts[file.componentType]++;
|
|
3161
|
+
} catch (error) {
|
|
3162
|
+
hasFailed = true;
|
|
3163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3164
|
+
errors.push({
|
|
3165
|
+
code: "COPY_FAILED",
|
|
3166
|
+
message: `Failed to copy ${file.relativePath}: ${message}`,
|
|
3167
|
+
filePath: file.relativePath,
|
|
3168
|
+
originalError: message
|
|
3169
|
+
});
|
|
3170
|
+
} finally {
|
|
3171
|
+
completedCount++;
|
|
3172
|
+
try {
|
|
3173
|
+
onProgress({
|
|
3174
|
+
fileName: file.fileName,
|
|
3175
|
+
current: completedCount,
|
|
3176
|
+
total
|
|
3177
|
+
});
|
|
3178
|
+
} catch {
|
|
3179
|
+
}
|
|
3180
|
+
semaphore.release();
|
|
3181
|
+
}
|
|
3182
|
+
});
|
|
3183
|
+
await Promise.all(tasks);
|
|
3184
|
+
const success = errors.length === 0;
|
|
3185
|
+
return buildResult(
|
|
3186
|
+
success,
|
|
3187
|
+
copiedCount,
|
|
3188
|
+
skippedCount,
|
|
3189
|
+
warnings,
|
|
3190
|
+
errors,
|
|
3191
|
+
startTime,
|
|
3192
|
+
componentCounts
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
function buildResult(success, copiedCount, skippedCount, warnings, errors, startTime, componentCounts) {
|
|
3196
|
+
return {
|
|
3197
|
+
success,
|
|
3198
|
+
copiedCount,
|
|
3199
|
+
skippedCount,
|
|
3200
|
+
warnings,
|
|
3201
|
+
errors,
|
|
3202
|
+
durationMs: Date.now() - startTime,
|
|
3203
|
+
componentCounts: {
|
|
3204
|
+
agents: componentCounts["agents"] ?? 0,
|
|
3205
|
+
skills: componentCounts["skills"] ?? 0,
|
|
3206
|
+
commands: componentCounts["commands"] ?? 0,
|
|
3207
|
+
cli: componentCounts["cli"] ?? 0,
|
|
3208
|
+
workflows: componentCounts["workflows"] ?? 0,
|
|
3209
|
+
metadata: componentCounts["metadata"] ?? 0
|
|
3210
|
+
}
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
// src/engine/rollback.ts
|
|
3215
|
+
import * as fs7 from "fs/promises";
|
|
3216
|
+
import * as path4 from "path";
|
|
3217
|
+
var BACKUP_ROOT_DIR = ".ai-dlc-backup";
|
|
3218
|
+
function generateTimestamp() {
|
|
3219
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "").replace(/\.\d+Z$/, "Z");
|
|
3220
|
+
}
|
|
3221
|
+
async function createBackup(targetDirectory, filesToBackup) {
|
|
3222
|
+
if (filesToBackup.length === 0) {
|
|
3223
|
+
return null;
|
|
3224
|
+
}
|
|
3225
|
+
const timestamp = generateTimestamp();
|
|
3226
|
+
const backupDirectory = path4.join(
|
|
3227
|
+
targetDirectory,
|
|
3228
|
+
BACKUP_ROOT_DIR,
|
|
3229
|
+
timestamp
|
|
3230
|
+
);
|
|
3231
|
+
await fs7.mkdir(backupDirectory, { recursive: true });
|
|
3232
|
+
const backedUpFiles = [];
|
|
3233
|
+
for (const relativePath of filesToBackup) {
|
|
3234
|
+
const sourcePath = path4.join(targetDirectory, relativePath);
|
|
3235
|
+
const backupPath = path4.join(backupDirectory, relativePath);
|
|
3236
|
+
try {
|
|
3237
|
+
await fs7.access(sourcePath);
|
|
3238
|
+
await fs7.mkdir(path4.dirname(backupPath), { recursive: true });
|
|
3239
|
+
await fs7.copyFile(sourcePath, backupPath);
|
|
3240
|
+
backedUpFiles.push({
|
|
3241
|
+
originalRelativePath: relativePath,
|
|
3242
|
+
backupPath
|
|
3243
|
+
});
|
|
3244
|
+
} catch {
|
|
3245
|
+
continue;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
return {
|
|
3249
|
+
backupDirectory,
|
|
3250
|
+
files: backedUpFiles,
|
|
3251
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
async function restoreFromBackup(backup, targetDirectory) {
|
|
3255
|
+
const errors = [];
|
|
3256
|
+
let restoredCount = 0;
|
|
3257
|
+
for (const file of backup.files) {
|
|
3258
|
+
const restorePath = path4.join(targetDirectory, file.originalRelativePath);
|
|
3259
|
+
try {
|
|
3260
|
+
await fs7.mkdir(path4.dirname(restorePath), { recursive: true });
|
|
3261
|
+
await fs7.copyFile(file.backupPath, restorePath);
|
|
3262
|
+
restoredCount++;
|
|
3263
|
+
} catch (error) {
|
|
3264
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3265
|
+
errors.push({
|
|
3266
|
+
code: "ROLLBACK_RESTORE_FAILED",
|
|
3267
|
+
message: `Failed to restore ${file.originalRelativePath}: ${message}`,
|
|
3268
|
+
filePath: file.originalRelativePath,
|
|
3269
|
+
originalError: message
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
return {
|
|
3274
|
+
success: errors.length === 0,
|
|
3275
|
+
restoredCount,
|
|
3276
|
+
errors
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
// src/engine/validator.ts
|
|
3281
|
+
import * as fs8 from "fs/promises";
|
|
3282
|
+
import * as path5 from "path";
|
|
3283
|
+
async function validateSingleFile(destinationPath, relativePath) {
|
|
3284
|
+
try {
|
|
3285
|
+
const stat5 = await fs8.stat(destinationPath);
|
|
3286
|
+
if (!stat5.isFile()) {
|
|
3287
|
+
return {
|
|
3288
|
+
relativePath,
|
|
3289
|
+
reason: "Path exists but is not a regular file"
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
if (stat5.size === 0) {
|
|
3293
|
+
return {
|
|
3294
|
+
relativePath,
|
|
3295
|
+
reason: "File exists but has zero size"
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
return null;
|
|
3299
|
+
} catch (error) {
|
|
3300
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
3301
|
+
if (code === "ENOENT") {
|
|
3302
|
+
return {
|
|
3303
|
+
relativePath,
|
|
3304
|
+
reason: "File does not exist at destination"
|
|
3305
|
+
};
|
|
3306
|
+
}
|
|
3307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3308
|
+
return {
|
|
3309
|
+
relativePath,
|
|
3310
|
+
reason: `Cannot access file: ${message}`
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
async function validateInstallation(files) {
|
|
3315
|
+
if (files.length === 0) {
|
|
3316
|
+
return {
|
|
3317
|
+
valid: true,
|
|
3318
|
+
totalChecked: 0,
|
|
3319
|
+
validCount: 0,
|
|
3320
|
+
missingFiles: [],
|
|
3321
|
+
invalidFiles: []
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
3324
|
+
const missingFiles = [];
|
|
3325
|
+
const invalidFiles = [];
|
|
3326
|
+
let validCount = 0;
|
|
3327
|
+
for (const file of files) {
|
|
3328
|
+
const issue = await validateSingleFile(
|
|
3329
|
+
file.destinationPath,
|
|
3330
|
+
file.relativePath
|
|
3331
|
+
);
|
|
3332
|
+
if (issue === null) {
|
|
3333
|
+
validCount++;
|
|
3334
|
+
} else if (issue.reason.includes("does not exist")) {
|
|
3335
|
+
missingFiles.push(file.relativePath);
|
|
3336
|
+
invalidFiles.push(issue);
|
|
3337
|
+
} else {
|
|
3338
|
+
invalidFiles.push(issue);
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
return {
|
|
3342
|
+
valid: missingFiles.length === 0 && invalidFiles.length === 0,
|
|
3343
|
+
totalChecked: files.length,
|
|
3344
|
+
validCount,
|
|
3345
|
+
missingFiles,
|
|
3346
|
+
invalidFiles
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
// src/orchestrator/workflow.ts
|
|
3351
|
+
function createInitialState() {
|
|
3352
|
+
return {
|
|
3353
|
+
status: "pending",
|
|
3354
|
+
steps: WORKFLOW_STEPS.map((def) => ({
|
|
3355
|
+
def,
|
|
3356
|
+
status: "pending",
|
|
3357
|
+
output: null,
|
|
3358
|
+
error: null,
|
|
3359
|
+
startedAt: null,
|
|
3360
|
+
completedAt: null
|
|
3361
|
+
})),
|
|
3362
|
+
currentStepId: null,
|
|
3363
|
+
startedAt: null,
|
|
3364
|
+
completedAt: null,
|
|
3365
|
+
cancellationReason: null
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
function createInitialContext(sourceDirectory) {
|
|
3369
|
+
return {
|
|
3370
|
+
platformEnv: null,
|
|
3371
|
+
terminalCapabilities: null,
|
|
3372
|
+
runtimeReport: null,
|
|
3373
|
+
targetDirectory: null,
|
|
3374
|
+
claudeDirectory: null,
|
|
3375
|
+
sourceDirectory,
|
|
3376
|
+
manifest: null,
|
|
3377
|
+
conflictReport: null,
|
|
3378
|
+
backupManifest: null,
|
|
3379
|
+
installationResult: null,
|
|
3380
|
+
validationReport: null,
|
|
3381
|
+
fileMappings: []
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
function buildTerminalCapabilities(env) {
|
|
3385
|
+
return {
|
|
3386
|
+
colorSupport: mapColorSupport(env.terminal.colorSupport),
|
|
3387
|
+
unicodeSupport: env.terminal.unicodeSupport.supported,
|
|
3388
|
+
width: env.terminal.dimensions.columns,
|
|
3389
|
+
height: env.terminal.dimensions.rows,
|
|
3390
|
+
isInteractive: env.terminal.interactivity.isInteractive
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
function buildFileMappings(manifest, sourceDirectory, claudeDirectory) {
|
|
3394
|
+
return manifest.entries.map((entry) => ({
|
|
3395
|
+
relativePath: entry.relativePath,
|
|
3396
|
+
sourcePath: path6.join(sourceDirectory, entry.relativePath),
|
|
3397
|
+
destinationPath: path6.join(claudeDirectory, entry.relativePath),
|
|
3398
|
+
componentType: entry.componentType,
|
|
3399
|
+
fileName: entry.fileName,
|
|
3400
|
+
sizeBytes: entry.sizeBytes
|
|
3401
|
+
}));
|
|
3402
|
+
}
|
|
3403
|
+
function buildDirectoryOptions(env) {
|
|
3404
|
+
const cwd = process.cwd();
|
|
3405
|
+
const home = env.platform.homeDirectory.path;
|
|
3406
|
+
const options = [
|
|
3407
|
+
{
|
|
3408
|
+
path: cwd,
|
|
3409
|
+
label: cwd,
|
|
3410
|
+
isDefault: true,
|
|
3411
|
+
exists: true
|
|
3412
|
+
}
|
|
3413
|
+
];
|
|
3414
|
+
if (home !== cwd) {
|
|
3415
|
+
options.push({
|
|
3416
|
+
path: home,
|
|
3417
|
+
label: home,
|
|
3418
|
+
isDefault: false,
|
|
3419
|
+
exists: true
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
return options;
|
|
3423
|
+
}
|
|
3424
|
+
function verboseLog(verbose, ...args) {
|
|
3425
|
+
if (verbose) {
|
|
3426
|
+
console.error("[verbose]", ...args);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
async function executeInstallation(options) {
|
|
3430
|
+
const errorHandler = new ErrorHandler();
|
|
3431
|
+
const workflow = createInitialState();
|
|
3432
|
+
let sourceDirectory;
|
|
3433
|
+
try {
|
|
3434
|
+
sourceDirectory = await resolveSourceDirectory();
|
|
3435
|
+
} catch (error) {
|
|
3436
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
3437
|
+
return EXIT_CANCELLED;
|
|
3438
|
+
}
|
|
3439
|
+
const ctx = createInitialContext(sourceDirectory);
|
|
3440
|
+
let cancelled = false;
|
|
3441
|
+
const sigintHandler = () => {
|
|
3442
|
+
cancelled = true;
|
|
3443
|
+
workflow.status = "cancelled";
|
|
3444
|
+
workflow.cancellationReason = "User pressed Ctrl+C";
|
|
3445
|
+
};
|
|
3446
|
+
process.on("SIGINT", sigintHandler);
|
|
3447
|
+
workflow.status = "running";
|
|
3448
|
+
workflow.startedAt = Date.now();
|
|
3449
|
+
let theme = null;
|
|
3450
|
+
try {
|
|
3451
|
+
for (const stepState of workflow.steps) {
|
|
3452
|
+
if (cancelled) {
|
|
3453
|
+
break;
|
|
3454
|
+
}
|
|
3455
|
+
const stepId = stepState.def.id;
|
|
3456
|
+
const isInteractive = ctx.terminalCapabilities?.isInteractive ?? true;
|
|
3457
|
+
if (stepState.def.isSkippable && shouldSkipStep(stepId, options, isInteractive)) {
|
|
3458
|
+
stepState.status = "skipped";
|
|
3459
|
+
verboseLog(options.verbose, `Skipping step: ${stepState.def.name}`);
|
|
3460
|
+
continue;
|
|
3461
|
+
}
|
|
3462
|
+
stepState.status = "running";
|
|
3463
|
+
stepState.startedAt = Date.now();
|
|
3464
|
+
workflow.currentStepId = stepId;
|
|
3465
|
+
verboseLog(options.verbose, `Starting step: ${stepState.def.name}`);
|
|
3466
|
+
try {
|
|
3467
|
+
const output = await executeStep(stepId, options, ctx, theme, errorHandler);
|
|
3468
|
+
stepState.status = "completed";
|
|
3469
|
+
stepState.output = output;
|
|
3470
|
+
stepState.completedAt = Date.now();
|
|
3471
|
+
if (stepId === "detect-platform" && ctx.terminalCapabilities) {
|
|
3472
|
+
theme = ThemeEngine.initialize(ctx.terminalCapabilities, {
|
|
3473
|
+
forceNoColor: options.noColor,
|
|
3474
|
+
forceColor: false
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
verboseLog(
|
|
3478
|
+
options.verbose,
|
|
3479
|
+
`Completed step: ${stepState.def.name} (${stepState.completedAt - (stepState.startedAt ?? 0)}ms)`
|
|
3480
|
+
);
|
|
3481
|
+
} catch (error) {
|
|
3482
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3483
|
+
if (err instanceof UserCancellationError) {
|
|
3484
|
+
workflow.status = "cancelled";
|
|
3485
|
+
workflow.cancellationReason = err.message;
|
|
3486
|
+
break;
|
|
3487
|
+
}
|
|
3488
|
+
const recovery = errorHandler.handleError(err, stepId);
|
|
3489
|
+
stepState.status = "failed";
|
|
3490
|
+
stepState.error = err;
|
|
3491
|
+
stepState.completedAt = Date.now();
|
|
3492
|
+
verboseLog(options.verbose, `Step failed: ${stepState.def.name} - ${err.message}`);
|
|
3493
|
+
verboseLog(options.verbose, `Recovery strategy: ${recovery.strategy}`);
|
|
3494
|
+
if (recovery.strategy === "PromptUser" && isInteractive && theme) {
|
|
3495
|
+
const errorContent = {
|
|
3496
|
+
title: "Installation Error",
|
|
3497
|
+
message: recovery.userGuidance,
|
|
3498
|
+
cause: err.message,
|
|
3499
|
+
suggestedActions: recovery.platformGuidance ? [{ step: 1, instruction: recovery.platformGuidance, command: null }] : [],
|
|
3500
|
+
retryAvailable: true,
|
|
3501
|
+
errorCode: null
|
|
3502
|
+
};
|
|
3503
|
+
const lines = renderError(errorContent, theme);
|
|
3504
|
+
for (const line of lines) {
|
|
3505
|
+
console.log(line);
|
|
3506
|
+
}
|
|
3507
|
+
const shouldRetry = await promptRetry();
|
|
3508
|
+
if (shouldRetry && errorHandler.canRetry(stepId)) {
|
|
3509
|
+
errorHandler.recordRetry(stepId);
|
|
3510
|
+
stepState.status = "running";
|
|
3511
|
+
stepState.error = null;
|
|
3512
|
+
stepState.startedAt = Date.now();
|
|
3513
|
+
stepState.completedAt = null;
|
|
3514
|
+
try {
|
|
3515
|
+
const output = await executeStep(stepId, options, ctx, theme, errorHandler);
|
|
3516
|
+
stepState.status = "completed";
|
|
3517
|
+
stepState.output = output;
|
|
3518
|
+
stepState.completedAt = Date.now();
|
|
3519
|
+
continue;
|
|
3520
|
+
} catch (retryError) {
|
|
3521
|
+
const retryErr = retryError instanceof Error ? retryError : new Error(String(retryError));
|
|
3522
|
+
stepState.status = "failed";
|
|
3523
|
+
stepState.error = retryErr;
|
|
3524
|
+
stepState.completedAt = Date.now();
|
|
3525
|
+
errorHandler.handleError(retryErr, stepId);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
workflow.status = "failed";
|
|
3529
|
+
break;
|
|
3530
|
+
}
|
|
3531
|
+
if (recovery.strategy === "RollbackAndAbort" || recovery.strategy === "AbortImmediately") {
|
|
3532
|
+
await attemptRollback(ctx, options);
|
|
3533
|
+
if (theme) {
|
|
3534
|
+
const errorContent = {
|
|
3535
|
+
title: "Installation Failed",
|
|
3536
|
+
message: recovery.userGuidance,
|
|
3537
|
+
cause: err.message,
|
|
3538
|
+
suggestedActions: recovery.platformGuidance ? [{ step: 1, instruction: recovery.platformGuidance, command: null }] : [],
|
|
3539
|
+
retryAvailable: false,
|
|
3540
|
+
errorCode: null
|
|
3541
|
+
};
|
|
3542
|
+
const lines = renderError(errorContent, theme);
|
|
3543
|
+
for (const line of lines) {
|
|
3544
|
+
console.log(line);
|
|
3545
|
+
}
|
|
3546
|
+
} else {
|
|
3547
|
+
console.error(`
|
|
3548
|
+
Error: ${err.message}`);
|
|
3549
|
+
console.error(recovery.userGuidance);
|
|
3550
|
+
}
|
|
3551
|
+
workflow.status = "failed";
|
|
3552
|
+
break;
|
|
3553
|
+
}
|
|
3554
|
+
if (recovery.strategy === "SkipAndContinue" && stepState.def.isSkippable) {
|
|
3555
|
+
stepState.status = "skipped";
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
workflow.status = "failed";
|
|
3559
|
+
break;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
if (workflow.status === "running") {
|
|
3563
|
+
workflow.status = "completed";
|
|
3564
|
+
}
|
|
3565
|
+
workflow.completedAt = Date.now();
|
|
3566
|
+
if (workflow.status === "cancelled") {
|
|
3567
|
+
await attemptRollback(ctx, options);
|
|
3568
|
+
if (theme) {
|
|
3569
|
+
promptOutro("Installation cancelled. All changes have been rolled back.");
|
|
3570
|
+
} else {
|
|
3571
|
+
console.log("\nInstallation cancelled. All changes have been rolled back.");
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
} finally {
|
|
3575
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
3576
|
+
}
|
|
3577
|
+
return errorHandler.getExitCode(workflow);
|
|
3578
|
+
}
|
|
3579
|
+
async function executeDryRun(options) {
|
|
3580
|
+
let sourceDirectory;
|
|
3581
|
+
try {
|
|
3582
|
+
sourceDirectory = await resolveSourceDirectory();
|
|
3583
|
+
} catch (error) {
|
|
3584
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
3585
|
+
return EXIT_CANCELLED;
|
|
3586
|
+
}
|
|
3587
|
+
const ctx = createInitialContext(sourceDirectory);
|
|
3588
|
+
const details = [];
|
|
3589
|
+
console.log("\n[dry-run] Simulating installation workflow...\n");
|
|
3590
|
+
try {
|
|
3591
|
+
const facade = new PlatformFacade();
|
|
3592
|
+
ctx.platformEnv = await facade.detectAll();
|
|
3593
|
+
ctx.terminalCapabilities = buildTerminalCapabilities(ctx.platformEnv);
|
|
3594
|
+
details.push({
|
|
3595
|
+
stepId: "detect-platform",
|
|
3596
|
+
stepName: "Detect Platform",
|
|
3597
|
+
result: `${ctx.platformEnv.platform.os.family} ${ctx.platformEnv.platform.os.architecture.name}`,
|
|
3598
|
+
warnings: []
|
|
3599
|
+
});
|
|
3600
|
+
} catch (error) {
|
|
3601
|
+
console.error(`Platform detection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3602
|
+
return EXIT_CANCELLED;
|
|
3603
|
+
}
|
|
3604
|
+
const prereqWarnings = [];
|
|
3605
|
+
if (!ctx.platformEnv.runtime.allRequirementsMet) {
|
|
3606
|
+
for (const err of ctx.platformEnv.runtime.errors) {
|
|
3607
|
+
prereqWarnings.push(err.message);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
details.push({
|
|
3611
|
+
stepId: "check-prerequisites",
|
|
3612
|
+
stepName: "Check Prerequisites",
|
|
3613
|
+
result: prereqWarnings.length === 0 ? "All prerequisites met" : `${prereqWarnings.length} issue(s)`,
|
|
3614
|
+
warnings: prereqWarnings
|
|
3615
|
+
});
|
|
3616
|
+
const targetDir = options.target ?? process.cwd();
|
|
3617
|
+
ctx.targetDirectory = targetDir;
|
|
3618
|
+
ctx.claudeDirectory = path6.join(targetDir, CLAUDE_DIRECTORY_NAME);
|
|
3619
|
+
details.push({
|
|
3620
|
+
stepId: "select-directory",
|
|
3621
|
+
stepName: "Select Target Directory",
|
|
3622
|
+
result: `Target: ${targetDir}`,
|
|
3623
|
+
warnings: []
|
|
3624
|
+
});
|
|
3625
|
+
try {
|
|
3626
|
+
ctx.manifest = await scanSourceDirectory(sourceDirectory);
|
|
3627
|
+
details.push({
|
|
3628
|
+
stepId: "scan-source",
|
|
3629
|
+
stepName: "Scan Source Components",
|
|
3630
|
+
result: `${ctx.manifest.totalCount} components found`,
|
|
3631
|
+
warnings: []
|
|
3632
|
+
});
|
|
3633
|
+
} catch (error) {
|
|
3634
|
+
console.error(`Source scan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3635
|
+
return EXIT_CANCELLED;
|
|
3636
|
+
}
|
|
3637
|
+
try {
|
|
3638
|
+
ctx.conflictReport = await detectConflicts(ctx.manifest, sourceDirectory, ctx.claudeDirectory);
|
|
3639
|
+
const conflictWarnings = [];
|
|
3640
|
+
if (ctx.conflictReport.hasConflicts) {
|
|
3641
|
+
conflictWarnings.push(`${ctx.conflictReport.modifiedCount} file(s) differ from source`);
|
|
3642
|
+
}
|
|
3643
|
+
details.push({
|
|
3644
|
+
stepId: "detect-conflicts",
|
|
3645
|
+
stepName: "Detect Conflicts",
|
|
3646
|
+
result: ctx.conflictReport.hasConflicts ? `${ctx.conflictReport.modifiedCount} conflict(s) detected` : "No conflicts",
|
|
3647
|
+
warnings: conflictWarnings
|
|
3648
|
+
});
|
|
3649
|
+
} catch (error) {
|
|
3650
|
+
details.push({
|
|
3651
|
+
stepId: "detect-conflicts",
|
|
3652
|
+
stepName: "Detect Conflicts",
|
|
3653
|
+
result: `Could not check: ${error instanceof Error ? error.message : "unknown"}`,
|
|
3654
|
+
warnings: []
|
|
3655
|
+
});
|
|
3656
|
+
}
|
|
3657
|
+
const report = {
|
|
3658
|
+
filesToInstall: ctx.manifest.totalCount,
|
|
3659
|
+
filesToOverwrite: ctx.conflictReport?.modifiedCount ?? 0,
|
|
3660
|
+
conflictsDetected: ctx.conflictReport?.modifiedCount ?? 0,
|
|
3661
|
+
wouldSucceed: prereqWarnings.length === 0,
|
|
3662
|
+
details
|
|
3663
|
+
};
|
|
3664
|
+
console.log("[dry-run] Installation Preview");
|
|
3665
|
+
console.log("\u2500".repeat(50));
|
|
3666
|
+
console.log(` Files to install: ${report.filesToInstall}`);
|
|
3667
|
+
console.log(` Files to overwrite: ${report.filesToOverwrite}`);
|
|
3668
|
+
console.log(` Conflicts detected: ${report.conflictsDetected}`);
|
|
3669
|
+
console.log(` Would succeed: ${report.wouldSucceed ? "yes" : "no"}`);
|
|
3670
|
+
console.log("");
|
|
3671
|
+
for (const detail of report.details) {
|
|
3672
|
+
console.log(` ${detail.stepName}: ${detail.result}`);
|
|
3673
|
+
for (const warning of detail.warnings) {
|
|
3674
|
+
console.log(` ! ${warning}`);
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
console.log("");
|
|
3678
|
+
console.log("[dry-run] No files were modified.");
|
|
3679
|
+
return EXIT_SUCCESS;
|
|
3680
|
+
}
|
|
3681
|
+
async function executeStep(stepId, options, ctx, theme, errorHandler) {
|
|
3682
|
+
switch (stepId) {
|
|
3683
|
+
case "detect-platform":
|
|
3684
|
+
return executeDetectPlatform(ctx, options);
|
|
3685
|
+
case "check-prerequisites":
|
|
3686
|
+
return executeCheckPrerequisites(ctx, options);
|
|
3687
|
+
case "show-banner":
|
|
3688
|
+
return executeShowBanner(ctx, options, theme);
|
|
3689
|
+
case "select-directory":
|
|
3690
|
+
return executeSelectDirectory(ctx, options, theme);
|
|
3691
|
+
case "scan-source":
|
|
3692
|
+
return executeScanSource(ctx, options);
|
|
3693
|
+
case "detect-conflicts":
|
|
3694
|
+
return executeDetectConflicts(ctx, options, theme);
|
|
3695
|
+
case "confirm-installation":
|
|
3696
|
+
return executeConfirmInstallation(ctx, options, theme);
|
|
3697
|
+
case "execute-installation":
|
|
3698
|
+
return executeInstallFiles(ctx, options, theme);
|
|
3699
|
+
case "validate-installation":
|
|
3700
|
+
return executeValidateInstallation(ctx, options);
|
|
3701
|
+
case "show-summary":
|
|
3702
|
+
return executeShowSummary(ctx, options, theme);
|
|
3703
|
+
default: {
|
|
3704
|
+
const _exhaustive = stepId;
|
|
3705
|
+
throw new Error(`Unknown step: ${_exhaustive}`);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
async function executeDetectPlatform(ctx, options) {
|
|
3710
|
+
const facade = new PlatformFacade();
|
|
3711
|
+
ctx.platformEnv = await facade.detectAll();
|
|
3712
|
+
ctx.terminalCapabilities = buildTerminalCapabilities(ctx.platformEnv);
|
|
3713
|
+
ctx.runtimeReport = ctx.platformEnv.runtime;
|
|
3714
|
+
const os3 = ctx.platformEnv.platform.os;
|
|
3715
|
+
return {
|
|
3716
|
+
data: { os: os3.family, arch: os3.architecture.name, version: os3.version },
|
|
3717
|
+
summary: `Platform: ${os3.family} ${os3.architecture.name} (${os3.version})`
|
|
3718
|
+
};
|
|
3719
|
+
}
|
|
3720
|
+
async function executeCheckPrerequisites(ctx, options) {
|
|
3721
|
+
const runtime = ctx.runtimeReport ?? ctx.platformEnv?.runtime;
|
|
3722
|
+
if (!runtime) {
|
|
3723
|
+
throw new Error("Platform detection must complete before prerequisite check.");
|
|
3724
|
+
}
|
|
3725
|
+
if (!runtime.allRequirementsMet) {
|
|
3726
|
+
const errorMessages = runtime.errors.map((e) => e.message);
|
|
3727
|
+
const fixMessages = runtime.errors.map((e) => e.fix);
|
|
3728
|
+
throw new Error(
|
|
3729
|
+
"Runtime requirements not met:\n" + errorMessages.map((msg, i) => ` - ${msg}
|
|
3730
|
+
Fix: ${fixMessages[i]}`).join("\n")
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
const nodeVersion = runtime.node.version.raw;
|
|
3734
|
+
const npmVersion = runtime.npm?.version.raw ?? "not detected";
|
|
3735
|
+
return {
|
|
3736
|
+
data: { nodeVersion, npmVersion },
|
|
3737
|
+
summary: `Node.js ${nodeVersion}, npm ${npmVersion}`
|
|
3738
|
+
};
|
|
3739
|
+
}
|
|
3740
|
+
async function executeShowBanner(ctx, options, theme) {
|
|
3741
|
+
if (!theme) {
|
|
3742
|
+
console.log("\n SixSevenAI - AI-DLC Framework Installer\n");
|
|
3743
|
+
return { data: {}, summary: "Banner displayed (plain)" };
|
|
3744
|
+
}
|
|
3745
|
+
const lines = renderBanner(INSTALLER_VERSION, theme);
|
|
3746
|
+
for (const line of lines) {
|
|
3747
|
+
console.log(line);
|
|
3748
|
+
}
|
|
3749
|
+
promptIntro(`SixSevenAI Installer v${INSTALLER_VERSION}`);
|
|
3750
|
+
return { data: {}, summary: "Banner displayed" };
|
|
3751
|
+
}
|
|
3752
|
+
async function executeSelectDirectory(ctx, options, theme) {
|
|
3753
|
+
if (options.target) {
|
|
3754
|
+
ctx.targetDirectory = path6.resolve(options.target);
|
|
3755
|
+
ctx.claudeDirectory = path6.join(ctx.targetDirectory, CLAUDE_DIRECTORY_NAME);
|
|
3756
|
+
return {
|
|
3757
|
+
data: { targetDirectory: ctx.targetDirectory },
|
|
3758
|
+
summary: `Target: ${ctx.targetDirectory} (from --target)`
|
|
3759
|
+
};
|
|
3760
|
+
}
|
|
3761
|
+
if (!ctx.platformEnv) {
|
|
3762
|
+
throw new Error("Platform detection must complete before directory selection.");
|
|
3763
|
+
}
|
|
3764
|
+
if (!theme) {
|
|
3765
|
+
ctx.targetDirectory = process.cwd();
|
|
3766
|
+
ctx.claudeDirectory = path6.join(ctx.targetDirectory, CLAUDE_DIRECTORY_NAME);
|
|
3767
|
+
return {
|
|
3768
|
+
data: { targetDirectory: ctx.targetDirectory },
|
|
3769
|
+
summary: `Target: ${ctx.targetDirectory} (default)`
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
const dirOptions = buildDirectoryOptions(ctx.platformEnv);
|
|
3773
|
+
const selection = await promptDirectorySelection(dirOptions, theme);
|
|
3774
|
+
ctx.targetDirectory = path6.resolve(selection.path);
|
|
3775
|
+
ctx.claudeDirectory = path6.join(ctx.targetDirectory, CLAUDE_DIRECTORY_NAME);
|
|
3776
|
+
return {
|
|
3777
|
+
data: { targetDirectory: ctx.targetDirectory, isCustom: selection.isCustom },
|
|
3778
|
+
summary: `Target: ${ctx.targetDirectory}`
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
async function executeScanSource(ctx, options) {
|
|
3782
|
+
ctx.manifest = await scanSourceDirectory(ctx.sourceDirectory);
|
|
3783
|
+
verboseLog(options.verbose, `Scanned ${ctx.manifest.totalCount} components from ${ctx.sourceDirectory}`);
|
|
3784
|
+
verboseLog(options.verbose, ` Agents: ${ctx.manifest.countsByType.agents}`);
|
|
3785
|
+
verboseLog(options.verbose, ` Skills: ${ctx.manifest.countsByType.skills}`);
|
|
3786
|
+
verboseLog(options.verbose, ` Commands: ${ctx.manifest.countsByType.commands}`);
|
|
3787
|
+
return {
|
|
3788
|
+
data: {
|
|
3789
|
+
totalCount: ctx.manifest.totalCount,
|
|
3790
|
+
countsByType: ctx.manifest.countsByType
|
|
3791
|
+
},
|
|
3792
|
+
summary: `${ctx.manifest.totalCount} components scanned`
|
|
3793
|
+
};
|
|
3794
|
+
}
|
|
3795
|
+
async function executeDetectConflicts(ctx, options, theme) {
|
|
3796
|
+
if (!ctx.manifest || !ctx.claudeDirectory) {
|
|
3797
|
+
throw new Error("Source scan and directory selection must complete before conflict detection.");
|
|
3798
|
+
}
|
|
3799
|
+
ctx.conflictReport = await detectConflicts(
|
|
3800
|
+
ctx.manifest,
|
|
3801
|
+
ctx.sourceDirectory,
|
|
3802
|
+
ctx.claudeDirectory
|
|
3803
|
+
);
|
|
3804
|
+
if (ctx.conflictReport.hasConflicts && theme && !options.force) {
|
|
3805
|
+
const shouldOverwrite = await promptOverwriteExisting(ctx.claudeDirectory);
|
|
3806
|
+
if (!shouldOverwrite) {
|
|
3807
|
+
throw new UserCancellationError("User declined to overwrite existing files.");
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
if (ctx.conflictReport.hasConflicts && !options.force) {
|
|
3811
|
+
try {
|
|
3812
|
+
ctx.backupManifest = await createBackup(
|
|
3813
|
+
ctx.claudeDirectory,
|
|
3814
|
+
ctx.conflictReport.modifiedFiles
|
|
3815
|
+
);
|
|
3816
|
+
if (ctx.backupManifest) {
|
|
3817
|
+
verboseLog(
|
|
3818
|
+
options.verbose,
|
|
3819
|
+
`Backed up ${ctx.backupManifest.files.length} files to ${ctx.backupManifest.backupDirectory}`
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
} catch (backupError) {
|
|
3823
|
+
verboseLog(options.verbose, `Backup warning: ${backupError instanceof Error ? backupError.message : String(backupError)}`);
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
return {
|
|
3827
|
+
data: {
|
|
3828
|
+
hasConflicts: ctx.conflictReport.hasConflicts,
|
|
3829
|
+
newCount: ctx.conflictReport.newCount,
|
|
3830
|
+
identicalCount: ctx.conflictReport.identicalCount,
|
|
3831
|
+
modifiedCount: ctx.conflictReport.modifiedCount
|
|
3832
|
+
},
|
|
3833
|
+
summary: ctx.conflictReport.hasConflicts ? `${ctx.conflictReport.modifiedCount} conflict(s) found` : "No conflicts"
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
async function executeConfirmInstallation(ctx, options, theme) {
|
|
3837
|
+
if (!ctx.manifest) {
|
|
3838
|
+
throw new Error("Source scan must complete before confirmation.");
|
|
3839
|
+
}
|
|
3840
|
+
const fileCount = ctx.manifest.totalCount;
|
|
3841
|
+
const target = ctx.claudeDirectory ?? ctx.targetDirectory ?? "unknown";
|
|
3842
|
+
const confirmed = await promptConfirmation(
|
|
3843
|
+
`Install ${fileCount} files to ${target}?`,
|
|
3844
|
+
true
|
|
3845
|
+
);
|
|
3846
|
+
if (!confirmed) {
|
|
3847
|
+
throw new UserCancellationError("User declined installation.");
|
|
3848
|
+
}
|
|
3849
|
+
return {
|
|
3850
|
+
data: { confirmed: true },
|
|
3851
|
+
summary: "Installation confirmed"
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
async function executeInstallFiles(ctx, options, theme) {
|
|
3855
|
+
if (!ctx.manifest || !ctx.claudeDirectory) {
|
|
3856
|
+
throw new Error("Source scan and directory selection must complete before installation.");
|
|
3857
|
+
}
|
|
3858
|
+
ctx.fileMappings = buildFileMappings(ctx.manifest, ctx.sourceDirectory, ctx.claudeDirectory);
|
|
3859
|
+
if (options.dryRun) {
|
|
3860
|
+
return {
|
|
3861
|
+
data: { dryRun: true, fileCount: ctx.fileMappings.length },
|
|
3862
|
+
summary: `[dry-run] Would install ${ctx.fileMappings.length} files`
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
let progressTracker = null;
|
|
3866
|
+
if (theme && !options.quiet) {
|
|
3867
|
+
progressTracker = new ProgressTracker();
|
|
3868
|
+
progressTracker.addStep({ id: "copy-files", label: `Installing ${ctx.fileMappings.length} files` });
|
|
3869
|
+
progressTracker.start();
|
|
3870
|
+
progressTracker.startStep("copy-files");
|
|
3871
|
+
}
|
|
3872
|
+
const result = await copyFiles(ctx.fileMappings, {
|
|
3873
|
+
concurrency: 16,
|
|
3874
|
+
onProgress: (event) => {
|
|
3875
|
+
verboseLog(
|
|
3876
|
+
options.verbose,
|
|
3877
|
+
` [${event.current}/${event.total}] ${event.fileName}`
|
|
3878
|
+
);
|
|
3879
|
+
}
|
|
3880
|
+
});
|
|
3881
|
+
ctx.installationResult = result;
|
|
3882
|
+
if (progressTracker) {
|
|
3883
|
+
if (result.success) {
|
|
3884
|
+
progressTracker.completeStep("copy-files");
|
|
3885
|
+
} else {
|
|
3886
|
+
progressTracker.failStep("copy-files", { message: "File copy failed", cause: null });
|
|
3887
|
+
}
|
|
3888
|
+
progressTracker.finish();
|
|
3889
|
+
}
|
|
3890
|
+
if (!result.success) {
|
|
3891
|
+
const errorMessages = result.errors.map((e) => e.message).join("; ");
|
|
3892
|
+
throw new Error(`File installation failed: ${errorMessages}`);
|
|
3893
|
+
}
|
|
3894
|
+
return {
|
|
3895
|
+
data: {
|
|
3896
|
+
copiedCount: result.copiedCount,
|
|
3897
|
+
skippedCount: result.skippedCount,
|
|
3898
|
+
errorCount: result.errors.length
|
|
3899
|
+
},
|
|
3900
|
+
summary: `${result.copiedCount} files installed, ${result.skippedCount} skipped`
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
async function executeValidateInstallation(ctx, options) {
|
|
3904
|
+
if (!ctx.fileMappings || ctx.fileMappings.length === 0) {
|
|
3905
|
+
return {
|
|
3906
|
+
data: { valid: true, skipped: true },
|
|
3907
|
+
summary: "Validation skipped (no files to validate)"
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
ctx.validationReport = await validateInstallation(ctx.fileMappings);
|
|
3911
|
+
if (!ctx.validationReport.valid) {
|
|
3912
|
+
const missing = ctx.validationReport.missingFiles;
|
|
3913
|
+
const invalid = ctx.validationReport.invalidFiles;
|
|
3914
|
+
throw new Error(
|
|
3915
|
+
`Installation validation failed: ${missing.length} missing file(s), ${invalid.length} invalid file(s)`
|
|
3916
|
+
);
|
|
3917
|
+
}
|
|
3918
|
+
return {
|
|
3919
|
+
data: {
|
|
3920
|
+
valid: true,
|
|
3921
|
+
totalChecked: ctx.validationReport.totalChecked,
|
|
3922
|
+
validCount: ctx.validationReport.validCount
|
|
3923
|
+
},
|
|
3924
|
+
summary: `${ctx.validationReport.validCount} files validated`
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
async function executeShowSummary(ctx, options, theme) {
|
|
3928
|
+
const elapsed = Date.now() - (ctx.installationResult ? Date.now() : Date.now());
|
|
3929
|
+
const summary = {
|
|
3930
|
+
success: ctx.installationResult?.success ?? true,
|
|
3931
|
+
targetDirectory: ctx.targetDirectory ?? "unknown",
|
|
3932
|
+
componentCounts: {
|
|
3933
|
+
agents: ctx.manifest?.countsByType.agents ?? 0,
|
|
3934
|
+
skills: ctx.manifest?.countsByType.skills ?? 0,
|
|
3935
|
+
commands: ctx.manifest?.countsByType.commands ?? 0,
|
|
3936
|
+
cliFiles: ctx.manifest?.countsByType.cli ?? 0,
|
|
3937
|
+
other: (ctx.manifest?.countsByType.workflows ?? 0) + (ctx.manifest?.countsByType.metadata ?? 0)
|
|
3938
|
+
},
|
|
3939
|
+
totalDuration: formatDuration(0),
|
|
3940
|
+
// Will be updated by caller
|
|
3941
|
+
totalFilesCopied: ctx.installationResult?.copiedCount ?? 0,
|
|
3942
|
+
totalFilesSkipped: ctx.installationResult?.skippedCount ?? 0,
|
|
3943
|
+
totalFilesFailed: ctx.installationResult?.errors.length ?? 0,
|
|
3944
|
+
installerVersion: INSTALLER_VERSION
|
|
3945
|
+
};
|
|
3946
|
+
const nextSteps = getDefaultNextSteps(summary.success);
|
|
3947
|
+
if (theme && !options.quiet) {
|
|
3948
|
+
const lines = renderSummary(summary, nextSteps, theme);
|
|
3949
|
+
for (const line of lines) {
|
|
3950
|
+
console.log(line);
|
|
3951
|
+
}
|
|
3952
|
+
if (summary.success) {
|
|
3953
|
+
promptOutro("Installation complete!");
|
|
3954
|
+
}
|
|
3955
|
+
} else if (!options.quiet) {
|
|
3956
|
+
console.log("\n--- Installation Summary ---");
|
|
3957
|
+
console.log(` Status: ${summary.success ? "Success" : "Failed"}`);
|
|
3958
|
+
console.log(` Files installed: ${summary.totalFilesCopied}`);
|
|
3959
|
+
console.log(` Files skipped: ${summary.totalFilesSkipped}`);
|
|
3960
|
+
console.log(` Target: ${summary.targetDirectory}`);
|
|
3961
|
+
if (summary.success) {
|
|
3962
|
+
console.log("\n Next steps:");
|
|
3963
|
+
for (const step of nextSteps) {
|
|
3964
|
+
console.log(` ${step.order}. ${step.title}`);
|
|
3965
|
+
if (step.command) {
|
|
3966
|
+
console.log(` $ ${step.command}`);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
return {
|
|
3972
|
+
data: { success: summary.success },
|
|
3973
|
+
summary: summary.success ? "Installation successful" : "Installation completed with errors"
|
|
3974
|
+
};
|
|
3975
|
+
}
|
|
3976
|
+
async function attemptRollback(ctx, options) {
|
|
3977
|
+
if (!ctx.backupManifest || !ctx.claudeDirectory) {
|
|
3978
|
+
verboseLog(options.verbose, "No backup available for rollback.");
|
|
3979
|
+
return;
|
|
3980
|
+
}
|
|
3981
|
+
try {
|
|
3982
|
+
verboseLog(options.verbose, "Rolling back installation...");
|
|
3983
|
+
const rollbackResult = await restoreFromBackup(ctx.backupManifest, ctx.claudeDirectory);
|
|
3984
|
+
if (rollbackResult.success) {
|
|
3985
|
+
verboseLog(options.verbose, `Rollback complete: ${rollbackResult.restoredCount} files restored.`);
|
|
3986
|
+
} else {
|
|
3987
|
+
const errorMessages = rollbackResult.errors.map((e) => e.message).join("; ");
|
|
3988
|
+
console.error(`Rollback encountered errors: ${errorMessages}`);
|
|
3989
|
+
}
|
|
3990
|
+
} catch (error) {
|
|
3991
|
+
console.error(
|
|
3992
|
+
`Rollback failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3993
|
+
);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
// src/index.ts
|
|
3998
|
+
async function runInstaller(options = {}) {
|
|
3999
|
+
const cliOptions = {
|
|
4000
|
+
command: "install",
|
|
4001
|
+
target: options.target ?? null,
|
|
4002
|
+
force: options.force ?? false,
|
|
4003
|
+
dryRun: options.dryRun ?? false,
|
|
4004
|
+
verbose: options.verbose ?? false,
|
|
4005
|
+
noColor: options.noColor ?? false,
|
|
4006
|
+
quiet: options.quiet ?? false,
|
|
4007
|
+
rawArgs: []
|
|
4008
|
+
};
|
|
4009
|
+
let exitCode;
|
|
4010
|
+
if (cliOptions.dryRun) {
|
|
4011
|
+
exitCode = await executeDryRun(cliOptions);
|
|
4012
|
+
} else {
|
|
4013
|
+
exitCode = await executeInstallation(cliOptions);
|
|
4014
|
+
}
|
|
4015
|
+
return {
|
|
4016
|
+
success: exitCode === EXIT_SUCCESS,
|
|
4017
|
+
filesWritten: 0,
|
|
4018
|
+
// Detailed counts are in the workflow output
|
|
4019
|
+
summary: exitCode === EXIT_SUCCESS ? "Installation completed successfully." : exitCode === 2 ? "Installation cancelled by user." : "Installation failed.",
|
|
4020
|
+
exitCode
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
// src/cli.ts
|
|
4025
|
+
var CLI_NAME = "sixsevenai";
|
|
4026
|
+
var KNOWN_FLAGS = [
|
|
4027
|
+
"--version",
|
|
4028
|
+
"-v",
|
|
4029
|
+
"--help",
|
|
4030
|
+
"-h",
|
|
4031
|
+
"--verbose",
|
|
4032
|
+
"--dry-run",
|
|
4033
|
+
"-d",
|
|
4034
|
+
"--no-color",
|
|
4035
|
+
"--force",
|
|
4036
|
+
"-f",
|
|
4037
|
+
"--quiet",
|
|
4038
|
+
"-q",
|
|
4039
|
+
"--target",
|
|
4040
|
+
"-t"
|
|
4041
|
+
];
|
|
4042
|
+
function parseArgs(argv) {
|
|
4043
|
+
const args = argv.slice(2);
|
|
4044
|
+
const filtered = args[0] === "install" ? args.slice(1) : args;
|
|
4045
|
+
let target = null;
|
|
4046
|
+
const version2 = filtered.includes("--version") || filtered.includes("-v");
|
|
4047
|
+
const help = filtered.includes("--help") || filtered.includes("-h");
|
|
4048
|
+
const verbose = filtered.includes("--verbose");
|
|
4049
|
+
const dryRun = filtered.includes("--dry-run") || filtered.includes("-d");
|
|
4050
|
+
const noColor = filtered.includes("--no-color");
|
|
4051
|
+
const force = filtered.includes("--force") || filtered.includes("-f");
|
|
4052
|
+
const quiet = filtered.includes("--quiet") || filtered.includes("-q");
|
|
4053
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
4054
|
+
const arg = filtered[i];
|
|
4055
|
+
if ((arg === "--target" || arg === "-t") && i + 1 < filtered.length) {
|
|
4056
|
+
target = filtered[i + 1];
|
|
4057
|
+
i++;
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
if (target === null) {
|
|
4061
|
+
for (const arg of filtered) {
|
|
4062
|
+
if (!arg.startsWith("-") && arg !== "install") {
|
|
4063
|
+
target = arg;
|
|
4064
|
+
break;
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
if (verbose && quiet) {
|
|
4069
|
+
console.error("Error: Cannot use --verbose and --quiet together.");
|
|
4070
|
+
process.exit(1);
|
|
4071
|
+
}
|
|
4072
|
+
for (const arg of filtered) {
|
|
4073
|
+
if (arg.startsWith("-") && !KNOWN_FLAGS.includes(arg) && arg !== target) {
|
|
4074
|
+
const prevIdx = filtered.indexOf(arg) - 1;
|
|
4075
|
+
if (prevIdx >= 0 && (filtered[prevIdx] === "--target" || filtered[prevIdx] === "-t")) {
|
|
4076
|
+
continue;
|
|
4077
|
+
}
|
|
4078
|
+
const suggestion = findClosestFlag(arg);
|
|
4079
|
+
const suggestionText = suggestion ? ` Did you mean ${suggestion}?` : "";
|
|
4080
|
+
console.error(`Error: Unknown flag: ${arg}.${suggestionText}`);
|
|
4081
|
+
process.exit(1);
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
return { version: version2, help, verbose, dryRun, noColor, force, quiet, target };
|
|
4085
|
+
}
|
|
4086
|
+
function findClosestFlag(input) {
|
|
4087
|
+
let bestMatch = null;
|
|
4088
|
+
let bestDistance = Infinity;
|
|
4089
|
+
for (const flag of KNOWN_FLAGS) {
|
|
4090
|
+
const dist = levenshteinDistance(input, flag);
|
|
4091
|
+
if (dist < bestDistance && dist <= 3) {
|
|
4092
|
+
bestDistance = dist;
|
|
4093
|
+
bestMatch = flag;
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
return bestMatch;
|
|
4097
|
+
}
|
|
4098
|
+
function levenshteinDistance(a, b) {
|
|
4099
|
+
const m = a.length;
|
|
4100
|
+
const n = b.length;
|
|
4101
|
+
const dp = Array.from(
|
|
4102
|
+
{ length: m + 1 },
|
|
4103
|
+
() => Array.from({ length: n + 1 }, () => 0)
|
|
4104
|
+
);
|
|
4105
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
4106
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
4107
|
+
for (let i = 1; i <= m; i++) {
|
|
4108
|
+
for (let j = 1; j <= n; j++) {
|
|
4109
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
4110
|
+
dp[i][j] = Math.min(
|
|
4111
|
+
dp[i - 1][j] + 1,
|
|
4112
|
+
dp[i][j - 1] + 1,
|
|
4113
|
+
dp[i - 1][j - 1] + cost
|
|
4114
|
+
);
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
return dp[m][n];
|
|
4118
|
+
}
|
|
4119
|
+
function printHelp() {
|
|
4120
|
+
const help = `
|
|
4121
|
+
Usage: ${CLI_NAME} install [target-directory] [options]
|
|
4122
|
+
|
|
4123
|
+
Interactive terminal installer for the AI-DLC framework.
|
|
4124
|
+
|
|
4125
|
+
Arguments:
|
|
4126
|
+
target-directory Target project directory (default: current directory)
|
|
4127
|
+
|
|
4128
|
+
Options:
|
|
4129
|
+
--target, -t <dir> Target project directory (overrides positional arg)
|
|
4130
|
+
--force, -f Overwrite existing files without prompting
|
|
4131
|
+
--dry-run, -d Preview changes without writing to disk
|
|
4132
|
+
--verbose Enable verbose logging to stderr
|
|
4133
|
+
--no-color Disable colored output (also respects NO_COLOR env)
|
|
4134
|
+
--quiet, -q Suppress non-error output (mutually exclusive with --verbose)
|
|
4135
|
+
-h, --help Show this help message
|
|
4136
|
+
-v, --version Show version number
|
|
4137
|
+
|
|
4138
|
+
Exit Codes:
|
|
4139
|
+
0 Installation completed successfully
|
|
4140
|
+
1 Installation failed
|
|
4141
|
+
2 Installation cancelled by user
|
|
4142
|
+
|
|
4143
|
+
Examples:
|
|
4144
|
+
${CLI_NAME} install
|
|
4145
|
+
${CLI_NAME} install --target /home/user/my-project --force
|
|
4146
|
+
${CLI_NAME} install --dry-run --verbose
|
|
4147
|
+
${CLI_NAME} install /path/to/project --dry-run > preview.txt
|
|
4148
|
+
`;
|
|
4149
|
+
console.log(help.trim());
|
|
4150
|
+
}
|
|
4151
|
+
async function main() {
|
|
4152
|
+
const flags = parseArgs(process.argv);
|
|
4153
|
+
if (flags.version) {
|
|
4154
|
+
console.log(`${CLI_NAME} v${INSTALLER_VERSION}`);
|
|
4155
|
+
return;
|
|
4156
|
+
}
|
|
4157
|
+
if (flags.help) {
|
|
4158
|
+
printHelp();
|
|
4159
|
+
return;
|
|
4160
|
+
}
|
|
4161
|
+
if (flags.noColor) {
|
|
4162
|
+
process.env["NO_COLOR"] = "1";
|
|
4163
|
+
}
|
|
4164
|
+
const options = {
|
|
4165
|
+
verbose: flags.verbose,
|
|
4166
|
+
dryRun: flags.dryRun,
|
|
4167
|
+
force: flags.force,
|
|
4168
|
+
quiet: flags.quiet,
|
|
4169
|
+
noColor: flags.noColor,
|
|
4170
|
+
target: flags.target ?? void 0
|
|
4171
|
+
};
|
|
4172
|
+
const result = await runInstaller(options);
|
|
4173
|
+
process.exit(result.exitCode);
|
|
4174
|
+
}
|
|
4175
|
+
process.title = CLI_NAME;
|
|
4176
|
+
main().catch((error) => {
|
|
4177
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4178
|
+
console.error(`
|
|
4179
|
+
Fatal error: ${message}`);
|
|
4180
|
+
if (error instanceof Error && error.stack && process.env["DEBUG"]) {
|
|
4181
|
+
console.error(error.stack);
|
|
4182
|
+
}
|
|
4183
|
+
process.exit(1);
|
|
4184
|
+
});
|
|
4185
|
+
//# sourceMappingURL=cli.js.map
|