legion-cc 0.1.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/LICENSE +21 -0
- package/README.md +269 -0
- package/VERSION +1 -0
- package/agents/legion-orchestrator.md +95 -0
- package/bin/install.js +898 -0
- package/bin/legion-tools.cjs +421 -0
- package/bin/lib/config.cjs +141 -0
- package/bin/lib/core.cjs +216 -0
- package/bin/lib/domain.cjs +107 -0
- package/bin/lib/init.cjs +184 -0
- package/bin/lib/session.cjs +140 -0
- package/bin/lib/state.cjs +280 -0
- package/commands/legion/devops/architect.md +44 -0
- package/commands/legion/devops/build.md +52 -0
- package/commands/legion/devops/cycle.md +52 -0
- package/commands/legion/devops/execute.md +52 -0
- package/commands/legion/devops/plan.md +51 -0
- package/commands/legion/devops/quick.md +45 -0
- package/commands/legion/devops/review.md +52 -0
- package/commands/legion/resume.md +52 -0
- package/commands/legion/status.md +53 -0
- package/hooks/legion-context-monitor.js +180 -0
- package/hooks/legion-statusline.js +191 -0
- package/package.json +48 -0
- package/references/agent-routing.md +64 -0
- package/references/devops/agent-map.md +61 -0
- package/references/devops/pipeline-patterns.md +87 -0
- package/references/domain-registry.md +63 -0
- package/references/ui-brand.md +102 -0
- package/templates/config.json +25 -0
- package/templates/devops/architect-output.md +28 -0
- package/templates/devops/execution-report.md +23 -0
- package/templates/devops/plan-output.md +33 -0
- package/templates/devops/review-checklist.md +35 -0
- package/templates/session.md +17 -0
- package/templates/state.md +17 -0
- package/templates/task-record.md +19 -0
- package/workflows/core/completion.md +70 -0
- package/workflows/core/context-load.md +57 -0
- package/workflows/core/init.md +52 -0
- package/workflows/devops/architect.md +91 -0
- package/workflows/devops/build.md +92 -0
- package/workflows/devops/cycle.md +237 -0
- package/workflows/devops/execute.md +118 -0
- package/workflows/devops/plan.md +108 -0
- package/workflows/devops/quick.md +107 -0
- package/workflows/devops/review.md +112 -0
- package/workflows/resume.md +88 -0
- package/workflows/status.md +72 -0
package/bin/install.js
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Legion Installer — bin/install.js
|
|
4
|
+
// Entry point for `npx legion-cc@latest`
|
|
5
|
+
// Copies Legion framework files into ~/.claude/ (or custom config dir)
|
|
6
|
+
//
|
|
7
|
+
// CommonJS, no external dependencies — only Node.js built-ins.
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
// ─── Colors ──────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const bold = '\x1b[1m';
|
|
19
|
+
const dim = '\x1b[2m';
|
|
20
|
+
const red = '\x1b[31m';
|
|
21
|
+
const green = '\x1b[32m';
|
|
22
|
+
const yellow = '\x1b[33m';
|
|
23
|
+
const cyan = '\x1b[36m';
|
|
24
|
+
const reset = '\x1b[0m';
|
|
25
|
+
|
|
26
|
+
// Disable colors when not a TTY
|
|
27
|
+
const isTTY = process.stdout.isTTY;
|
|
28
|
+
function c(color, text) {
|
|
29
|
+
return isTTY ? `${color}${text}${reset}` : text;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Read version ────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const srcDir = path.resolve(__dirname, '..');
|
|
35
|
+
const versionFile = path.join(srcDir, 'VERSION');
|
|
36
|
+
let version = '0.0.0';
|
|
37
|
+
try {
|
|
38
|
+
version = fs.readFileSync(versionFile, 'utf8').trim();
|
|
39
|
+
} catch (_) {
|
|
40
|
+
// Try package.json as fallback
|
|
41
|
+
try {
|
|
42
|
+
const pkg = require(path.join(srcDir, 'package.json'));
|
|
43
|
+
version = pkg.version || '0.0.0';
|
|
44
|
+
} catch (_2) {
|
|
45
|
+
// Keep default
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Parse CLI flags ─────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const args = process.argv.slice(2);
|
|
52
|
+
|
|
53
|
+
function hasFlag(name) {
|
|
54
|
+
return args.includes(name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getFlagValue(name) {
|
|
58
|
+
const idx = args.indexOf(name);
|
|
59
|
+
if (idx !== -1 && idx + 1 < args.length) {
|
|
60
|
+
const val = args[idx + 1];
|
|
61
|
+
if (!val.startsWith('-')) return val;
|
|
62
|
+
}
|
|
63
|
+
// Also handle --flag=value
|
|
64
|
+
const eqArg = args.find(a => a.startsWith(name + '='));
|
|
65
|
+
if (eqArg) {
|
|
66
|
+
const val = eqArg.split('=').slice(1).join('=');
|
|
67
|
+
if (val) return val;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const flagForce = hasFlag('--force');
|
|
73
|
+
const flagDryRun = hasFlag('--dry-run');
|
|
74
|
+
const flagUninstall = hasFlag('--uninstall');
|
|
75
|
+
const flagHelp = hasFlag('--help') || hasFlag('-h');
|
|
76
|
+
const flagConfigDir = getFlagValue('--config-dir');
|
|
77
|
+
|
|
78
|
+
// ─── Help ────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
if (flagHelp) {
|
|
81
|
+
console.log(`
|
|
82
|
+
${c(bold + cyan, 'Legion')} ${c(dim, 'v' + version)} — Installer
|
|
83
|
+
|
|
84
|
+
${c(yellow, 'Usage:')} npx legion-cc [options]
|
|
85
|
+
|
|
86
|
+
${c(yellow, 'Options:')}
|
|
87
|
+
${c(cyan, '--force')} Overwrite existing agent files
|
|
88
|
+
${c(cyan, '--dry-run')} Show what would happen without doing it
|
|
89
|
+
${c(cyan, '--uninstall')} Remove Legion from ~/.claude/
|
|
90
|
+
${c(cyan, '--config-dir <path>')} Custom config directory (default: ~/.claude)
|
|
91
|
+
${c(cyan, '--help, -h')} Show this help message
|
|
92
|
+
|
|
93
|
+
${c(yellow, 'Examples:')}
|
|
94
|
+
${c(dim, '# Install Legion')}
|
|
95
|
+
npx legion-cc
|
|
96
|
+
|
|
97
|
+
${c(dim, '# Install with force-overwrite of agents')}
|
|
98
|
+
npx legion-cc --force
|
|
99
|
+
|
|
100
|
+
${c(dim, '# Preview without changes')}
|
|
101
|
+
npx legion-cc --dry-run
|
|
102
|
+
|
|
103
|
+
${c(dim, '# Uninstall')}
|
|
104
|
+
npx legion-cc --uninstall
|
|
105
|
+
|
|
106
|
+
${c(yellow, 'Environment:')}
|
|
107
|
+
CLAUDE_CONFIG_DIR Override config directory (same as --config-dir)
|
|
108
|
+
`);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Determine paths ─────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR
|
|
115
|
+
|| flagConfigDir
|
|
116
|
+
|| path.join(os.homedir(), '.claude');
|
|
117
|
+
|
|
118
|
+
// ─── Banner ──────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function printBanner() {
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(c(bold + cyan, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
123
|
+
console.log(c(bold + cyan, ' LEGION') + ' ' + c(dim, 'v' + version) + c(bold + cyan, ' — Installer'));
|
|
124
|
+
console.log(c(bold + cyan, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
125
|
+
console.log('');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
printBanner();
|
|
129
|
+
|
|
130
|
+
if (flagDryRun) {
|
|
131
|
+
console.log(c(yellow, ' [DRY RUN] No files will be written.'));
|
|
132
|
+
console.log('');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Logging helpers ─────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function logStep(msg) {
|
|
138
|
+
console.log(` ${c(bold + cyan, '==>') } ${msg}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function logOk(msg) {
|
|
142
|
+
console.log(` ${c(green, '+')} ${msg}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function logSkip(msg) {
|
|
146
|
+
console.log(` ${c(dim, '-')} ${msg} ${c(dim, '(skipped)')}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function logWarn(msg) {
|
|
150
|
+
console.log(` ${c(yellow, '!')} ${msg}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function logErr(msg) {
|
|
154
|
+
console.error(` ${c(red, 'ERROR:')} ${msg}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Counters ────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
const counts = {
|
|
160
|
+
commands: 0,
|
|
161
|
+
workflows: 0,
|
|
162
|
+
templates: 0,
|
|
163
|
+
references: 0,
|
|
164
|
+
hooks: 0,
|
|
165
|
+
agents: 0,
|
|
166
|
+
bin: 0,
|
|
167
|
+
other: 0,
|
|
168
|
+
};
|
|
169
|
+
let totalFiles = 0;
|
|
170
|
+
let skippedFiles = 0;
|
|
171
|
+
|
|
172
|
+
// ─── File utility helpers ────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Recursively count files in a directory.
|
|
176
|
+
*/
|
|
177
|
+
function countFilesInDir(dir) {
|
|
178
|
+
let count = 0;
|
|
179
|
+
if (!fs.existsSync(dir)) return 0;
|
|
180
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
const full = path.join(dir, entry.name);
|
|
183
|
+
if (entry.isDirectory()) {
|
|
184
|
+
count += countFilesInDir(full);
|
|
185
|
+
} else if (entry.isFile()) {
|
|
186
|
+
count++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return count;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Compute SHA256 hex digest of a file.
|
|
194
|
+
*/
|
|
195
|
+
function sha256File(filePath) {
|
|
196
|
+
const data = fs.readFileSync(filePath);
|
|
197
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Recursively walk a directory and return all file paths.
|
|
202
|
+
*/
|
|
203
|
+
function walkDir(dir, fileList) {
|
|
204
|
+
fileList = fileList || [];
|
|
205
|
+
if (!fs.existsSync(dir)) return fileList;
|
|
206
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
const full = path.join(dir, entry.name);
|
|
209
|
+
if (entry.isDirectory()) {
|
|
210
|
+
walkDir(full, fileList);
|
|
211
|
+
} else if (entry.isFile()) {
|
|
212
|
+
fileList.push(full);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return fileList;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Ensure a directory exists (create recursively if needed).
|
|
220
|
+
*/
|
|
221
|
+
function ensureDir(dir) {
|
|
222
|
+
if (flagDryRun) return;
|
|
223
|
+
try {
|
|
224
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
225
|
+
} catch (err) {
|
|
226
|
+
if (err.code !== 'EEXIST') {
|
|
227
|
+
logWarn(`Could not create directory ${dir}: ${err.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Copy a single file. Creates parent directories as needed.
|
|
234
|
+
*/
|
|
235
|
+
function copyFile(src, dest) {
|
|
236
|
+
if (flagDryRun) {
|
|
237
|
+
console.log(` ${c(dim, '[dry-run]')} cp ${src} -> ${dest}`);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
ensureDir(path.dirname(dest));
|
|
242
|
+
fs.copyFileSync(src, dest);
|
|
243
|
+
return true;
|
|
244
|
+
} catch (err) {
|
|
245
|
+
logWarn(`Could not copy ${src} -> ${dest}: ${err.message}`);
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Copy a directory recursively using fs.cpSync.
|
|
252
|
+
* Returns the number of files copied.
|
|
253
|
+
*/
|
|
254
|
+
function copyDir(src, dest) {
|
|
255
|
+
if (!fs.existsSync(src)) {
|
|
256
|
+
logWarn(`Source directory not found: ${src}`);
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const fileCount = countFilesInDir(src);
|
|
261
|
+
|
|
262
|
+
if (flagDryRun) {
|
|
263
|
+
console.log(` ${c(dim, '[dry-run]')} cpSync ${src}/ -> ${dest}/ (${fileCount} files)`);
|
|
264
|
+
return fileCount;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
ensureDir(dest);
|
|
269
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
270
|
+
return fileCount;
|
|
271
|
+
} catch (err) {
|
|
272
|
+
logWarn(`Could not copy directory ${src} -> ${dest}: ${err.message}`);
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Read and parse a JSON file, returning null on failure.
|
|
279
|
+
*/
|
|
280
|
+
function readJSON(filePath) {
|
|
281
|
+
try {
|
|
282
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
283
|
+
return JSON.parse(raw);
|
|
284
|
+
} catch (_) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Write a JSON file with pretty formatting.
|
|
291
|
+
*/
|
|
292
|
+
function writeJSON(filePath, data) {
|
|
293
|
+
if (flagDryRun) {
|
|
294
|
+
console.log(` ${c(dim, '[dry-run]')} write ${filePath}`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
ensureDir(path.dirname(filePath));
|
|
299
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
300
|
+
} catch (err) {
|
|
301
|
+
logWarn(`Could not write ${filePath}: ${err.message}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── Check prerequisites ────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
function checkPrerequisites() {
|
|
308
|
+
logStep('Checking prerequisites...');
|
|
309
|
+
|
|
310
|
+
// Node.js >= 18
|
|
311
|
+
const nodeVersion = process.versions.node;
|
|
312
|
+
const major = parseInt(nodeVersion.split('.')[0], 10);
|
|
313
|
+
if (major < 18) {
|
|
314
|
+
logErr(`Node.js >= 18 is required (found v${nodeVersion}).`);
|
|
315
|
+
logErr('Install from https://nodejs.org');
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
logOk(`Node.js v${nodeVersion}`);
|
|
319
|
+
|
|
320
|
+
// configDir — create if not exists
|
|
321
|
+
if (!fs.existsSync(configDir)) {
|
|
322
|
+
logWarn(`Config directory ${configDir} does not exist, creating...`);
|
|
323
|
+
ensureDir(configDir);
|
|
324
|
+
if (!flagDryRun && !fs.existsSync(configDir)) {
|
|
325
|
+
logErr(`Could not create config directory: ${configDir}`);
|
|
326
|
+
logErr('Please install Claude Code first: https://claude.ai/download');
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
logOk(`Config directory: ${configDir}`);
|
|
331
|
+
|
|
332
|
+
console.log('');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ─── Install logic ──────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
function doInstall() {
|
|
338
|
+
checkPrerequisites();
|
|
339
|
+
|
|
340
|
+
// ── Step 1: Copy files ──────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
// commands/legion/ -> commands/legion/
|
|
343
|
+
logStep('Copying commands...');
|
|
344
|
+
const commandsSrc = path.join(srcDir, 'commands', 'legion');
|
|
345
|
+
const commandsDest = path.join(configDir, 'commands', 'legion');
|
|
346
|
+
const commandCount = copyDir(commandsSrc, commandsDest);
|
|
347
|
+
counts.commands = commandCount;
|
|
348
|
+
totalFiles += commandCount;
|
|
349
|
+
logOk(`${commandCount} command files`);
|
|
350
|
+
|
|
351
|
+
// workflows/ -> legion/workflows/
|
|
352
|
+
logStep('Copying workflows...');
|
|
353
|
+
const workflowsSrc = path.join(srcDir, 'workflows');
|
|
354
|
+
const workflowsDest = path.join(configDir, 'legion', 'workflows');
|
|
355
|
+
const workflowCount = copyDir(workflowsSrc, workflowsDest);
|
|
356
|
+
counts.workflows = workflowCount;
|
|
357
|
+
totalFiles += workflowCount;
|
|
358
|
+
logOk(`${workflowCount} workflow files`);
|
|
359
|
+
|
|
360
|
+
// templates/ -> legion/templates/
|
|
361
|
+
logStep('Copying templates...');
|
|
362
|
+
const templatesSrc = path.join(srcDir, 'templates');
|
|
363
|
+
const templatesDest = path.join(configDir, 'legion', 'templates');
|
|
364
|
+
const templateCount = copyDir(templatesSrc, templatesDest);
|
|
365
|
+
counts.templates = templateCount;
|
|
366
|
+
totalFiles += templateCount;
|
|
367
|
+
logOk(`${templateCount} template files`);
|
|
368
|
+
|
|
369
|
+
// references/ -> legion/references/
|
|
370
|
+
logStep('Copying references...');
|
|
371
|
+
const referencesSrc = path.join(srcDir, 'references');
|
|
372
|
+
const referencesDest = path.join(configDir, 'legion', 'references');
|
|
373
|
+
const referenceCount = copyDir(referencesSrc, referencesDest);
|
|
374
|
+
counts.references = referenceCount;
|
|
375
|
+
totalFiles += referenceCount;
|
|
376
|
+
logOk(`${referenceCount} reference files`);
|
|
377
|
+
|
|
378
|
+
// bin/legion-tools.cjs -> legion/bin/legion-tools.cjs
|
|
379
|
+
logStep('Copying bin...');
|
|
380
|
+
const toolsSrc = path.join(srcDir, 'bin', 'legion-tools.cjs');
|
|
381
|
+
const toolsDest = path.join(configDir, 'legion', 'bin', 'legion-tools.cjs');
|
|
382
|
+
if (copyFile(toolsSrc, toolsDest)) {
|
|
383
|
+
counts.bin++;
|
|
384
|
+
totalFiles++;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// bin/lib/ -> legion/bin/lib/
|
|
388
|
+
const libSrc = path.join(srcDir, 'bin', 'lib');
|
|
389
|
+
const libDest = path.join(configDir, 'legion', 'bin', 'lib');
|
|
390
|
+
const libCount = copyDir(libSrc, libDest);
|
|
391
|
+
counts.bin += libCount;
|
|
392
|
+
totalFiles += libCount;
|
|
393
|
+
logOk(`${counts.bin} bin files`);
|
|
394
|
+
|
|
395
|
+
// VERSION -> legion/VERSION
|
|
396
|
+
logStep('Copying VERSION...');
|
|
397
|
+
const versionSrc = path.join(srcDir, 'VERSION');
|
|
398
|
+
const versionDest = path.join(configDir, 'legion', 'VERSION');
|
|
399
|
+
if (copyFile(versionSrc, versionDest)) {
|
|
400
|
+
counts.other++;
|
|
401
|
+
totalFiles++;
|
|
402
|
+
}
|
|
403
|
+
logOk('VERSION');
|
|
404
|
+
|
|
405
|
+
// hooks/legion-context-monitor.js -> hooks/legion-context-monitor.js
|
|
406
|
+
// hooks/legion-statusline.js -> hooks/legion-statusline.js
|
|
407
|
+
logStep('Copying hooks...');
|
|
408
|
+
const hookFiles = ['legion-context-monitor.js', 'legion-statusline.js'];
|
|
409
|
+
for (const hookName of hookFiles) {
|
|
410
|
+
const hookSrc = path.join(srcDir, 'hooks', hookName);
|
|
411
|
+
const hookDest = path.join(configDir, 'hooks', hookName);
|
|
412
|
+
if (fs.existsSync(hookSrc)) {
|
|
413
|
+
if (copyFile(hookSrc, hookDest)) {
|
|
414
|
+
counts.hooks++;
|
|
415
|
+
totalFiles++;
|
|
416
|
+
logOk(hookName);
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
logWarn(`Hook source not found: ${hookSrc}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// agents/legion-orchestrator.md -> agents/legion-orchestrator.md (skip if exists unless --force)
|
|
424
|
+
logStep('Copying agents...');
|
|
425
|
+
const agentsSrc = path.join(srcDir, 'agents');
|
|
426
|
+
if (fs.existsSync(agentsSrc)) {
|
|
427
|
+
const agentFiles = fs.readdirSync(agentsSrc).filter(f => f.endsWith('.md'));
|
|
428
|
+
for (const agentFile of agentFiles) {
|
|
429
|
+
const src = path.join(agentsSrc, agentFile);
|
|
430
|
+
const dest = path.join(configDir, 'agents', agentFile);
|
|
431
|
+
|
|
432
|
+
if (fs.existsSync(dest) && !flagForce) {
|
|
433
|
+
logSkip(`agents/${agentFile} already exists (use --force to overwrite)`);
|
|
434
|
+
skippedFiles++;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (copyFile(src, dest)) {
|
|
439
|
+
counts.agents++;
|
|
440
|
+
totalFiles++;
|
|
441
|
+
logOk(`agents/${agentFile}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── Step 2: Make hooks executable ────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
logStep('Making hooks executable...');
|
|
449
|
+
for (const hookName of hookFiles) {
|
|
450
|
+
const hookPath = path.join(configDir, 'hooks', hookName);
|
|
451
|
+
if (flagDryRun) {
|
|
452
|
+
console.log(` ${c(dim, '[dry-run]')} chmod 755 ${hookPath}`);
|
|
453
|
+
} else {
|
|
454
|
+
try {
|
|
455
|
+
if (fs.existsSync(hookPath)) {
|
|
456
|
+
fs.chmodSync(hookPath, 0o755);
|
|
457
|
+
logOk(`chmod 755 ${hookName}`);
|
|
458
|
+
}
|
|
459
|
+
} catch (err) {
|
|
460
|
+
logWarn(`Could not chmod ${hookPath}: ${err.message}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── Step 3: Register hooks in settings ──────────────────────────────────
|
|
466
|
+
|
|
467
|
+
logStep('Registering hooks in settings...');
|
|
468
|
+
registerHooks();
|
|
469
|
+
|
|
470
|
+
// ── Step 4: Generate manifest ───────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
logStep('Generating file manifest...');
|
|
473
|
+
generateManifest();
|
|
474
|
+
|
|
475
|
+
// ── Step 5: Print summary ───────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
printSummary();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ─── Register hooks in settings.json ─────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
function registerHooks() {
|
|
483
|
+
// Determine settings file
|
|
484
|
+
let settingsPath = null;
|
|
485
|
+
const settingsLocalPath = path.join(configDir, 'settings.local.json');
|
|
486
|
+
const settingsJsonPath = path.join(configDir, 'settings.json');
|
|
487
|
+
|
|
488
|
+
if (fs.existsSync(settingsLocalPath)) {
|
|
489
|
+
settingsPath = settingsLocalPath;
|
|
490
|
+
} else if (fs.existsSync(settingsJsonPath)) {
|
|
491
|
+
settingsPath = settingsJsonPath;
|
|
492
|
+
} else {
|
|
493
|
+
// No settings file exists — create settings.local.json
|
|
494
|
+
settingsPath = settingsLocalPath;
|
|
495
|
+
if (!flagDryRun) {
|
|
496
|
+
writeJSON(settingsPath, {});
|
|
497
|
+
}
|
|
498
|
+
logOk(`Created ${settingsPath}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const settings = readJSON(settingsPath) || {};
|
|
502
|
+
|
|
503
|
+
let changed = false;
|
|
504
|
+
|
|
505
|
+
// ── PostToolUse hook: legion-context-monitor.js ──
|
|
506
|
+
|
|
507
|
+
const contextMonitorCmd = `node "${path.join(configDir, 'hooks', 'legion-context-monitor.js').replace(/\\/g, '/')}"`;
|
|
508
|
+
|
|
509
|
+
if (!settings.hooks) {
|
|
510
|
+
settings.hooks = {};
|
|
511
|
+
}
|
|
512
|
+
if (!Array.isArray(settings.hooks.PostToolUse)) {
|
|
513
|
+
settings.hooks.PostToolUse = [];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const alreadyRegistered = settings.hooks.PostToolUse.some(entry => {
|
|
517
|
+
const entryStr = JSON.stringify(entry);
|
|
518
|
+
return entryStr.includes('legion-context-monitor');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (!alreadyRegistered) {
|
|
522
|
+
settings.hooks.PostToolUse.push({
|
|
523
|
+
hooks: [
|
|
524
|
+
{
|
|
525
|
+
type: 'command',
|
|
526
|
+
command: contextMonitorCmd,
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
});
|
|
530
|
+
changed = true;
|
|
531
|
+
logOk('Registered PostToolUse: legion-context-monitor.js');
|
|
532
|
+
} else {
|
|
533
|
+
logSkip('PostToolUse: legion-context-monitor.js already registered');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ── StatusLine: legion-statusline.js ──
|
|
537
|
+
|
|
538
|
+
const statuslineCmd = `node "${path.join(configDir, 'hooks', 'legion-statusline.js').replace(/\\/g, '/')}"`;
|
|
539
|
+
|
|
540
|
+
const currentStatusLine = settings.statusLine || null;
|
|
541
|
+
const currentCmd = (currentStatusLine && typeof currentStatusLine === 'object')
|
|
542
|
+
? (currentStatusLine.command || '')
|
|
543
|
+
: '';
|
|
544
|
+
|
|
545
|
+
if (!currentStatusLine) {
|
|
546
|
+
// No statusline configured — set Legion's
|
|
547
|
+
settings.statusLine = {
|
|
548
|
+
type: 'command',
|
|
549
|
+
command: statuslineCmd,
|
|
550
|
+
};
|
|
551
|
+
changed = true;
|
|
552
|
+
logOk('Configured statusLine: legion-statusline.js');
|
|
553
|
+
} else if (currentCmd.includes('legion-statusline')) {
|
|
554
|
+
logSkip('statusLine: legion-statusline.js already configured');
|
|
555
|
+
} else if (currentCmd.includes('gsd-statusline')) {
|
|
556
|
+
logWarn('GSD statusline is currently active.');
|
|
557
|
+
logWarn('Only one statusline can be active at a time.');
|
|
558
|
+
logWarn(`To switch to Legion, manually update "statusLine" in ${settingsPath}`);
|
|
559
|
+
} else if (currentCmd) {
|
|
560
|
+
logWarn(`Another statusline is configured: ${currentCmd}`);
|
|
561
|
+
logWarn(`To use Legion statusline, update "statusLine" in ${settingsPath}`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Write settings if changed
|
|
565
|
+
if (changed) {
|
|
566
|
+
writeJSON(settingsPath, settings);
|
|
567
|
+
logOk(`Updated ${settingsPath}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ─── Generate manifest ──────────────────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
function generateManifest() {
|
|
574
|
+
const manifestPath = path.join(configDir, 'legion-file-manifest.json');
|
|
575
|
+
|
|
576
|
+
const manifest = {
|
|
577
|
+
version: version,
|
|
578
|
+
timestamp: new Date().toISOString(),
|
|
579
|
+
files: {},
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// Directories to scan for manifest
|
|
583
|
+
const scanDirs = [
|
|
584
|
+
path.join(configDir, 'legion'),
|
|
585
|
+
path.join(configDir, 'commands', 'legion'),
|
|
586
|
+
];
|
|
587
|
+
|
|
588
|
+
// Individual files to include
|
|
589
|
+
const scanFiles = [
|
|
590
|
+
path.join(configDir, 'hooks', 'legion-context-monitor.js'),
|
|
591
|
+
path.join(configDir, 'hooks', 'legion-statusline.js'),
|
|
592
|
+
];
|
|
593
|
+
|
|
594
|
+
// Also include agent files we installed
|
|
595
|
+
const agentsSrc = path.join(srcDir, 'agents');
|
|
596
|
+
if (fs.existsSync(agentsSrc)) {
|
|
597
|
+
const agentFiles = fs.readdirSync(agentsSrc).filter(f => f.endsWith('.md'));
|
|
598
|
+
for (const agentFile of agentFiles) {
|
|
599
|
+
scanFiles.push(path.join(configDir, 'agents', agentFile));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (flagDryRun) {
|
|
604
|
+
console.log(` ${c(dim, '[dry-run]')} would write ${manifestPath}`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Walk directories
|
|
609
|
+
for (const dir of scanDirs) {
|
|
610
|
+
const files = walkDir(dir);
|
|
611
|
+
for (const filePath of files) {
|
|
612
|
+
const rel = path.relative(configDir, filePath);
|
|
613
|
+
try {
|
|
614
|
+
manifest.files[rel] = sha256File(filePath);
|
|
615
|
+
} catch (err) {
|
|
616
|
+
logWarn(`Could not hash ${filePath}: ${err.message}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Hash individual files
|
|
622
|
+
for (const filePath of scanFiles) {
|
|
623
|
+
if (fs.existsSync(filePath)) {
|
|
624
|
+
const rel = path.relative(configDir, filePath);
|
|
625
|
+
try {
|
|
626
|
+
manifest.files[rel] = sha256File(filePath);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
logWarn(`Could not hash ${filePath}: ${err.message}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Sort keys
|
|
634
|
+
const sortedFiles = {};
|
|
635
|
+
for (const key of Object.keys(manifest.files).sort()) {
|
|
636
|
+
sortedFiles[key] = manifest.files[key];
|
|
637
|
+
}
|
|
638
|
+
manifest.files = sortedFiles;
|
|
639
|
+
|
|
640
|
+
writeJSON(manifestPath, manifest);
|
|
641
|
+
logOk(`Manifest written: ${manifestPath} (${Object.keys(manifest.files).length} files)`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ─── Print summary ──────────────────────────────────────────────────────────
|
|
645
|
+
|
|
646
|
+
function printSummary() {
|
|
647
|
+
console.log('');
|
|
648
|
+
console.log(` ${c(bold + green, 'Legion v' + version + ' installed to ' + configDir + '/')}`);
|
|
649
|
+
console.log('');
|
|
650
|
+
console.log(` Commands: ${c(bold, String(counts.commands))} (commands/legion/)`);
|
|
651
|
+
console.log(` Workflows: ${c(bold, String(counts.workflows))} (legion/workflows/)`);
|
|
652
|
+
console.log(` Templates: ${c(bold, String(counts.templates))} (legion/templates/)`);
|
|
653
|
+
console.log(` References: ${c(bold, String(counts.references))} (legion/references/)`);
|
|
654
|
+
console.log(` Hooks: ${c(bold, String(counts.hooks))} (hooks/)`);
|
|
655
|
+
console.log(` Agents: ${c(bold, String(counts.agents))} (agents/)`);
|
|
656
|
+
console.log(` Bin: ${c(bold, String(counts.bin))} (legion/bin/)`);
|
|
657
|
+
console.log('');
|
|
658
|
+
console.log(` Total: ${c(bold, String(totalFiles))} files installed`);
|
|
659
|
+
if (skippedFiles > 0) {
|
|
660
|
+
console.log(` Skipped: ${c(dim, String(skippedFiles))} files`);
|
|
661
|
+
}
|
|
662
|
+
console.log('');
|
|
663
|
+
console.log(` ${c(dim, 'To get started:')}`);
|
|
664
|
+
console.log(` ${c(cyan, '/legion:devops:quick <task>')}`);
|
|
665
|
+
console.log(` ${c(cyan, '/legion:devops:architect <task>')}`);
|
|
666
|
+
console.log(` ${c(cyan, '/legion:status')}`);
|
|
667
|
+
console.log('');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ─── Uninstall logic ────────────────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
function doUninstall() {
|
|
673
|
+
checkPrerequisites();
|
|
674
|
+
|
|
675
|
+
logStep('Uninstalling Legion...');
|
|
676
|
+
|
|
677
|
+
const manifestPath = path.join(configDir, 'legion-file-manifest.json');
|
|
678
|
+
const manifest = readJSON(manifestPath);
|
|
679
|
+
|
|
680
|
+
let removedCount = 0;
|
|
681
|
+
let failedCount = 0;
|
|
682
|
+
|
|
683
|
+
// ── Step 1: Remove manifested files ──
|
|
684
|
+
|
|
685
|
+
if (manifest && manifest.files) {
|
|
686
|
+
logStep('Removing manifested files...');
|
|
687
|
+
const files = Object.keys(manifest.files).sort().reverse();
|
|
688
|
+
let agentsSkipped = 0;
|
|
689
|
+
for (const rel of files) {
|
|
690
|
+
// Do NOT remove agent files — they may have been customized by the user
|
|
691
|
+
if (rel.startsWith('agents/') || rel.startsWith('agents\\')) {
|
|
692
|
+
agentsSkipped++;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
const filePath = path.join(configDir, rel);
|
|
696
|
+
if (flagDryRun) {
|
|
697
|
+
console.log(` ${c(dim, '[dry-run]')} rm ${filePath}`);
|
|
698
|
+
removedCount++;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
if (fs.existsSync(filePath)) {
|
|
703
|
+
fs.unlinkSync(filePath);
|
|
704
|
+
removedCount++;
|
|
705
|
+
}
|
|
706
|
+
} catch (err) {
|
|
707
|
+
logWarn(`Could not remove ${filePath}: ${err.message}`);
|
|
708
|
+
failedCount++;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
logOk(`Removed ${removedCount} manifested files`);
|
|
712
|
+
if (agentsSkipped > 0) {
|
|
713
|
+
logOk(`Preserved ${agentsSkipped} agent file(s) in agents/`);
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
logWarn('No manifest found, performing directory-based cleanup...');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ── Step 2: Remove directories ──
|
|
720
|
+
|
|
721
|
+
logStep('Removing directories...');
|
|
722
|
+
|
|
723
|
+
// Remove ~/.claude/legion/
|
|
724
|
+
const legionDir = path.join(configDir, 'legion');
|
|
725
|
+
removeDir(legionDir);
|
|
726
|
+
|
|
727
|
+
// Remove ~/.claude/commands/legion/
|
|
728
|
+
const commandsLegionDir = path.join(configDir, 'commands', 'legion');
|
|
729
|
+
removeDir(commandsLegionDir);
|
|
730
|
+
|
|
731
|
+
// ── Step 3: Remove hooks ──
|
|
732
|
+
|
|
733
|
+
logStep('Removing hooks...');
|
|
734
|
+
const hookFiles = [
|
|
735
|
+
path.join(configDir, 'hooks', 'legion-context-monitor.js'),
|
|
736
|
+
path.join(configDir, 'hooks', 'legion-statusline.js'),
|
|
737
|
+
];
|
|
738
|
+
for (const hookPath of hookFiles) {
|
|
739
|
+
if (flagDryRun) {
|
|
740
|
+
if (fs.existsSync(hookPath)) {
|
|
741
|
+
console.log(` ${c(dim, '[dry-run]')} rm ${hookPath}`);
|
|
742
|
+
}
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
if (fs.existsSync(hookPath)) {
|
|
747
|
+
fs.unlinkSync(hookPath);
|
|
748
|
+
logOk(`Removed ${path.basename(hookPath)}`);
|
|
749
|
+
}
|
|
750
|
+
} catch (err) {
|
|
751
|
+
logWarn(`Could not remove ${hookPath}: ${err.message}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ── Step 4: Unregister hooks from settings ──
|
|
756
|
+
|
|
757
|
+
logStep('Unregistering hooks from settings...');
|
|
758
|
+
unregisterHooks();
|
|
759
|
+
|
|
760
|
+
// ── Step 5: Remove manifest ──
|
|
761
|
+
|
|
762
|
+
if (flagDryRun) {
|
|
763
|
+
if (fs.existsSync(manifestPath)) {
|
|
764
|
+
console.log(` ${c(dim, '[dry-run]')} rm ${manifestPath}`);
|
|
765
|
+
}
|
|
766
|
+
} else {
|
|
767
|
+
try {
|
|
768
|
+
if (fs.existsSync(manifestPath)) {
|
|
769
|
+
fs.unlinkSync(manifestPath);
|
|
770
|
+
logOk('Removed legion-file-manifest.json');
|
|
771
|
+
}
|
|
772
|
+
} catch (err) {
|
|
773
|
+
logWarn(`Could not remove manifest: ${err.message}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ── Summary ──
|
|
778
|
+
|
|
779
|
+
console.log('');
|
|
780
|
+
console.log(` ${c(bold + green, 'Legion v' + version + ' uninstalled from ' + configDir + '/')}`);
|
|
781
|
+
console.log('');
|
|
782
|
+
console.log(` ${c(dim, 'Removed:')} ${removedCount} files`);
|
|
783
|
+
if (failedCount > 0) {
|
|
784
|
+
console.log(` ${c(yellow, 'Failed:')} ${failedCount} files`);
|
|
785
|
+
}
|
|
786
|
+
console.log('');
|
|
787
|
+
console.log(` ${c(dim, 'Note: Agent files (agents/) and project data (.planning/legion/) were NOT removed.')}`);
|
|
788
|
+
console.log('');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Recursively remove a directory and its contents.
|
|
793
|
+
*/
|
|
794
|
+
function removeDir(dir) {
|
|
795
|
+
if (!fs.existsSync(dir)) return;
|
|
796
|
+
|
|
797
|
+
if (flagDryRun) {
|
|
798
|
+
const count = countFilesInDir(dir);
|
|
799
|
+
console.log(` ${c(dim, '[dry-run]')} rmdir ${dir} (${count} files)`);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
805
|
+
logOk(`Removed ${dir}`);
|
|
806
|
+
} catch (err) {
|
|
807
|
+
logWarn(`Could not remove ${dir}: ${err.message}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Remove Legion hooks from settings.json.
|
|
813
|
+
*/
|
|
814
|
+
function unregisterHooks() {
|
|
815
|
+
let settingsPath = null;
|
|
816
|
+
const settingsLocalPath = path.join(configDir, 'settings.local.json');
|
|
817
|
+
const settingsJsonPath = path.join(configDir, 'settings.json');
|
|
818
|
+
|
|
819
|
+
if (fs.existsSync(settingsLocalPath)) {
|
|
820
|
+
settingsPath = settingsLocalPath;
|
|
821
|
+
} else if (fs.existsSync(settingsJsonPath)) {
|
|
822
|
+
settingsPath = settingsJsonPath;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (!settingsPath) {
|
|
826
|
+
logSkip('No settings file found');
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const settings = readJSON(settingsPath);
|
|
831
|
+
if (!settings) {
|
|
832
|
+
logWarn(`Could not parse ${settingsPath}`);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
let changed = false;
|
|
837
|
+
|
|
838
|
+
// Remove PostToolUse hook entries that reference legion-context-monitor
|
|
839
|
+
if (settings.hooks && Array.isArray(settings.hooks.PostToolUse)) {
|
|
840
|
+
const before = settings.hooks.PostToolUse.length;
|
|
841
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(entry => {
|
|
842
|
+
const entryStr = JSON.stringify(entry);
|
|
843
|
+
return !entryStr.includes('legion-context-monitor');
|
|
844
|
+
});
|
|
845
|
+
if (settings.hooks.PostToolUse.length < before) {
|
|
846
|
+
changed = true;
|
|
847
|
+
logOk('Removed PostToolUse: legion-context-monitor.js');
|
|
848
|
+
}
|
|
849
|
+
// Clean up empty arrays/objects
|
|
850
|
+
if (settings.hooks.PostToolUse.length === 0) {
|
|
851
|
+
delete settings.hooks.PostToolUse;
|
|
852
|
+
}
|
|
853
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
854
|
+
delete settings.hooks;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Remove statusLine if it references legion
|
|
859
|
+
if (settings.statusLine) {
|
|
860
|
+
const cmd = (typeof settings.statusLine === 'object')
|
|
861
|
+
? (settings.statusLine.command || '')
|
|
862
|
+
: '';
|
|
863
|
+
if (cmd.includes('legion-statusline')) {
|
|
864
|
+
delete settings.statusLine;
|
|
865
|
+
changed = true;
|
|
866
|
+
logOk('Removed statusLine: legion-statusline.js');
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (changed) {
|
|
871
|
+
writeJSON(settingsPath, settings);
|
|
872
|
+
logOk(`Updated ${settingsPath}`);
|
|
873
|
+
} else {
|
|
874
|
+
logSkip('No Legion hooks found in settings');
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
879
|
+
|
|
880
|
+
function main() {
|
|
881
|
+
try {
|
|
882
|
+
if (flagUninstall) {
|
|
883
|
+
doUninstall();
|
|
884
|
+
} else {
|
|
885
|
+
doInstall();
|
|
886
|
+
}
|
|
887
|
+
process.exit(0);
|
|
888
|
+
} catch (err) {
|
|
889
|
+
console.error('');
|
|
890
|
+
logErr(`Unexpected error: ${err.message}`);
|
|
891
|
+
if (err.stack) {
|
|
892
|
+
console.error(c(dim, err.stack));
|
|
893
|
+
}
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
main();
|