awesome-agv 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/bin/awesome-agv.js +352 -0
- package/package.json +35 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { createInterface } = require('readline');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { createGunzip } = require('zlib');
|
|
13
|
+
|
|
14
|
+
// ── ANSI Colors (zero-dependency) ──────────────────────────────────────────────
|
|
15
|
+
const color = {
|
|
16
|
+
reset: '\x1b[0m',
|
|
17
|
+
bold: '\x1b[1m',
|
|
18
|
+
dim: '\x1b[2m',
|
|
19
|
+
green: '\x1b[32m',
|
|
20
|
+
yellow: '\x1b[33m',
|
|
21
|
+
red: '\x1b[31m',
|
|
22
|
+
cyan: '\x1b[36m',
|
|
23
|
+
magenta: '\x1b[35m',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const icons = {
|
|
27
|
+
check: `${color.green}✓${color.reset}`,
|
|
28
|
+
warn: `${color.yellow}⚠${color.reset}`,
|
|
29
|
+
error: `${color.red}✗${color.reset}`,
|
|
30
|
+
arrow: `${color.cyan}→${color.reset}`,
|
|
31
|
+
rocket: '🚀',
|
|
32
|
+
shield: '🛡️',
|
|
33
|
+
gear: '⚙️',
|
|
34
|
+
book: '📏',
|
|
35
|
+
tool: '🛠️',
|
|
36
|
+
cycle: '🔄',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
40
|
+
const REPO_OWNER = 'irahardianto';
|
|
41
|
+
const REPO_NAME = 'awesome-agv';
|
|
42
|
+
const BRANCH = 'main';
|
|
43
|
+
const TARBALL_URL = `https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/heads/${BRANCH}.tar.gz`;
|
|
44
|
+
const AGENT_DIR = '.agent';
|
|
45
|
+
|
|
46
|
+
// ── CLI Argument Parsing ───────────────────────────────────────────────────────
|
|
47
|
+
function parseArgs(argv) {
|
|
48
|
+
const args = argv.slice(2);
|
|
49
|
+
const options = {
|
|
50
|
+
force: false,
|
|
51
|
+
targetDir: process.cwd(),
|
|
52
|
+
help: false,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const arg of args) {
|
|
56
|
+
if (arg === '--force' || arg === '-f') {
|
|
57
|
+
options.force = true;
|
|
58
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
59
|
+
options.help = true;
|
|
60
|
+
} else if (!arg.startsWith('-')) {
|
|
61
|
+
options.targetDir = path.resolve(arg);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return options;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Help Text ──────────────────────────────────────────────────────────────────
|
|
69
|
+
function printHelp() {
|
|
70
|
+
console.log(`
|
|
71
|
+
${color.bold}awesome-agv${color.reset} — Install the Awesome AGV AI Agent configuration suite
|
|
72
|
+
|
|
73
|
+
${color.bold}USAGE${color.reset}
|
|
74
|
+
npx awesome-agv [target-dir] [options]
|
|
75
|
+
|
|
76
|
+
${color.bold}ARGUMENTS${color.reset}
|
|
77
|
+
target-dir Directory to install into (default: current directory)
|
|
78
|
+
|
|
79
|
+
${color.bold}OPTIONS${color.reset}
|
|
80
|
+
-f, --force Overwrite existing .agent directory without prompting
|
|
81
|
+
-h, --help Show this help message
|
|
82
|
+
|
|
83
|
+
${color.bold}EXAMPLES${color.reset}
|
|
84
|
+
${color.dim}# Install into current directory${color.reset}
|
|
85
|
+
npx awesome-agv
|
|
86
|
+
|
|
87
|
+
${color.dim}# Install into a specific project${color.reset}
|
|
88
|
+
npx awesome-agv ./my-project
|
|
89
|
+
|
|
90
|
+
${color.dim}# Overwrite existing installation${color.reset}
|
|
91
|
+
npx awesome-agv --force
|
|
92
|
+
|
|
93
|
+
${color.bold}WHAT GETS INSTALLED${color.reset}
|
|
94
|
+
${icons.book} 30 Rules — Security, architecture, testing, DevOps standards
|
|
95
|
+
${icons.tool} 7 Skills — Debugging, design, code review, and more
|
|
96
|
+
${icons.cycle} 10 Workflows — End-to-end development processes
|
|
97
|
+
|
|
98
|
+
${color.dim}https://github.com/${REPO_OWNER}/${REPO_NAME}${color.reset}
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── User Prompt ────────────────────────────────────────────────────────────────
|
|
103
|
+
function promptUser(question) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const rl = createInterface({
|
|
106
|
+
input: process.stdin,
|
|
107
|
+
output: process.stdout,
|
|
108
|
+
});
|
|
109
|
+
rl.question(question, (answer) => {
|
|
110
|
+
rl.close();
|
|
111
|
+
resolve(answer.trim().toLowerCase());
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── HTTP Download with Redirect Following ──────────────────────────────────────
|
|
117
|
+
function downloadToFile(url, destPath, maxRedirects = 5) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
if (maxRedirects <= 0) {
|
|
120
|
+
reject(new Error('Too many redirects'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const client = url.startsWith('https') ? https : http;
|
|
125
|
+
client
|
|
126
|
+
.get(url, (res) => {
|
|
127
|
+
// Follow redirects (GitHub returns 302)
|
|
128
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
129
|
+
res.resume(); // Consume response to free up memory
|
|
130
|
+
downloadToFile(res.headers.location, destPath, maxRedirects - 1)
|
|
131
|
+
.then(resolve)
|
|
132
|
+
.catch(reject);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (res.statusCode !== 200) {
|
|
137
|
+
res.resume();
|
|
138
|
+
reject(new Error(`Failed to download: HTTP ${res.statusCode}`));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const fileStream = fs.createWriteStream(destPath);
|
|
143
|
+
res.pipe(fileStream);
|
|
144
|
+
fileStream.on('finish', () => {
|
|
145
|
+
fileStream.close(resolve);
|
|
146
|
+
});
|
|
147
|
+
fileStream.on('error', (err) => {
|
|
148
|
+
fs.unlinkSync(destPath);
|
|
149
|
+
reject(err);
|
|
150
|
+
});
|
|
151
|
+
})
|
|
152
|
+
.on('error', reject);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Extract .agent directory from tarball ──────────────────────────────────────
|
|
157
|
+
function extractAgentDir(tarballPath, targetDir) {
|
|
158
|
+
// The tarball from GitHub has a root directory like: awesome-agv-main/
|
|
159
|
+
// We need to extract awesome-agv-main/.agent/ → targetDir/.agent/
|
|
160
|
+
const stripPrefix = `${REPO_NAME}-${BRANCH}/${AGENT_DIR}`;
|
|
161
|
+
|
|
162
|
+
const agentTargetDir = path.join(targetDir, AGENT_DIR);
|
|
163
|
+
|
|
164
|
+
// Ensure target exists
|
|
165
|
+
fs.mkdirSync(agentTargetDir, { recursive: true });
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Use system tar (available on macOS and Linux)
|
|
169
|
+
execSync(
|
|
170
|
+
`tar -xzf "${tarballPath}" --strip-components=2 -C "${agentTargetDir}" "${stripPrefix}"`,
|
|
171
|
+
{ stdio: 'pipe' }
|
|
172
|
+
);
|
|
173
|
+
} catch {
|
|
174
|
+
// Fallback: manual extraction using Node.js streams
|
|
175
|
+
extractManually(tarballPath, targetDir, stripPrefix);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Manual tar extraction fallback (for Windows or missing tar) ────────────────
|
|
180
|
+
function extractManually(tarballPath, targetDir, stripPrefix) {
|
|
181
|
+
// Read the gzipped tarball
|
|
182
|
+
const gzipData = fs.readFileSync(tarballPath);
|
|
183
|
+
|
|
184
|
+
// Decompress using zlib
|
|
185
|
+
const { execFileSync } = require('child_process');
|
|
186
|
+
const tmpExtractDir = path.join(os.tmpdir(), `awesome-agv-extract-${Date.now()}`);
|
|
187
|
+
fs.mkdirSync(tmpExtractDir, { recursive: true });
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
// Try using tar with different flags (Windows Git Bash compatibility)
|
|
191
|
+
execFileSync('tar', ['-xzf', tarballPath, '-C', tmpExtractDir], { stdio: 'pipe' });
|
|
192
|
+
|
|
193
|
+
// Find the extracted .agent directory
|
|
194
|
+
const extractedRoot = path.join(tmpExtractDir, `${REPO_NAME}-${BRANCH}`, AGENT_DIR);
|
|
195
|
+
|
|
196
|
+
if (fs.existsSync(extractedRoot)) {
|
|
197
|
+
copyDirRecursive(extractedRoot, path.join(targetDir, AGENT_DIR));
|
|
198
|
+
} else {
|
|
199
|
+
throw new Error(`Could not find ${AGENT_DIR} in downloaded archive`);
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
// Clean up temp extraction dir
|
|
203
|
+
fs.rmSync(tmpExtractDir, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Recursive directory copy ───────────────────────────────────────────────────
|
|
208
|
+
function copyDirRecursive(src, dest) {
|
|
209
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
210
|
+
|
|
211
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
const srcPath = path.join(src, entry.name);
|
|
214
|
+
const destPath = path.join(dest, entry.name);
|
|
215
|
+
|
|
216
|
+
if (entry.isDirectory()) {
|
|
217
|
+
copyDirRecursive(srcPath, destPath);
|
|
218
|
+
} else {
|
|
219
|
+
fs.copyFileSync(srcPath, destPath);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Count files recursively ────────────────────────────────────────────────────
|
|
225
|
+
function countFiles(dirPath) {
|
|
226
|
+
let count = 0;
|
|
227
|
+
if (!fs.existsSync(dirPath)) return count;
|
|
228
|
+
|
|
229
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
232
|
+
if (entry.isDirectory()) {
|
|
233
|
+
count += countFiles(fullPath);
|
|
234
|
+
} else {
|
|
235
|
+
count++;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return count;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Print Banner ───────────────────────────────────────────────────────────────
|
|
242
|
+
function printBanner() {
|
|
243
|
+
// Load version from package.json
|
|
244
|
+
const { version } = require('../package.json');
|
|
245
|
+
console.log(`
|
|
246
|
+
${color.bold}${color.cyan} ╔══════════════════════════════════════════╗
|
|
247
|
+
║ ${color.magenta}awesome-agv${color.cyan} ${color.dim}v${version}${color.cyan}${color.bold} ║
|
|
248
|
+
║ ${color.reset}${color.dim}Awesome AGV AI Agent Configuration${color.cyan}${color.bold} ║
|
|
249
|
+
╚══════════════════════════════════════════╝${color.reset}
|
|
250
|
+
`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Print Success Summary ──────────────────────────────────────────────────────
|
|
254
|
+
function printSuccess(targetDir) {
|
|
255
|
+
const agentDir = path.join(targetDir, AGENT_DIR);
|
|
256
|
+
const rulesDir = path.join(agentDir, 'rules');
|
|
257
|
+
const skillsDir = path.join(agentDir, 'skills');
|
|
258
|
+
const workflowsDir = path.join(agentDir, 'workflows');
|
|
259
|
+
|
|
260
|
+
const rulesCount = fs.existsSync(rulesDir)
|
|
261
|
+
? fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md')).length
|
|
262
|
+
: 0;
|
|
263
|
+
const skillsCount = fs.existsSync(skillsDir)
|
|
264
|
+
? fs.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).length
|
|
265
|
+
: 0;
|
|
266
|
+
const workflowsCount = fs.existsSync(workflowsDir)
|
|
267
|
+
? fs.readdirSync(workflowsDir).filter((f) => f.endsWith('.md')).length
|
|
268
|
+
: 0;
|
|
269
|
+
const totalFiles = countFiles(agentDir);
|
|
270
|
+
|
|
271
|
+
console.log(`
|
|
272
|
+
${color.green}${color.bold} Installation complete! ${icons.rocket}${color.reset}
|
|
273
|
+
|
|
274
|
+
${icons.check} ${color.bold}${totalFiles} files${color.reset} installed to ${color.cyan}${path.relative(process.cwd(), agentDir) || AGENT_DIR}/${color.reset}
|
|
275
|
+
|
|
276
|
+
${icons.book} ${color.bold}${rulesCount}${color.reset} Rules ${color.dim}Security, architecture, testing standards${color.reset}
|
|
277
|
+
${icons.tool} ${color.bold}${skillsCount}${color.reset} Skills ${color.dim}Debugging, design, code review${color.reset}
|
|
278
|
+
${icons.cycle} ${color.bold}${workflowsCount}${color.reset} Workflows ${color.dim}End-to-end dev processes${color.reset}
|
|
279
|
+
|
|
280
|
+
${icons.arrow} ${color.dim}Your AI agent will automatically pick up the${color.reset}
|
|
281
|
+
${color.dim}${AGENT_DIR}/ directory. No additional configuration needed.${color.reset}
|
|
282
|
+
|
|
283
|
+
${icons.shield} ${color.dim}Learn more: ${color.cyan}https://github.com/${REPO_OWNER}/${REPO_NAME}${color.reset}
|
|
284
|
+
`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Main ───────────────────────────────────────────────────────────────────────
|
|
288
|
+
async function main() {
|
|
289
|
+
const options = parseArgs(process.argv);
|
|
290
|
+
|
|
291
|
+
if (options.help) {
|
|
292
|
+
printHelp();
|
|
293
|
+
process.exit(0);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
printBanner();
|
|
297
|
+
|
|
298
|
+
const targetAgentDir = path.join(options.targetDir, AGENT_DIR);
|
|
299
|
+
|
|
300
|
+
// Check if .agent already exists
|
|
301
|
+
if (fs.existsSync(targetAgentDir)) {
|
|
302
|
+
if (!options.force) {
|
|
303
|
+
console.log(
|
|
304
|
+
` ${icons.warn} ${color.yellow}An existing ${AGENT_DIR}/ directory was found at:${color.reset}`
|
|
305
|
+
);
|
|
306
|
+
console.log(` ${color.dim}${targetAgentDir}${color.reset}\n`);
|
|
307
|
+
|
|
308
|
+
const answer = await promptUser(
|
|
309
|
+
` ${color.bold}Overwrite? ${color.reset}${color.dim}(y/N)${color.reset} `
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
313
|
+
console.log(`\n ${icons.arrow} ${color.dim}Installation cancelled.${color.reset}\n`);
|
|
314
|
+
process.exit(0);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Remove existing .agent directory
|
|
319
|
+
console.log(` ${icons.arrow} Removing existing ${AGENT_DIR}/ directory...`);
|
|
320
|
+
fs.rmSync(targetAgentDir, { recursive: true, force: true });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Download tarball to a temp file
|
|
324
|
+
const tmpDir = os.tmpdir();
|
|
325
|
+
const tarballPath = path.join(tmpDir, `awesome-agv-${Date.now()}.tar.gz`);
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
console.log(` ${icons.arrow} Fetching latest configuration from GitHub...`);
|
|
329
|
+
await downloadToFile(TARBALL_URL, tarballPath);
|
|
330
|
+
|
|
331
|
+
console.log(` ${icons.arrow} Extracting ${AGENT_DIR}/ directory...`);
|
|
332
|
+
extractAgentDir(tarballPath, options.targetDir);
|
|
333
|
+
|
|
334
|
+
printSuccess(options.targetDir);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
console.error(`\n ${icons.error} ${color.red}Installation failed:${color.reset} ${err.message}\n`);
|
|
337
|
+
|
|
338
|
+
// Clean up partial installation
|
|
339
|
+
if (fs.existsSync(targetAgentDir)) {
|
|
340
|
+
fs.rmSync(targetAgentDir, { recursive: true, force: true });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
process.exit(1);
|
|
344
|
+
} finally {
|
|
345
|
+
// Clean up tarball
|
|
346
|
+
if (fs.existsSync(tarballPath)) {
|
|
347
|
+
fs.unlinkSync(tarballPath);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "awesome-agv",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "1-click installer for Awesome AGV — a rugged, high-quality configuration suite for AI Agents.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"awesome-agv": "./bin/awesome-agv.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"ai",
|
|
13
|
+
"agent",
|
|
14
|
+
"coding-assistant",
|
|
15
|
+
"configuration",
|
|
16
|
+
"rules",
|
|
17
|
+
"skills",
|
|
18
|
+
"workflows",
|
|
19
|
+
"awesome-agv",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/irahardianto/awesome-agv.git"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/irahardianto/awesome-agv",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/irahardianto/awesome-agv/issues"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"author": "irahardianto",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|