iterate-skill-installer 2.1.3

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 ADDED
@@ -0,0 +1,46 @@
1
+ # iterate-skill-installer
2
+
3
+ One-command installer for [iterate-skill](https://github.com/jingzhao-l/iterate-skill) across AI coding assistants.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # Interactive install — detects your AI assistants and lets you choose
9
+ npx iterate-skill-installer
10
+
11
+ # Install to a specific assistant only
12
+ npx iterate-skill-installer --ai trae
13
+ npx iterate-skill-installer --ai claude
14
+
15
+ # Install into a project directory instead of globally
16
+ npx iterate-skill-installer --target ./my-project
17
+
18
+ # Force overwrite existing skill files
19
+ npx iterate-skill-installer --force
20
+ ```
21
+
22
+ ## What it does
23
+
24
+ 1. Checks for Python 3 on your system.
25
+ 2. Fetches the latest iterate-skill release from GitHub.
26
+ 3. Downloads the release tarball and `SHA256SUMS.txt`.
27
+ 4. Verifies the tarball checksum.
28
+ 5. Extracts the release into a temporary directory.
29
+ 6. Runs the bundled Python install script (`scripts/install.py`) which:
30
+ - Detects installed AI coding assistants on your machine.
31
+ - Prompts you to select targets (default: all detected assistants).
32
+ - Copies the skill files into the correct skills directories.
33
+
34
+ ## Supported AI assistants
35
+
36
+ Trae, Claude / Claude Code, Cursor, Windsurf, GitHub Copilot, Codex, Gemini CLI, OpenCode, Aider, AiderDesk, Zed, Warp, Continue, Cline, Roo Code, Qoder, Augment, OpenClaw, Autohand Code CLI, IBM Bob, CodeArts Agent, Antigravity, Amp, Deep Agents, Kimi Code CLI, Astral.
37
+
38
+ ## Requirements
39
+
40
+ - Node.js 18+
41
+ - Python 3.9+
42
+ - `tar` command available on PATH (available by default on macOS, Linux, Windows 10+)
43
+
44
+ ## License
45
+
46
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * iterate-skill-installer CLI
5
+ *
6
+ * One-command installer for iterate-skill across AI coding assistants.
7
+ *
8
+ * Usage:
9
+ * npx iterate-skill-installer
10
+ * npx iterate-skill-installer --ai trae
11
+ * npx iterate-skill-installer --target /path/to/project
12
+ * npx iterate-skill-installer --global
13
+ *
14
+ * The installer downloads the latest GitHub release, verifies its SHA256
15
+ * checksum, then delegates file copying and assistant selection to the
16
+ * Python install script bundled in the release.
17
+ */
18
+
19
+ const { main } = require('../lib/installer');
20
+
21
+ function parseArgs(argv) {
22
+ const options = {
23
+ global: true,
24
+ ai: null,
25
+ target: null,
26
+ force: false,
27
+ token: process.env.GITHUB_TOKEN || null,
28
+ };
29
+
30
+ for (let i = 0; i < argv.length; i++) {
31
+ const arg = argv[i];
32
+ const next = argv[i + 1];
33
+
34
+ switch (arg) {
35
+ case '--ai':
36
+ if (!next) {
37
+ console.error('Error: --ai requires a value');
38
+ process.exit(1);
39
+ }
40
+ options.ai = next;
41
+ i++;
42
+ break;
43
+ case '--target':
44
+ if (!next) {
45
+ console.error('Error: --target requires a value');
46
+ process.exit(1);
47
+ }
48
+ options.target = next;
49
+ options.global = false;
50
+ i++;
51
+ break;
52
+ case '--global':
53
+ options.global = true;
54
+ options.target = null;
55
+ break;
56
+ case '--force':
57
+ options.force = true;
58
+ break;
59
+ case '--token':
60
+ if (!next) {
61
+ console.error('Error: --token requires a value');
62
+ process.exit(1);
63
+ }
64
+ options.token = next;
65
+ i++;
66
+ break;
67
+ case '-h':
68
+ case '--help':
69
+ printHelp();
70
+ process.exit(0);
71
+ break;
72
+ case '-v':
73
+ case '--version':
74
+ console.log(require('../package.json').version);
75
+ process.exit(0);
76
+ break;
77
+ default:
78
+ if (arg.startsWith('-')) {
79
+ console.error(`Error: unknown option ${arg}`);
80
+ process.exit(1);
81
+ }
82
+ break;
83
+ }
84
+ }
85
+
86
+ return options;
87
+ }
88
+
89
+ function printHelp() {
90
+ console.log(`
91
+ Usage: npx iterate-skill-installer [options]
92
+
93
+ Options:
94
+ --ai <assistant> Install only to the specified assistant (e.g. trae, claude, cursor).
95
+ --target <path> Install into a project directory instead of the global home dir.
96
+ --global Install into the user's home directory (default).
97
+ --force Overwrite existing skill files.
98
+ --token <token> GitHub token for higher API rate limits.
99
+ -h, --help Show this help message.
100
+ -v, --version Show version.
101
+
102
+ Examples:
103
+ npx iterate-skill-installer
104
+ npx iterate-skill-installer --ai trae
105
+ npx iterate-skill-installer --target ./my-project
106
+ `);
107
+ }
108
+
109
+ async function run() {
110
+ const options = parseArgs(process.argv.slice(2));
111
+ try {
112
+ const code = await main(options);
113
+ process.exit(code);
114
+ } catch (err) {
115
+ console.error(`\nUnexpected error: ${err.message}`);
116
+ process.exit(1);
117
+ }
118
+ }
119
+
120
+ run();
@@ -0,0 +1,352 @@
1
+ /**
2
+ * iterate-skill-installer — Node.js entry point for npx one-command install.
3
+ *
4
+ * This module orchestrates the cross-assistant installation of iterate-skill:
5
+ * 1. Detect Python / pip availability
6
+ * 2. Download the latest GitHub release tarball + checksum
7
+ * 3. Verify SHA256 checksum
8
+ * 4. Extract to a temporary directory
9
+ * 5. Run scripts/install.py to copy skill files into selected AI assistants
10
+ *
11
+ * The actual copy logic and interactive assistant selection live in the
12
+ * Python install script (scripts/install.py) so that behavior stays in one
13
+ * place and can be tested with the Python test suite.
14
+ */
15
+
16
+ const { spawn, execFile } = require('node:child_process');
17
+ const fs = require('node:fs');
18
+ const os = require('node:os');
19
+ const path = require('node:path');
20
+ const crypto = require('node:crypto');
21
+
22
+ const GITHUB_OWNER = 'jingzhao-l';
23
+ const GITHUB_REPO = 'iterate-skill';
24
+ const RELEASE_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`;
25
+
26
+ const ITERATE_BANNER = [
27
+ '██╗████████╗███████╗██████╗ █████╗ ████████╗███████╗',
28
+ '██║╚══██╔══╝██╔════╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝',
29
+ '██║ ██║ █████╗ ██████╔╝███████║ ██║ █████╗ ',
30
+ '██║ ██║ ██╔══╝ ██╔══██╗██╔══██║ ██║ ██╔══╝ ',
31
+ '██║ ██║ ███████╗██║ ██║██║ ██║ ██║ ███████╗',
32
+ '╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝',
33
+ ];
34
+
35
+ class InstallerError extends Error {}
36
+
37
+ function printBanner() {
38
+ console.log();
39
+ for (const line of ITERATE_BANNER) {
40
+ console.log(`\x1b[36m${line}\x1b[0m`);
41
+ }
42
+ console.log();
43
+ }
44
+
45
+ function info(message) {
46
+ console.log(`\x1b[34mℹ\x1b[0m ${message}`);
47
+ }
48
+
49
+ function success(message) {
50
+ console.log(`\x1b[32m✓\x1b[0m ${message}`);
51
+ }
52
+
53
+ function warning(message) {
54
+ console.log(`\x1b[33m⚠\x1b[0m ${message}`);
55
+ }
56
+
57
+ function error(message) {
58
+ console.error(`\x1b[31m✗\x1b[0m ${message}`);
59
+ }
60
+
61
+ function hint(message) {
62
+ console.log(`\x1b[2m ${message}\x1b[0m`);
63
+ }
64
+
65
+ function step(message) {
66
+ console.log(`\x1b[36m◆\x1b[0m ${message}`);
67
+ }
68
+
69
+ function frameSection(title, lines) {
70
+ const maxLen = Math.max(
71
+ title.length,
72
+ ...lines.map((l) => stripAnsi(l).length),
73
+ );
74
+ const innerWidth = maxLen + 2;
75
+ const top = `┌─ ${title} ${'─'.repeat(Math.max(0, innerWidth - title.length - 2))}┐`;
76
+ const bottom = `└${'─'.repeat(innerWidth + 1)}┘`;
77
+ console.log(top);
78
+ for (const line of lines) {
79
+ const visibleLen = stripAnsi(line).length;
80
+ const padding = ' '.repeat(Math.max(0, innerWidth - visibleLen));
81
+ console.log(`│ ${line}${padding}│`);
82
+ }
83
+ console.log(bottom);
84
+ }
85
+
86
+ function stripAnsi(str) {
87
+ // eslint-disable-next-line no-control-regex
88
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
89
+ }
90
+
91
+ async function findPython() {
92
+ for (const bin of ['python3', 'python']) {
93
+ const found = await commandExists(bin);
94
+ if (found) return bin;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ function commandExists(bin) {
100
+ return new Promise((resolve) => {
101
+ execFile(bin, ['--version'], (err) => {
102
+ resolve(!err);
103
+ });
104
+ });
105
+ }
106
+
107
+ async function fetchJson(url, token) {
108
+ // Prefer curl over Node.js fetch because curl uses the system CA store
109
+ // and avoids Node-specific certificate issues in some environments.
110
+ const args = ['-sSL', '--fail-with-body', '-H', 'Accept: application/vnd.github+json', '-H', 'X-GitHub-Api-Version: 2022-11-28', '-H', 'User-Agent: iterate-skill-installer'];
111
+ if (token) {
112
+ args.push('-H', `Authorization: Bearer ${token}`);
113
+ }
114
+ args.push(url);
115
+ const stdout = await runCommand('curl', args);
116
+ try {
117
+ return JSON.parse(stdout);
118
+ } catch (err) {
119
+ throw new InstallerError(`Failed to parse GitHub API response: ${err.message}`);
120
+ }
121
+ }
122
+
123
+ async function downloadFile(url, destPath, token) {
124
+ const args = ['-sSL', '--fail-with-body', '-o', destPath, '-H', 'User-Agent: iterate-skill-installer'];
125
+ if (token) {
126
+ args.push('-H', `Authorization: Bearer ${token}`);
127
+ }
128
+ args.push(url);
129
+ await runCommand('curl', args);
130
+ }
131
+
132
+ function sha256File(filePath) {
133
+ return new Promise((resolve, reject) => {
134
+ const hash = crypto.createHash('sha256');
135
+ const stream = fs.createReadStream(filePath);
136
+ stream.on('error', reject);
137
+ stream.on('end', () => resolve(hash.digest('hex')));
138
+ stream.pipe(hash);
139
+ });
140
+ }
141
+
142
+ function parseChecksums(text) {
143
+ const map = new Map();
144
+ for (const line of text.split(/\r?\n/)) {
145
+ const trimmed = line.trim();
146
+ if (!trimmed) continue;
147
+ const parts = trimmed.split(/\s+/);
148
+ if (parts.length >= 2) {
149
+ map.set(parts[1], parts[0]);
150
+ }
151
+ }
152
+ return map;
153
+ }
154
+
155
+ function runCommand(bin, args, options = {}) {
156
+ return new Promise((resolve, reject) => {
157
+ const child = spawn(bin, args, { stdio: 'pipe', ...options });
158
+ let stdout = '';
159
+ let stderr = '';
160
+ child.stdout?.on('data', (data) => { stdout += data.toString(); });
161
+ child.stderr?.on('data', (data) => { stderr += data.toString(); });
162
+ child.on('close', (code) => {
163
+ if (code === 0) {
164
+ resolve(stdout);
165
+ } else {
166
+ reject(new InstallerError(`${bin} exited with ${code}: ${stderr || stdout}`));
167
+ }
168
+ });
169
+ child.on('error', reject);
170
+ });
171
+ }
172
+
173
+ async function extractTarball(tarballPath, destDir) {
174
+ // Use the system tar command (available on macOS, Linux, and Windows 10+).
175
+ // Strip the top-level directory so files land directly in destDir.
176
+ await runCommand('tar', ['-xzf', tarballPath, '-C', destDir, '--strip-components=1']);
177
+ }
178
+
179
+ function runPythonInstall(pythonBin, installScript, args, options = {}) {
180
+ return new Promise((resolve, reject) => {
181
+ const child = spawn(pythonBin, [installScript, 'install', ...args], {
182
+ stdio: 'inherit',
183
+ env: { ...process.env, FORCE_COLOR: '1' },
184
+ ...options,
185
+ });
186
+ child.on('close', (code) => {
187
+ if (code === 0) {
188
+ resolve(code);
189
+ } else {
190
+ reject(new InstallerError(`Python install exited with code ${code}`));
191
+ }
192
+ });
193
+ child.on('error', reject);
194
+ });
195
+ }
196
+
197
+ async function cleanup(dir) {
198
+ try {
199
+ fs.rmSync(dir, { recursive: true, force: true });
200
+ } catch {
201
+ // ignore cleanup errors
202
+ }
203
+ }
204
+
205
+ function getVenvPython(venvDir) {
206
+ const isWindows = process.platform === 'win32';
207
+ const pythonName = isWindows ? 'python.exe' : 'python';
208
+ return path.join(venvDir, isWindows ? 'Scripts' : 'bin', pythonName);
209
+ }
210
+
211
+ async function createVenv(pythonBin, venvDir) {
212
+ await runCommand(pythonBin, ['-m', 'venv', venvDir]);
213
+ }
214
+
215
+ async function installRequirements(pythonBin, requirementsPath) {
216
+ if (!fs.existsSync(requirementsPath)) return;
217
+ await runCommand(pythonBin, ['-m', 'pip', 'install', '--quiet', '--requirement', requirementsPath]);
218
+ }
219
+
220
+ async function main(options = {}) {
221
+ const { global: globalInstall = true, ai = null, token = null, target = null, force = false } = options;
222
+
223
+ printBanner();
224
+
225
+ step('Checking environment');
226
+ const pythonBin = await findPython();
227
+ if (!pythonBin) {
228
+ error('Python is required but was not found on PATH.');
229
+ hint('Install Python 3.9+ and ensure "python3" or "python" is available.');
230
+ return 1;
231
+ }
232
+ success(`Found Python: ${pythonBin}`);
233
+ frameSection('Environment', [
234
+ `\x1b[32m✓\x1b[0m Python: ${pythonBin}`,
235
+ `\x1b[34mℹ\x1b[0m Install mode: ${globalInstall ? 'global' : 'project'}`,
236
+ ]);
237
+
238
+ info('Fetching latest release from GitHub...');
239
+ let release;
240
+ try {
241
+ release = await fetchJson(RELEASE_API_URL, token);
242
+ } catch (err) {
243
+ error(`Could not fetch release info: ${err.message}`);
244
+ hint('You can set GITHUB_TOKEN for higher API rate limits.');
245
+ return 1;
246
+ }
247
+
248
+ const tag = release.tag_name;
249
+ if (!tag) {
250
+ error('Latest release has no tag.');
251
+ return 1;
252
+ }
253
+ success(`Latest release: ${tag}`);
254
+
255
+ const tarballUrl = release.tarball_url;
256
+ const checksumAsset = release.assets?.find((a) => a.name === 'SHA256SUMS.txt');
257
+
258
+ if (!tarballUrl) {
259
+ error('Release has no tarball URL.');
260
+ return 1;
261
+ }
262
+ if (!checksumAsset) {
263
+ error('Release is missing SHA256SUMS.txt asset.');
264
+ return 1;
265
+ }
266
+
267
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'iterate-skill-install-'));
268
+ const tarballPath = path.join(tmpDir, 'iterate-skill.tar.gz');
269
+ const checksumPath = path.join(tmpDir, 'SHA256SUMS.txt');
270
+
271
+ try {
272
+ info('Downloading release tarball...');
273
+ await downloadFile(tarballUrl, tarballPath, token);
274
+
275
+ info('Downloading checksums...');
276
+ await downloadFile(checksumAsset.browser_download_url, checksumPath, token);
277
+
278
+ info('Verifying checksum...');
279
+ const checksums = parseChecksums(fs.readFileSync(checksumPath, 'utf8'));
280
+ const expectedHash = checksums.get('iterate-skill.tar.gz');
281
+ if (!expectedHash) {
282
+ error('Checksum file does not contain iterate-skill.tar.gz');
283
+ return 1;
284
+ }
285
+ const actualHash = await sha256File(tarballPath);
286
+ if (actualHash.toLowerCase() !== expectedHash.toLowerCase()) {
287
+ error('Checksum mismatch — refusing to install.');
288
+ return 1;
289
+ }
290
+ success('Checksum verified.');
291
+ frameSection('Release', [
292
+ `\x1b[32m✓\x1b[0m Version: ${tag}`,
293
+ `\x1b[32m✓\x1b[0m SHA256 checksum verified`,
294
+ ]);
295
+
296
+ info('Extracting release...');
297
+ const sourceDir = path.join(tmpDir, 'source');
298
+ fs.mkdirSync(sourceDir, { recursive: true });
299
+ await extractTarball(tarballPath, sourceDir);
300
+ success('Release extracted.');
301
+
302
+ const installScript = path.join(sourceDir, 'scripts', 'install.py');
303
+ if (!fs.existsSync(installScript)) {
304
+ error(`Install script not found at ${installScript}`);
305
+ return 1;
306
+ }
307
+
308
+ info('Creating isolated Python environment...');
309
+ const venvDir = path.join(tmpDir, 'venv');
310
+ try {
311
+ await createVenv(pythonBin, venvDir);
312
+ } catch (err) {
313
+ error(`Could not create Python venv: ${err.message}`);
314
+ hint('Ensure the Python "venv" module is available.');
315
+ return 1;
316
+ }
317
+ const venvPython = getVenvPython(venvDir);
318
+
319
+ info('Installing Python dependencies...');
320
+ const requirementsPath = path.join(sourceDir, 'scripts', 'requirements.txt');
321
+ try {
322
+ await installRequirements(venvPython, requirementsPath);
323
+ success('Dependencies ready.');
324
+ } catch (err) {
325
+ error(`Could not install Python dependencies: ${err.message}`);
326
+ return 1;
327
+ }
328
+
329
+ const installArgs = [];
330
+ if (ai) installArgs.push('--ai', ai);
331
+ if (target) installArgs.push('--target', target);
332
+ if (force) installArgs.push('--force');
333
+ if (globalInstall) installArgs.push('--global');
334
+
335
+ info('Starting skill installation (Python installer)...');
336
+ await runPythonInstall(venvPython, installScript, installArgs, { cwd: sourceDir });
337
+ success('iterate-skill installation finished.');
338
+ frameSection('Done', [
339
+ `\x1b[32m✓\x1b[0m iterate-skill ${tag} installed`,
340
+ ` Run \x1b[36miterate onboard\x1b[0m in your project to initialize.`,
341
+ ]);
342
+ return 0;
343
+ } finally {
344
+ await cleanup(tmpDir);
345
+ }
346
+ }
347
+
348
+ module.exports = {
349
+ main,
350
+ ITERATE_BANNER,
351
+ InstallerError,
352
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "iterate-skill-installer",
3
+ "version": "2.1.3",
4
+ "description": "One-command installer for iterate-skill across AI coding assistants",
5
+ "main": "lib/installer.js",
6
+ "bin": {
7
+ "iterate-skill-installer": "bin/cli.js",
8
+ "iterate-skill": "bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node --check bin/cli.js && node --check lib/installer.js"
12
+ },
13
+ "keywords": [
14
+ "iterate-skill",
15
+ "ai-assistant",
16
+ "skills",
17
+ "claude",
18
+ "cursor",
19
+ "trae",
20
+ "windsurf",
21
+ "installer"
22
+ ],
23
+ "author": "jingzhao-l",
24
+ "license": "MIT",
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/jingzhao-l/iterate-skill.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/jingzhao-l/iterate-skill/issues"
34
+ },
35
+ "homepage": "https://github.com/jingzhao-l/iterate-skill#readme"
36
+ }