awesome-agv 2.1.0 → 3.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/README.md +53 -29
- package/agv.config.json +253 -0
- package/bin/awesome-agv.js +789 -157
- package/package.json +7 -3
package/bin/awesome-agv.js
CHANGED
|
@@ -9,10 +9,9 @@ const path = require('path');
|
|
|
9
9
|
const { createInterface } = require('readline');
|
|
10
10
|
const { execSync } = require('child_process');
|
|
11
11
|
const os = require('os');
|
|
12
|
-
const { createGunzip } = require('zlib');
|
|
13
12
|
|
|
14
13
|
// ── ANSI Colors (zero-dependency) ──────────────────────────────────────────────
|
|
15
|
-
const
|
|
14
|
+
const c = {
|
|
16
15
|
reset: '\x1b[0m',
|
|
17
16
|
bold: '\x1b[1m',
|
|
18
17
|
dim: '\x1b[2m',
|
|
@@ -21,19 +20,22 @@ const color = {
|
|
|
21
20
|
red: '\x1b[31m',
|
|
22
21
|
cyan: '\x1b[36m',
|
|
23
22
|
magenta: '\x1b[35m',
|
|
23
|
+
white: '\x1b[37m',
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
const
|
|
27
|
-
check: `${
|
|
28
|
-
warn: `${
|
|
29
|
-
error: `${
|
|
30
|
-
arrow: `${
|
|
26
|
+
const icon = {
|
|
27
|
+
check: `${c.green}✓${c.reset}`,
|
|
28
|
+
warn: `${c.yellow}⚠${c.reset}`,
|
|
29
|
+
error: `${c.red}✗${c.reset}`,
|
|
30
|
+
arrow: `${c.cyan}→${c.reset}`,
|
|
31
31
|
rocket: '🚀',
|
|
32
32
|
shield: '🛡️',
|
|
33
33
|
gear: '⚙️',
|
|
34
34
|
book: '📏',
|
|
35
35
|
tool: '🛠️',
|
|
36
36
|
cycle: '🔄',
|
|
37
|
+
target: '🎯',
|
|
38
|
+
bolt: '⚡',
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
@@ -42,6 +44,7 @@ const REPO_NAME = 'awesome-agv';
|
|
|
42
44
|
const BRANCH = 'main';
|
|
43
45
|
const TARBALL_URL = `https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/heads/${BRANCH}.tar.gz`;
|
|
44
46
|
const AGENT_DIR = '.agents';
|
|
47
|
+
const CONFIG_FILE = '.agvrc';
|
|
45
48
|
|
|
46
49
|
// ── CLI Argument Parsing ───────────────────────────────────────────────────────
|
|
47
50
|
function parseArgs(argv) {
|
|
@@ -50,13 +53,29 @@ function parseArgs(argv) {
|
|
|
50
53
|
force: false,
|
|
51
54
|
targetDir: process.cwd(),
|
|
52
55
|
help: false,
|
|
56
|
+
all: false,
|
|
57
|
+
stacks: null,
|
|
53
58
|
};
|
|
54
59
|
|
|
55
|
-
for (
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
const arg = args[i];
|
|
56
62
|
if (arg === '--force' || arg === '-f') {
|
|
57
63
|
options.force = true;
|
|
58
64
|
} else if (arg === '--help' || arg === '-h') {
|
|
59
65
|
options.help = true;
|
|
66
|
+
} else if (arg === '--all') {
|
|
67
|
+
options.all = true;
|
|
68
|
+
} else if (arg === '--stacks') {
|
|
69
|
+
const next = args[i + 1];
|
|
70
|
+
if (next && !next.startsWith('-')) {
|
|
71
|
+
options.stacks = next.split(',').map((s) => s.trim().toLowerCase());
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
} else if (arg.startsWith('--stacks=')) {
|
|
75
|
+
options.stacks = arg
|
|
76
|
+
.slice('--stacks='.length)
|
|
77
|
+
.split(',')
|
|
78
|
+
.map((s) => s.trim().toLowerCase());
|
|
60
79
|
} else if (!arg.startsWith('-')) {
|
|
61
80
|
options.targetDir = path.resolve(arg);
|
|
62
81
|
}
|
|
@@ -65,48 +84,81 @@ function parseArgs(argv) {
|
|
|
65
84
|
return options;
|
|
66
85
|
}
|
|
67
86
|
|
|
87
|
+
// ── Banner ─────────────────────────────────────────────────────────────────────
|
|
88
|
+
function printBanner() {
|
|
89
|
+
const { version } = require('../package.json');
|
|
90
|
+
|
|
91
|
+
// Gradient: magenta → cyan → white
|
|
92
|
+
const m = c.magenta + c.bold;
|
|
93
|
+
const cy = c.cyan + c.bold;
|
|
94
|
+
const w = c.white + c.bold;
|
|
95
|
+
|
|
96
|
+
console.log(`
|
|
97
|
+
${m} █████╗ ██╗ ██╗███████╗███████╗ ██████╗ ███╗ ███╗███████╗${c.reset}
|
|
98
|
+
${m} ██╔══██╗ ██║ ██║██╔════╝██╔════╝██╔═══██╗████╗ ████║██╔════╝${c.reset}
|
|
99
|
+
${cy} ███████║ ██║ █╗ ██║█████╗ ███████╗██║ ██║██╔████╔██║█████╗${c.reset}
|
|
100
|
+
${cy} ██╔══██║ ██║███╗██║██╔══╝ ╚════██║██║ ██║██║╚██╔╝██║██╔══╝${c.reset}
|
|
101
|
+
${w} ██║ ██║ ╚███╔███╔╝███████╗███████║╚██████╔╝██║ ╚═╝ ██║███████╗${c.reset}
|
|
102
|
+
${w} ╚═╝ ╚═╝ ╚══╝╚══╝╚══════╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝${c.reset}
|
|
103
|
+
${cy} █████╗ ██████╗ ██╗ ██╗${c.reset}
|
|
104
|
+
${w} ██╔══██╗██╔════╝ ██║ ██║${c.reset}
|
|
105
|
+
${w} ███████║██║ ███╗██║ ██║${c.reset}
|
|
106
|
+
${cy} ██╔══██║██║ ██║╚██╗ ██╔╝${c.reset}
|
|
107
|
+
${m} ██║ ██║╚██████╔╝ ╚████╔╝${c.reset}
|
|
108
|
+
${m} ╚═╝ ╚═╝ ╚═════╝ ╚═══╝${c.reset} ${c.dim}v${version}${c.reset}
|
|
109
|
+
|
|
110
|
+
${icon.bolt} ${c.bold}Rugged AI Agent Configuration Suite${c.reset}
|
|
111
|
+
${c.dim}─────────────────────────────────────────────────────${c.reset}
|
|
112
|
+
`);
|
|
113
|
+
}
|
|
114
|
+
|
|
68
115
|
// ── Help Text ──────────────────────────────────────────────────────────────────
|
|
69
116
|
function printHelp() {
|
|
117
|
+
const { version } = require('../package.json');
|
|
70
118
|
console.log(`
|
|
71
|
-
${
|
|
119
|
+
${c.bold}awesome-agv${c.reset} ${c.dim}v${version}${c.reset} — Install the Awesome AGV AI Agent configuration suite
|
|
72
120
|
|
|
73
|
-
${
|
|
121
|
+
${c.bold}USAGE${c.reset}
|
|
74
122
|
npx awesome-agv [target-dir] [options]
|
|
75
123
|
|
|
76
|
-
${
|
|
77
|
-
target-dir
|
|
124
|
+
${c.bold}ARGUMENTS${c.reset}
|
|
125
|
+
target-dir Directory to install into (default: current directory)
|
|
78
126
|
|
|
79
|
-
${
|
|
80
|
-
-
|
|
81
|
-
|
|
127
|
+
${c.bold}OPTIONS${c.reset}
|
|
128
|
+
--stacks <list> Comma-separated stacks to include (additive with existing .agvrc)
|
|
129
|
+
--all Install everything (all stacks, same as Default mode)
|
|
130
|
+
-f, --force Replace existing .agents/ — re-prompts from scratch
|
|
131
|
+
-h, --help Show this help message
|
|
82
132
|
|
|
83
|
-
${
|
|
84
|
-
${
|
|
133
|
+
${c.bold}INSTALL MODES${c.reset}
|
|
134
|
+
${icon.rocket} ${c.bold}Default${c.reset} Install everything. The full arsenal.
|
|
135
|
+
${icon.target} ${c.bold}Curated${c.reset} Pick languages & frameworks. Core always included.
|
|
136
|
+
${icon.gear} ${c.bold}Advanced${c.reset} Full control. Pick rules, skills, agents, everything.
|
|
137
|
+
|
|
138
|
+
${c.bold}EXAMPLES${c.reset}
|
|
139
|
+
${c.dim}# Interactive — choose your mode${c.reset}
|
|
85
140
|
npx awesome-agv
|
|
86
141
|
|
|
87
|
-
${
|
|
88
|
-
npx awesome-agv
|
|
142
|
+
${c.dim}# Non-interactive: specific stacks, core always included${c.reset}
|
|
143
|
+
npx awesome-agv --stacks go,python
|
|
144
|
+
|
|
145
|
+
${c.dim}# Add React to existing installation (additive)${c.reset}
|
|
146
|
+
npx awesome-agv --stacks react
|
|
89
147
|
|
|
90
|
-
${
|
|
91
|
-
npx awesome-agv --force
|
|
148
|
+
${c.dim}# Full install, no prompts${c.reset}
|
|
149
|
+
npx awesome-agv --all --force
|
|
92
150
|
|
|
93
|
-
${
|
|
94
|
-
|
|
95
|
-
${icons.tool} 43 Skills — Debugging, design, code review, language idioms, and more
|
|
96
|
-
${icons.cycle} 12 Workflows — End-to-end development processes
|
|
97
|
-
🤖 15 Agents — Specialized personas for multi-agent orchestration
|
|
151
|
+
${c.dim}# CI/CD: specific stacks, no prompts${c.reset}
|
|
152
|
+
npx awesome-agv --stacks typescript,vue --force
|
|
98
153
|
|
|
99
|
-
${
|
|
154
|
+
${c.dim}https://github.com/${REPO_OWNER}/${REPO_NAME}${c.reset}
|
|
100
155
|
`);
|
|
101
156
|
}
|
|
102
157
|
|
|
103
|
-
// ── User Prompt
|
|
158
|
+
// ── User Prompt (single-line question) ─────────────────────────────────────────
|
|
104
159
|
function promptUser(question) {
|
|
105
160
|
return new Promise((resolve) => {
|
|
106
|
-
const rl = createInterface({
|
|
107
|
-
input: process.stdin,
|
|
108
|
-
output: process.stdout,
|
|
109
|
-
});
|
|
161
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
110
162
|
rl.question(question, (answer) => {
|
|
111
163
|
rl.close();
|
|
112
164
|
resolve(answer.trim().toLowerCase());
|
|
@@ -114,6 +166,364 @@ function promptUser(question) {
|
|
|
114
166
|
});
|
|
115
167
|
}
|
|
116
168
|
|
|
169
|
+
// ── Single-Select Menu (arrow keys + enter) ────────────────────────────────────
|
|
170
|
+
function promptSelect(title, options) {
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
let selected = 0;
|
|
173
|
+
|
|
174
|
+
function render() {
|
|
175
|
+
// Move cursor up to clear previous render (except first render)
|
|
176
|
+
const totalLines = options.length + 2;
|
|
177
|
+
process.stdout.write(`\x1b[${totalLines}A\x1b[J`);
|
|
178
|
+
console.log(` ${c.bold}${title}${c.reset}\n`);
|
|
179
|
+
for (let i = 0; i < options.length; i++) {
|
|
180
|
+
const pointer = i === selected ? `${c.cyan}❯${c.reset}` : ' ';
|
|
181
|
+
const label =
|
|
182
|
+
i === selected
|
|
183
|
+
? `${c.bold}${options[i].label}${c.reset}`
|
|
184
|
+
: `${c.dim}${options[i].label}${c.reset}`;
|
|
185
|
+
console.log(` ${pointer} ${label}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Print initial blank lines so the first render can overwrite
|
|
190
|
+
console.log(` ${c.bold}${title}${c.reset}\n`);
|
|
191
|
+
for (const opt of options) {
|
|
192
|
+
console.log(` ${c.dim}${opt.label}${c.reset}`);
|
|
193
|
+
}
|
|
194
|
+
render();
|
|
195
|
+
|
|
196
|
+
process.stdin.setRawMode(true);
|
|
197
|
+
process.stdin.resume();
|
|
198
|
+
process.stdin.setEncoding('utf8');
|
|
199
|
+
|
|
200
|
+
const onData = (key) => {
|
|
201
|
+
if (key === '\x1b[A') {
|
|
202
|
+
// Up arrow
|
|
203
|
+
selected = (selected - 1 + options.length) % options.length;
|
|
204
|
+
render();
|
|
205
|
+
} else if (key === '\x1b[B') {
|
|
206
|
+
// Down arrow
|
|
207
|
+
selected = (selected + 1) % options.length;
|
|
208
|
+
render();
|
|
209
|
+
} else if (key === '\r' || key === '\n') {
|
|
210
|
+
// Enter
|
|
211
|
+
process.stdin.setRawMode(false);
|
|
212
|
+
process.stdin.pause();
|
|
213
|
+
process.stdin.removeListener('data', onData);
|
|
214
|
+
console.log('');
|
|
215
|
+
resolve(options[selected].value);
|
|
216
|
+
} else if (key === '\x03') {
|
|
217
|
+
// Ctrl+C
|
|
218
|
+
process.stdin.setRawMode(false);
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
process.stdin.on('data', onData);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Multi-Select Menu (arrow keys, space, enter) ───────────────────────────────
|
|
228
|
+
function promptMultiSelect(title, items, footerFn) {
|
|
229
|
+
// items: [{ key, label, indent, checked, disabled, warning }]
|
|
230
|
+
return new Promise((resolve) => {
|
|
231
|
+
let cursor = 0;
|
|
232
|
+
// Skip to first non-disabled item
|
|
233
|
+
while (cursor < items.length && items[cursor].disabled) cursor++;
|
|
234
|
+
|
|
235
|
+
function render() {
|
|
236
|
+
const totalLines = items.length + 4; // title + blank + items + footer + blank
|
|
237
|
+
process.stdout.write(`\x1b[${totalLines}A\x1b[J`);
|
|
238
|
+
console.log(` ${c.bold}${title}${c.reset}`);
|
|
239
|
+
console.log('');
|
|
240
|
+
for (let i = 0; i < items.length; i++) {
|
|
241
|
+
const item = items[i];
|
|
242
|
+
const pointer = i === cursor ? `${c.cyan}❯${c.reset}` : ' ';
|
|
243
|
+
const indent = ' '.repeat(item.indent || 0);
|
|
244
|
+
let checkbox;
|
|
245
|
+
if (item.separator) {
|
|
246
|
+
console.log(` ${c.dim}${item.label}${c.reset}`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (item.disabled) {
|
|
250
|
+
checkbox = `${c.dim}◉${c.reset}`;
|
|
251
|
+
} else {
|
|
252
|
+
checkbox = item.checked
|
|
253
|
+
? `${c.green}◉${c.reset}`
|
|
254
|
+
: `${c.dim}◯${c.reset}`;
|
|
255
|
+
}
|
|
256
|
+
const label =
|
|
257
|
+
i === cursor
|
|
258
|
+
? `${c.bold}${item.label}${c.reset}`
|
|
259
|
+
: item.checked
|
|
260
|
+
? `${item.label}`
|
|
261
|
+
: `${c.dim}${item.label}${c.reset}`;
|
|
262
|
+
|
|
263
|
+
console.log(` ${pointer} ${indent}${checkbox} ${label}`);
|
|
264
|
+
}
|
|
265
|
+
console.log('');
|
|
266
|
+
if (footerFn) {
|
|
267
|
+
console.log(` ${footerFn(items)}`);
|
|
268
|
+
} else {
|
|
269
|
+
console.log(` ${c.dim}(space to toggle, enter to confirm)${c.reset}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Initial blank lines
|
|
274
|
+
console.log(` ${c.bold}${title}${c.reset}`);
|
|
275
|
+
console.log('');
|
|
276
|
+
for (const item of items) {
|
|
277
|
+
console.log('');
|
|
278
|
+
}
|
|
279
|
+
console.log('');
|
|
280
|
+
console.log('');
|
|
281
|
+
render();
|
|
282
|
+
|
|
283
|
+
process.stdin.setRawMode(true);
|
|
284
|
+
process.stdin.resume();
|
|
285
|
+
process.stdin.setEncoding('utf8');
|
|
286
|
+
|
|
287
|
+
const onData = (key) => {
|
|
288
|
+
if (key === '\x1b[A') {
|
|
289
|
+
// Up — skip separators and disabled
|
|
290
|
+
let next = (cursor - 1 + items.length) % items.length;
|
|
291
|
+
let attempts = 0;
|
|
292
|
+
while ((items[next].separator || items[next].disabled) && attempts < items.length) {
|
|
293
|
+
next = (next - 1 + items.length) % items.length;
|
|
294
|
+
attempts++;
|
|
295
|
+
}
|
|
296
|
+
cursor = next;
|
|
297
|
+
render();
|
|
298
|
+
} else if (key === '\x1b[B') {
|
|
299
|
+
// Down — skip separators and disabled
|
|
300
|
+
let next = (cursor + 1) % items.length;
|
|
301
|
+
let attempts = 0;
|
|
302
|
+
while ((items[next].separator || items[next].disabled) && attempts < items.length) {
|
|
303
|
+
next = (next + 1) % items.length;
|
|
304
|
+
attempts++;
|
|
305
|
+
}
|
|
306
|
+
cursor = next;
|
|
307
|
+
render();
|
|
308
|
+
} else if (key === ' ') {
|
|
309
|
+
// Space — toggle
|
|
310
|
+
const item = items[cursor];
|
|
311
|
+
if (!item.separator && !item.disabled) {
|
|
312
|
+
item.checked = !item.checked;
|
|
313
|
+
// Handle requires: selecting a child auto-selects parent
|
|
314
|
+
if (item.checked && item.requires) {
|
|
315
|
+
for (const reqKey of item.requires) {
|
|
316
|
+
const parent = items.find((i) => i.key === reqKey);
|
|
317
|
+
if (parent && !parent.checked) {
|
|
318
|
+
parent.checked = true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Handle requires: deselecting a parent auto-deselects all transitive children
|
|
323
|
+
if (!item.checked) {
|
|
324
|
+
let cascadeChanged = true;
|
|
325
|
+
while (cascadeChanged) {
|
|
326
|
+
cascadeChanged = false;
|
|
327
|
+
for (const child of items) {
|
|
328
|
+
if (child.checked && child.requires) {
|
|
329
|
+
// If any required parent is unchecked, deselect this child
|
|
330
|
+
const allReqsMet = child.requires.every((reqKey) => {
|
|
331
|
+
const parent = items.find((i) => i.key === reqKey);
|
|
332
|
+
return parent && parent.checked;
|
|
333
|
+
});
|
|
334
|
+
if (!allReqsMet) {
|
|
335
|
+
child.checked = false;
|
|
336
|
+
cascadeChanged = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
render();
|
|
343
|
+
}
|
|
344
|
+
} else if (key === '\r' || key === '\n') {
|
|
345
|
+
// Enter — confirm
|
|
346
|
+
process.stdin.setRawMode(false);
|
|
347
|
+
process.stdin.pause();
|
|
348
|
+
process.stdin.removeListener('data', onData);
|
|
349
|
+
console.log('');
|
|
350
|
+
resolve(items.filter((i) => i.checked && !i.separator));
|
|
351
|
+
} else if (key === '\x03') {
|
|
352
|
+
process.stdin.setRawMode(false);
|
|
353
|
+
process.exit(0);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
process.stdin.on('data', onData);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Auto-Detection ─────────────────────────────────────────────────────────────
|
|
362
|
+
function detectStacks(targetDir, manifest) {
|
|
363
|
+
const detected = new Set();
|
|
364
|
+
const stacks = manifest.skills.stacks;
|
|
365
|
+
|
|
366
|
+
// Gather all detection patterns
|
|
367
|
+
const detectionMap = {};
|
|
368
|
+
for (const [key, stack] of Object.entries(stacks)) {
|
|
369
|
+
if (!stack.detect || stack.detect.length === 0) continue;
|
|
370
|
+
for (const pattern of stack.detect) {
|
|
371
|
+
if (!pattern.includes('*')) {
|
|
372
|
+
// Exact filename match
|
|
373
|
+
if (!detectionMap[pattern]) detectionMap[pattern] = [];
|
|
374
|
+
detectionMap[pattern].push(key);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Scan root directory
|
|
380
|
+
const scanDirs = [targetDir];
|
|
381
|
+
const subDirs = ['apps', 'src', 'packages', 'services'];
|
|
382
|
+
for (const sub of subDirs) {
|
|
383
|
+
const subPath = path.join(targetDir, sub);
|
|
384
|
+
if (fs.existsSync(subPath) && fs.statSync(subPath).isDirectory()) {
|
|
385
|
+
scanDirs.push(subPath);
|
|
386
|
+
// One more level for monorepo detection
|
|
387
|
+
try {
|
|
388
|
+
for (const entry of fs.readdirSync(subPath, { withFileTypes: true })) {
|
|
389
|
+
if (entry.isDirectory()) {
|
|
390
|
+
scanDirs.push(path.join(subPath, entry.name));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
// Permission error, skip
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const dir of scanDirs) {
|
|
400
|
+
let entries;
|
|
401
|
+
try {
|
|
402
|
+
entries = fs.readdirSync(dir);
|
|
403
|
+
} catch {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
for (const entry of entries) {
|
|
407
|
+
// Check exact filename match
|
|
408
|
+
if (detectionMap[entry]) {
|
|
409
|
+
for (const stackKey of detectionMap[entry]) {
|
|
410
|
+
detected.add(stackKey);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Check glob patterns (*.ext)
|
|
414
|
+
for (const [key, stack] of Object.entries(stacks)) {
|
|
415
|
+
if (!stack.detect) continue;
|
|
416
|
+
for (const pattern of stack.detect) {
|
|
417
|
+
if (pattern.startsWith('*.')) {
|
|
418
|
+
const ext = pattern.slice(1); // e.g. ".vue"
|
|
419
|
+
if (entry.endsWith(ext)) {
|
|
420
|
+
detected.add(key);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return detected;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Dependency Resolution ──────────────────────────────────────────────────────
|
|
432
|
+
function resolveRequires(selectedKeys, manifest) {
|
|
433
|
+
const stacks = manifest.skills.stacks;
|
|
434
|
+
const resolved = new Set(selectedKeys);
|
|
435
|
+
let changed = true;
|
|
436
|
+
while (changed) {
|
|
437
|
+
changed = false;
|
|
438
|
+
for (const key of [...resolved]) {
|
|
439
|
+
const stack = stacks[key];
|
|
440
|
+
if (stack && stack.requires) {
|
|
441
|
+
for (const req of stack.requires) {
|
|
442
|
+
if (!resolved.has(req)) {
|
|
443
|
+
resolved.add(req);
|
|
444
|
+
changed = true;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return resolved;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ── Build Skills Set ───────────────────────────────────────────────────────────
|
|
454
|
+
function buildSkillsSet(selectedStackKeys, manifest) {
|
|
455
|
+
const skills = new Set(manifest.skills.core);
|
|
456
|
+
const stacks = manifest.skills.stacks;
|
|
457
|
+
for (const key of selectedStackKeys) {
|
|
458
|
+
const stack = stacks[key];
|
|
459
|
+
if (stack && stack.skills) {
|
|
460
|
+
for (const skill of stack.skills) {
|
|
461
|
+
skills.add(skill);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return skills;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Build Stack Tree for Display ───────────────────────────────────────────────
|
|
469
|
+
function buildStackTree(manifest) {
|
|
470
|
+
const stacks = manifest.skills.stacks;
|
|
471
|
+
const tree = [];
|
|
472
|
+
|
|
473
|
+
// Separate languages (no requires) from frameworks (has requires)
|
|
474
|
+
const languages = {};
|
|
475
|
+
const frameworks = {};
|
|
476
|
+
|
|
477
|
+
for (const [key, stack] of Object.entries(stacks)) {
|
|
478
|
+
if (stack.requires && stack.requires.length > 0) {
|
|
479
|
+
frameworks[key] = stack;
|
|
480
|
+
} else {
|
|
481
|
+
languages[key] = stack;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Group: core languages first, then community
|
|
486
|
+
const coreLanguages = Object.entries(languages).filter(([, s]) => s.group === 'core');
|
|
487
|
+
const communityLanguages = Object.entries(languages).filter(([, s]) => s.group === 'community');
|
|
488
|
+
|
|
489
|
+
// Find direct children of a language
|
|
490
|
+
function getChildren(parentKey) {
|
|
491
|
+
const children = [];
|
|
492
|
+
for (const [key, fw] of Object.entries(frameworks)) {
|
|
493
|
+
if (fw.requires.includes(parentKey)) {
|
|
494
|
+
children.push([key, fw]);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return children;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
// Add a language and its framework children recursively
|
|
502
|
+
function addWithChildren(key, stack, indent) {
|
|
503
|
+
tree.push({ key, indent, label: `${stack.label.padEnd(14)}— ${stack.description}`, requires: stack.requires || null });
|
|
504
|
+
const children = getChildren(key);
|
|
505
|
+
for (const [childKey, childStack] of children) {
|
|
506
|
+
addWithChildren(childKey, childStack, indent + 1);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Core stacks section
|
|
511
|
+
tree.push({ separator: true, label: '── Core Stacks ──────────────────────────────────────' });
|
|
512
|
+
for (const [key, stack] of coreLanguages) {
|
|
513
|
+
addWithChildren(key, stack, 0);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Community languages section
|
|
517
|
+
if (communityLanguages.length > 0) {
|
|
518
|
+
tree.push({ separator: true, label: '── Community Languages ──────────────────────────────' });
|
|
519
|
+
for (const [key, stack] of communityLanguages) {
|
|
520
|
+
addWithChildren(key, stack, 0);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return tree;
|
|
525
|
+
}
|
|
526
|
+
|
|
117
527
|
// ── HTTP Download with Redirect Following ──────────────────────────────────────
|
|
118
528
|
function downloadToFile(url, destPath, maxRedirects = 5) {
|
|
119
529
|
return new Promise((resolve, reject) => {
|
|
@@ -125,9 +535,8 @@ function downloadToFile(url, destPath, maxRedirects = 5) {
|
|
|
125
535
|
const client = url.startsWith('https') ? https : http;
|
|
126
536
|
client
|
|
127
537
|
.get(url, (res) => {
|
|
128
|
-
// Follow redirects (GitHub returns 302)
|
|
129
538
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
130
|
-
res.resume();
|
|
539
|
+
res.resume();
|
|
131
540
|
downloadToFile(res.headers.location, destPath, maxRedirects - 1)
|
|
132
541
|
.then(resolve)
|
|
133
542
|
.catch(reject);
|
|
@@ -154,66 +563,113 @@ function downloadToFile(url, destPath, maxRedirects = 5) {
|
|
|
154
563
|
});
|
|
155
564
|
}
|
|
156
565
|
|
|
157
|
-
// ──
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Use system tar (available on macOS and Linux)
|
|
170
|
-
execSync(
|
|
171
|
-
`tar -xzf "${tarballPath}" --strip-components=2 -C "${agentTargetDir}" "${stripPrefix}"`,
|
|
172
|
-
{ stdio: 'pipe' }
|
|
173
|
-
);
|
|
174
|
-
} catch {
|
|
175
|
-
// Fallback: manual extraction using Node.js streams
|
|
176
|
-
extractManually(tarballPath, targetDir, stripPrefix);
|
|
566
|
+
// ── Recursive directory copy ───────────────────────────────────────────────────
|
|
567
|
+
function copyDirRecursive(src, dest) {
|
|
568
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
569
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
570
|
+
for (const entry of entries) {
|
|
571
|
+
const srcPath = path.join(src, entry.name);
|
|
572
|
+
const destPath = path.join(dest, entry.name);
|
|
573
|
+
if (entry.isDirectory()) {
|
|
574
|
+
copyDirRecursive(srcPath, destPath);
|
|
575
|
+
} else {
|
|
576
|
+
fs.copyFileSync(srcPath, destPath);
|
|
577
|
+
}
|
|
177
578
|
}
|
|
178
579
|
}
|
|
179
580
|
|
|
180
|
-
// ──
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
581
|
+
// ── Count files recursively ────────────────────────────────────────────────────
|
|
582
|
+
function countFiles(dirPath) {
|
|
583
|
+
let count = 0;
|
|
584
|
+
if (!fs.existsSync(dirPath)) return count;
|
|
585
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
586
|
+
for (const entry of entries) {
|
|
587
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
588
|
+
if (entry.isDirectory()) {
|
|
589
|
+
count += countFiles(fullPath);
|
|
590
|
+
} else {
|
|
591
|
+
count++;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return count;
|
|
595
|
+
}
|
|
184
596
|
|
|
185
|
-
|
|
186
|
-
|
|
597
|
+
// ── Extract with filtering ─────────────────────────────────────────────────────
|
|
598
|
+
// Returns the number of skill directories actually copied from the archive.
|
|
599
|
+
function extractFiltered(tarballPath, targetDir, selectedSkills, config) {
|
|
187
600
|
const tmpExtractDir = path.join(os.tmpdir(), `awesome-agv-extract-${Date.now()}`);
|
|
188
601
|
fs.mkdirSync(tmpExtractDir, { recursive: true });
|
|
602
|
+
let skillsCopied = 0;
|
|
189
603
|
|
|
190
604
|
try {
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
605
|
+
// Extract full tarball to temp
|
|
606
|
+
try {
|
|
607
|
+
execSync(`tar -xzf "${tarballPath}" -C "${tmpExtractDir}"`, { stdio: 'pipe' });
|
|
608
|
+
} catch {
|
|
609
|
+
const { execFileSync } = require('child_process');
|
|
610
|
+
execFileSync('tar', ['-xzf', tarballPath, '-C', tmpExtractDir], { stdio: 'pipe' });
|
|
611
|
+
}
|
|
196
612
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
} else {
|
|
613
|
+
const extractedAgents = path.join(tmpExtractDir, `${REPO_NAME}-${BRANCH}`, AGENT_DIR);
|
|
614
|
+
if (!fs.existsSync(extractedAgents)) {
|
|
200
615
|
throw new Error(`Could not find ${AGENT_DIR} in downloaded archive`);
|
|
201
616
|
}
|
|
617
|
+
|
|
618
|
+
const destAgents = path.join(targetDir, AGENT_DIR);
|
|
619
|
+
fs.mkdirSync(destAgents, { recursive: true });
|
|
620
|
+
|
|
621
|
+
// Copy core paths (rules/, workflows/, agents/)
|
|
622
|
+
for (const corePath of config.core.paths) {
|
|
623
|
+
const src = path.join(extractedAgents, corePath);
|
|
624
|
+
const dest = path.join(destAgents, corePath);
|
|
625
|
+
if (fs.existsSync(src)) {
|
|
626
|
+
// If advanced mode deselected items, filter them
|
|
627
|
+
if (config._deselectedRules && corePath === 'rules/') {
|
|
628
|
+
copyDirFiltered(src, dest, config._deselectedRules);
|
|
629
|
+
} else if (config._deselectedAgents && corePath === 'agents/') {
|
|
630
|
+
copyDirFiltered(src, dest, config._deselectedAgents);
|
|
631
|
+
} else if (config._deselectedWorkflows && corePath === 'workflows/') {
|
|
632
|
+
copyDirFiltered(src, dest, config._deselectedWorkflows);
|
|
633
|
+
} else {
|
|
634
|
+
copyDirRecursive(src, dest);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Copy selected skills only
|
|
640
|
+
const skillsSrc = path.join(extractedAgents, 'skills');
|
|
641
|
+
const skillsDest = path.join(destAgents, 'skills');
|
|
642
|
+
fs.mkdirSync(skillsDest, { recursive: true });
|
|
643
|
+
|
|
644
|
+
if (fs.existsSync(skillsSrc)) {
|
|
645
|
+
const allSkillDirs = fs.readdirSync(skillsSrc, { withFileTypes: true });
|
|
646
|
+
for (const entry of allSkillDirs) {
|
|
647
|
+
if (entry.isDirectory() && selectedSkills.has(entry.name)) {
|
|
648
|
+
copyDirRecursive(
|
|
649
|
+
path.join(skillsSrc, entry.name),
|
|
650
|
+
path.join(skillsDest, entry.name)
|
|
651
|
+
);
|
|
652
|
+
skillsCopied++;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
202
656
|
} finally {
|
|
203
|
-
// Clean up temp extraction dir
|
|
204
657
|
fs.rmSync(tmpExtractDir, { recursive: true, force: true });
|
|
205
658
|
}
|
|
659
|
+
|
|
660
|
+
return skillsCopied;
|
|
206
661
|
}
|
|
207
662
|
|
|
208
|
-
// ──
|
|
209
|
-
function
|
|
663
|
+
// ── Copy directory with exclusion filter ───────────────────────────────────────
|
|
664
|
+
function copyDirFiltered(src, dest, excludeSet) {
|
|
210
665
|
fs.mkdirSync(dest, { recursive: true });
|
|
211
|
-
|
|
212
666
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
213
667
|
for (const entry of entries) {
|
|
668
|
+
const baseName = entry.name.replace(/\.md$/, '');
|
|
669
|
+
if (excludeSet.has(baseName)) continue;
|
|
670
|
+
|
|
214
671
|
const srcPath = path.join(src, entry.name);
|
|
215
672
|
const destPath = path.join(dest, entry.name);
|
|
216
|
-
|
|
217
673
|
if (entry.isDirectory()) {
|
|
218
674
|
copyDirRecursive(srcPath, destPath);
|
|
219
675
|
} else {
|
|
@@ -222,65 +678,89 @@ function copyDirRecursive(src, dest) {
|
|
|
222
678
|
}
|
|
223
679
|
}
|
|
224
680
|
|
|
225
|
-
// ──
|
|
226
|
-
function
|
|
227
|
-
|
|
228
|
-
|
|
681
|
+
// ── Write .agvrc ───────────────────────────────────────────────────────────────
|
|
682
|
+
function writeAgvrc(targetDir, mode, selectedStacks, deselected) {
|
|
683
|
+
const rcPath = path.join(targetDir, AGENT_DIR, CONFIG_FILE);
|
|
684
|
+
const config = {
|
|
685
|
+
version: require('../package.json').version,
|
|
686
|
+
mode,
|
|
687
|
+
stacks: [...selectedStacks].sort(),
|
|
688
|
+
deselected_rules: deselected.rules ? [...deselected.rules].sort() : [],
|
|
689
|
+
deselected_core_skills: deselected.coreSkills ? [...deselected.coreSkills].sort() : [],
|
|
690
|
+
deselected_agents: deselected.agents ? [...deselected.agents].sort() : [],
|
|
691
|
+
deselected_workflows: deselected.workflows ? [...deselected.workflows].sort() : [],
|
|
692
|
+
installed_at: new Date().toISOString(),
|
|
693
|
+
};
|
|
694
|
+
fs.writeFileSync(rcPath, JSON.stringify(config, null, 2) + '\n');
|
|
695
|
+
}
|
|
229
696
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
697
|
+
// ── Read existing .agvrc ───────────────────────────────────────────────────────
|
|
698
|
+
function readAgvrc(targetDir) {
|
|
699
|
+
const rcPath = path.join(targetDir, AGENT_DIR, CONFIG_FILE);
|
|
700
|
+
if (!fs.existsSync(rcPath)) return null;
|
|
701
|
+
try {
|
|
702
|
+
return JSON.parse(fs.readFileSync(rcPath, 'utf8'));
|
|
703
|
+
} catch {
|
|
704
|
+
return null;
|
|
238
705
|
}
|
|
239
|
-
return count;
|
|
240
706
|
}
|
|
241
707
|
|
|
242
|
-
// ──
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
|
|
708
|
+
// ── Read manifest from extracted archive ───────────────────────────────────────
|
|
709
|
+
function readManifest(tarballPath) {
|
|
710
|
+
const tmpExtractDir = path.join(os.tmpdir(), `awesome-agv-manifest-${Date.now()}`);
|
|
711
|
+
fs.mkdirSync(tmpExtractDir, { recursive: true });
|
|
246
712
|
|
|
247
|
-
|
|
248
|
-
|
|
713
|
+
try {
|
|
714
|
+
// Extract only the manifest file
|
|
715
|
+
try {
|
|
716
|
+
execSync(
|
|
717
|
+
`tar -xzf "${tarballPath}" --strip-components=1 -C "${tmpExtractDir}" "${REPO_NAME}-${BRANCH}/agv.config.json"`,
|
|
718
|
+
{ stdio: 'pipe' }
|
|
719
|
+
);
|
|
720
|
+
} catch {
|
|
721
|
+
// Fallback: extract everything and find it
|
|
722
|
+
try {
|
|
723
|
+
execSync(`tar -xzf "${tarballPath}" -C "${tmpExtractDir}"`, { stdio: 'pipe' });
|
|
724
|
+
} catch {
|
|
725
|
+
// Ignore extraction failures — we'll fall back to bundled
|
|
726
|
+
}
|
|
727
|
+
}
|
|
249
728
|
|
|
250
|
-
|
|
251
|
-
|
|
729
|
+
// Try both locations in the extracted archive
|
|
730
|
+
let manifestPath = path.join(tmpExtractDir, 'agv.config.json');
|
|
731
|
+
if (!fs.existsSync(manifestPath)) {
|
|
732
|
+
manifestPath = path.join(tmpExtractDir, `${REPO_NAME}-${BRANCH}`, 'agv.config.json');
|
|
733
|
+
}
|
|
252
734
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
735
|
+
// Fallback: bundled copy shipped with the CLI package
|
|
736
|
+
if (!fs.existsSync(manifestPath)) {
|
|
737
|
+
// When installed via npm, layout is: cli/bin/awesome-agv.js + cli/agv.config.json
|
|
738
|
+
const bundledPath = path.resolve(__dirname, '..', 'agv.config.json');
|
|
739
|
+
if (fs.existsSync(bundledPath)) {
|
|
740
|
+
manifestPath = bundledPath;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
260
743
|
|
|
261
|
-
|
|
262
|
-
|
|
744
|
+
// Fallback: repo root (when running from source checkout)
|
|
745
|
+
if (!fs.existsSync(manifestPath)) {
|
|
746
|
+
const repoRootPath = path.resolve(__dirname, '..', '..', 'agv.config.json');
|
|
747
|
+
if (fs.existsSync(repoRootPath)) {
|
|
748
|
+
manifestPath = repoRootPath;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
263
751
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
`${color.magenta}awesome-agv${color.cyan}${color.bold} ${color.dim}v${version}${color.cyan}${color.bold}`
|
|
268
|
-
);
|
|
269
|
-
const subtitleColored = subtitlePadded.replace(
|
|
270
|
-
subtitleText,
|
|
271
|
-
`${color.reset}${color.dim}${subtitleText}${color.cyan}${color.bold}`
|
|
272
|
-
);
|
|
752
|
+
if (!fs.existsSync(manifestPath)) {
|
|
753
|
+
throw new Error('agv.config.json not found in archive or CLI bundle');
|
|
754
|
+
}
|
|
273
755
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
╚${border}╝${color.reset}
|
|
279
|
-
`);
|
|
756
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
757
|
+
} finally {
|
|
758
|
+
fs.rmSync(tmpExtractDir, { recursive: true, force: true });
|
|
759
|
+
}
|
|
280
760
|
}
|
|
281
761
|
|
|
282
762
|
// ── Print Success Summary ──────────────────────────────────────────────────────
|
|
283
|
-
function printSuccess(targetDir) {
|
|
763
|
+
function printSuccess(targetDir, mode, selectedStacks) {
|
|
284
764
|
const agentDir = path.join(targetDir, AGENT_DIR);
|
|
285
765
|
const rulesDir = path.join(agentDir, 'rules');
|
|
286
766
|
const skillsDir = path.join(agentDir, 'skills');
|
|
@@ -301,27 +781,40 @@ function printSuccess(targetDir) {
|
|
|
301
781
|
: 0;
|
|
302
782
|
const totalFiles = countFiles(agentDir);
|
|
303
783
|
|
|
784
|
+
const modeLabel =
|
|
785
|
+
mode === 'default'
|
|
786
|
+
? `${icon.rocket} Default`
|
|
787
|
+
: mode === 'curated'
|
|
788
|
+
? `${icon.target} Curated`
|
|
789
|
+
: `${icon.gear} Advanced`;
|
|
790
|
+
|
|
304
791
|
console.log(`
|
|
305
|
-
${
|
|
792
|
+
${c.green}${c.bold} Installation complete! ${icon.rocket}${c.reset}
|
|
306
793
|
|
|
307
|
-
${
|
|
794
|
+
${icon.check} ${c.bold}${totalFiles} files${c.reset} installed to ${c.cyan}${path.relative(process.cwd(), agentDir) || AGENT_DIR}/${c.reset}
|
|
795
|
+
${c.dim}Mode: ${modeLabel}${c.reset}
|
|
308
796
|
|
|
309
|
-
${
|
|
310
|
-
${
|
|
311
|
-
${
|
|
312
|
-
🤖 ${
|
|
797
|
+
${icon.book} ${c.bold}${rulesCount}${c.reset} Rules ${c.dim}Security, architecture, testing standards${c.reset}
|
|
798
|
+
${icon.tool} ${c.bold}${skillsCount}${c.reset} Skills ${c.dim}Debugging, design, code review, language idioms${c.reset}
|
|
799
|
+
${icon.cycle} ${c.bold}${workflowsCount}${c.reset} Workflows ${c.dim}End-to-end dev processes${c.reset}
|
|
800
|
+
🤖 ${c.bold}${agentsCount}${c.reset} Agents ${c.dim}Specialized multi-agent personas${c.reset}
|
|
801
|
+
`);
|
|
802
|
+
|
|
803
|
+
if (selectedStacks && selectedStacks.size > 0) {
|
|
804
|
+
const stackLabels = [...selectedStacks].sort().join(', ');
|
|
805
|
+
console.log(` ${icon.arrow} ${c.dim}Stacks: ${stackLabels}${c.reset}`);
|
|
806
|
+
}
|
|
313
807
|
|
|
314
|
-
|
|
315
|
-
|
|
808
|
+
console.log(`
|
|
809
|
+
${icon.arrow} ${c.dim}Your AI agent will automatically pick up the${c.reset}
|
|
810
|
+
${c.dim}${AGENT_DIR}/ directory. No additional configuration needed.${c.reset}
|
|
316
811
|
|
|
317
|
-
💡 ${
|
|
318
|
-
${
|
|
319
|
-
${
|
|
320
|
-
${
|
|
321
|
-
${color.cyan}/bugfix${color.reset} ${color.dim}Fix a bug fast${color.reset}
|
|
322
|
-
${color.dim}Rules and skills are modular — use with or without workflows.${color.reset}
|
|
812
|
+
💡 ${c.bold}Quick start${c.reset} — open a chat with your agent and try:
|
|
813
|
+
${c.cyan}/workflow-solo${c.reset} ${c.dim}Build a feature end-to-end (single agent)${c.reset}
|
|
814
|
+
${c.cyan}/workflow-team${c.reset} ${c.dim}Build with multi-agent orchestration${c.reset}
|
|
815
|
+
${c.cyan}/audit${c.reset} ${c.dim}Review code quality${c.reset}
|
|
323
816
|
|
|
324
|
-
${
|
|
817
|
+
${icon.shield} ${c.dim}Learn more: ${c.cyan}https://github.com/${REPO_OWNER}/${REPO_NAME}${c.reset}
|
|
325
818
|
`);
|
|
326
819
|
}
|
|
327
820
|
|
|
@@ -338,52 +831,191 @@ async function main() {
|
|
|
338
831
|
|
|
339
832
|
const targetAgentDir = path.join(options.targetDir, AGENT_DIR);
|
|
340
833
|
|
|
341
|
-
// Check
|
|
342
|
-
|
|
343
|
-
if (!options.force) {
|
|
344
|
-
console.log(
|
|
345
|
-
` ${icons.warn} ${color.yellow}An existing ${AGENT_DIR}/ directory was found at:${color.reset}`
|
|
346
|
-
);
|
|
347
|
-
console.log(` ${color.dim}${targetAgentDir}${color.reset}\n`);
|
|
834
|
+
// ── Check existing .agvrc for additive stacks ──
|
|
835
|
+
const existingRc = readAgvrc(options.targetDir);
|
|
348
836
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
837
|
+
// ── Handle additive --stacks with existing config ──
|
|
838
|
+
if (options.stacks && existingRc && !options.force) {
|
|
839
|
+
console.log(` ${icon.arrow} Found existing config (${existingRc.mode}: ${existingRc.stacks.join(', ')})`);
|
|
840
|
+
console.log(` ${icon.arrow} Adding stacks: ${options.stacks.join(', ')}`);
|
|
841
|
+
const mergedStacks = new Set([...existingRc.stacks, ...options.stacks]);
|
|
842
|
+
options.stacks = [...mergedStacks];
|
|
843
|
+
options._additive = true;
|
|
844
|
+
}
|
|
352
845
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
846
|
+
// ── Handle existing .agents directory ──
|
|
847
|
+
if (fs.existsSync(targetAgentDir)) {
|
|
848
|
+
if (!options.force && !options._additive) {
|
|
849
|
+
// Non-TTY environments can't prompt — auto-overwrite
|
|
850
|
+
if (!process.stdin.isTTY) {
|
|
851
|
+
console.log(` ${icon.arrow} ${c.dim}Existing ${AGENT_DIR}/ found — overwriting (non-interactive mode).${c.reset}`);
|
|
852
|
+
} else if (existingRc && !options.stacks && !options.all) {
|
|
853
|
+
// Offer to re-use existing config
|
|
854
|
+
const stackList = existingRc.stacks.join(', ');
|
|
855
|
+
console.log(
|
|
856
|
+
` ${icon.arrow} ${c.dim}Found existing config${c.reset} ${c.bold}(${existingRc.mode}: ${stackList})${c.reset}`
|
|
857
|
+
);
|
|
858
|
+
const answer = await promptUser(
|
|
859
|
+
` ${c.bold}Re-install with same config?${c.reset} ${c.dim}(Y/n)${c.reset} `
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
if (answer === '' || answer === 'y' || answer === 'yes') {
|
|
863
|
+
options.stacks = existingRc.stacks;
|
|
864
|
+
options._mode = existingRc.mode;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (process.stdin.isTTY && !options.stacks && !options.all) {
|
|
869
|
+
console.log(
|
|
870
|
+
` ${icon.warn} ${c.yellow}An existing ${AGENT_DIR}/ directory was found at:${c.reset}`
|
|
871
|
+
);
|
|
872
|
+
console.log(` ${c.dim}${targetAgentDir}${c.reset}\n`);
|
|
873
|
+
const answer = await promptUser(
|
|
874
|
+
` ${c.bold}Overwrite? ${c.reset}${c.dim}(y/N)${c.reset} `
|
|
875
|
+
);
|
|
876
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
877
|
+
console.log(`\n ${icon.arrow} ${c.dim}Installation cancelled.${c.reset}\n`);
|
|
878
|
+
process.exit(0);
|
|
879
|
+
}
|
|
356
880
|
}
|
|
357
881
|
}
|
|
358
882
|
|
|
359
|
-
|
|
360
|
-
console.log(` ${icons.arrow} Removing existing ${AGENT_DIR}/ directory...`);
|
|
883
|
+
console.log(` ${icon.arrow} Removing existing ${AGENT_DIR}/ directory...`);
|
|
361
884
|
fs.rmSync(targetAgentDir, { recursive: true, force: true });
|
|
362
885
|
}
|
|
363
886
|
|
|
364
|
-
// Download
|
|
887
|
+
// ── Download ──
|
|
365
888
|
const tmpDir = os.tmpdir();
|
|
366
889
|
const tarballPath = path.join(tmpDir, `awesome-agv-${Date.now()}.tar.gz`);
|
|
367
890
|
|
|
368
891
|
try {
|
|
369
|
-
console.log(` ${
|
|
892
|
+
console.log(` ${icon.arrow} Fetching latest configuration from GitHub...`);
|
|
370
893
|
await downloadToFile(TARBALL_URL, tarballPath);
|
|
371
894
|
|
|
372
|
-
|
|
373
|
-
|
|
895
|
+
// ── Read manifest ──
|
|
896
|
+
console.log(` ${icon.arrow} Reading manifest...`);
|
|
897
|
+
const manifest = readManifest(tarballPath);
|
|
898
|
+
|
|
899
|
+
// ── Determine what to install ──
|
|
900
|
+
let mode = options._mode || 'default';
|
|
901
|
+
let selectedStackKeys;
|
|
902
|
+
let deselected = { rules: null, coreSkills: null, agents: null, workflows: null };
|
|
903
|
+
let extractConfig = { ...manifest, _deselectedRules: null, _deselectedAgents: null, _deselectedWorkflows: null };
|
|
904
|
+
|
|
905
|
+
if (options.all) {
|
|
906
|
+
// ── Default: install everything ──
|
|
907
|
+
mode = 'default';
|
|
908
|
+
selectedStackKeys = new Set(Object.keys(manifest.skills.stacks));
|
|
909
|
+
} else if (options.stacks) {
|
|
910
|
+
// ── Non-interactive stacks ──
|
|
911
|
+
mode = options._mode || 'curated';
|
|
912
|
+
// Validate stack names against manifest
|
|
913
|
+
const validStackKeys = new Set(Object.keys(manifest.skills.stacks));
|
|
914
|
+
const unknownStacks = options.stacks.filter((s) => !validStackKeys.has(s));
|
|
915
|
+
if (unknownStacks.length > 0) {
|
|
916
|
+
console.log(` ${icon.warn} ${c.yellow}Unknown stack(s): ${unknownStacks.join(', ')}${c.reset}`);
|
|
917
|
+
console.log(` ${c.dim} Available: ${[...validStackKeys].sort().join(', ')}${c.reset}`);
|
|
918
|
+
console.log('');
|
|
919
|
+
}
|
|
920
|
+
const validStacks = options.stacks.filter((s) => validStackKeys.has(s));
|
|
921
|
+
const resolvedStacks = resolveRequires(new Set(validStacks), manifest);
|
|
922
|
+
selectedStackKeys = resolvedStacks;
|
|
923
|
+
} else {
|
|
924
|
+
// ── Interactive mode selection (requires TTY) ──
|
|
925
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
926
|
+
// Non-interactive environment (CI, piped input) — default to full install
|
|
927
|
+
console.log(` ${icon.arrow} ${c.dim}Non-interactive environment detected — defaulting to full install.${c.reset}`);
|
|
928
|
+
console.log(` ${c.dim} Use --stacks <list> or --all to control installation non-interactively.${c.reset}`);
|
|
929
|
+
console.log('');
|
|
930
|
+
mode = 'default';
|
|
931
|
+
selectedStackKeys = new Set(Object.keys(manifest.skills.stacks));
|
|
932
|
+
} else {
|
|
933
|
+
mode = await promptSelect('How would you like to install?', [
|
|
934
|
+
{ value: 'default', label: `${icon.rocket} ${c.bold}Default${c.reset} — Install everything. The full arsenal. ${c.dim}(recommended)${c.reset}` },
|
|
935
|
+
{ value: 'curated', label: `${icon.target} ${c.bold}Curated${c.reset} — Pick your languages & frameworks. Core always included.` },
|
|
936
|
+
{ value: 'advanced', label: `${icon.gear} ${c.bold}Advanced${c.reset} — Full control. Pick rules, skills, agents, everything.` },
|
|
937
|
+
]);
|
|
938
|
+
|
|
939
|
+
if (mode === 'default') {
|
|
940
|
+
selectedStackKeys = new Set(Object.keys(manifest.skills.stacks));
|
|
941
|
+
} else {
|
|
942
|
+
// ── Auto-detect stacks ──
|
|
943
|
+
const detected = detectStacks(options.targetDir, manifest);
|
|
944
|
+
if (detected.size > 0) {
|
|
945
|
+
console.log(` ${c.dim}Auto-detected from project files:${c.reset}`);
|
|
946
|
+
for (const key of detected) {
|
|
947
|
+
const stack = manifest.skills.stacks[key];
|
|
948
|
+
const detectedFile = stack.detect?.[0] || '';
|
|
949
|
+
console.log(` ${icon.check} ${c.bold}${stack.label}${c.reset} ${c.dim}(found ${detectedFile})${c.reset}`);
|
|
950
|
+
}
|
|
951
|
+
console.log('');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ── Build stack selection tree ──
|
|
955
|
+
const treeItems = buildStackTree(manifest);
|
|
956
|
+
const selectItems = treeItems.map((item) => {
|
|
957
|
+
if (item.separator) return item;
|
|
958
|
+
return {
|
|
959
|
+
...item,
|
|
960
|
+
checked: detected.has(item.key),
|
|
961
|
+
disabled: false,
|
|
962
|
+
};
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
const selectedItems = await promptMultiSelect(
|
|
966
|
+
mode === 'curated'
|
|
967
|
+
? `${icon.target} Curated — Pick your stacks. Core is always included.`
|
|
968
|
+
: `${icon.gear} Advanced — Select stacks:`,
|
|
969
|
+
selectItems,
|
|
970
|
+
(items) => {
|
|
971
|
+
const count = items.filter((i) => i.checked && !i.separator).length;
|
|
972
|
+
return `${c.dim}${count} stack(s) selected · (space to toggle, enter to confirm)${c.reset}`;
|
|
973
|
+
}
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
const rawKeys = new Set(selectedItems.map((i) => i.key));
|
|
977
|
+
selectedStackKeys = resolveRequires(rawKeys, manifest);
|
|
978
|
+
|
|
979
|
+
// ── Advanced mode: granular selection (future) ──
|
|
980
|
+
if (mode === 'advanced') {
|
|
981
|
+
console.log(` ${c.dim}Advanced mode: all rules, agents, and workflows are included.${c.reset}`);
|
|
982
|
+
console.log(` ${c.dim}To customize further, edit ${AGENT_DIR}/ after installation.${c.reset}`);
|
|
983
|
+
console.log('');
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
} // end TTY block
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
// ── Build final skill set ──
|
|
991
|
+
const selectedSkills = buildSkillsSet(selectedStackKeys, manifest);
|
|
992
|
+
|
|
993
|
+
// Handle advanced mode deselected core skills
|
|
994
|
+
if (deselected.coreSkills && deselected.coreSkills.size > 0) {
|
|
995
|
+
for (const skill of deselected.coreSkills) {
|
|
996
|
+
selectedSkills.delete(skill);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ── Extract ──
|
|
1001
|
+
console.log(` ${icon.arrow} Extracting ${AGENT_DIR}/ directory...`);
|
|
1002
|
+
const actualSkillsCopied = extractFiltered(tarballPath, options.targetDir, selectedSkills, extractConfig);
|
|
1003
|
+
console.log(` ${c.dim} ${actualSkillsCopied} skills installed${c.reset}`);
|
|
1004
|
+
|
|
1005
|
+
// ── Write .agvrc ──
|
|
1006
|
+
writeAgvrc(options.targetDir, mode, selectedStackKeys, deselected);
|
|
374
1007
|
|
|
375
|
-
|
|
1008
|
+
// ── Success ──
|
|
1009
|
+
printSuccess(options.targetDir, mode, selectedStackKeys);
|
|
376
1010
|
} catch (err) {
|
|
377
|
-
console.error(`\n ${
|
|
1011
|
+
console.error(`\n ${icon.error} ${c.red}Installation failed:${c.reset} ${err.message}\n`);
|
|
378
1012
|
|
|
379
|
-
// Clean up partial installation
|
|
380
1013
|
if (fs.existsSync(targetAgentDir)) {
|
|
381
1014
|
fs.rmSync(targetAgentDir, { recursive: true, force: true });
|
|
382
1015
|
}
|
|
383
1016
|
|
|
384
1017
|
process.exit(1);
|
|
385
1018
|
} finally {
|
|
386
|
-
// Clean up tarball
|
|
387
1019
|
if (fs.existsSync(tarballPath)) {
|
|
388
1020
|
fs.unlinkSync(tarballPath);
|
|
389
1021
|
}
|