@equipt/cli 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/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @equipt/cli
2
+
3
+ Install curated [Equipt](https://github.com/Salah-XD/equipt) skills & agents into your project's `.claude/`.
4
+
5
+ ```bash
6
+ npx @equipt/cli init # scaffold ./.claude + equipt.json (interactive in a terminal)
7
+ npx @equipt/cli add equipt-engineering # install a whole plugin
8
+ npx @equipt/cli add code-reviewer # install a single skill/agent
9
+ npx @equipt/cli list # browse the catalog (with Readiness + tier)
10
+ ```
11
+
12
+ Run in a terminal, `init` opens an interactive plugin picker, and `add` asks before overwriting an existing file. In non-interactive contexts (CI, pipes) it stays quiet — `init` just scaffolds and `add` skips conflicts unless you pass `--force`.
13
+
14
+ Flags: `--global` (target `~/.claude`), `--force` (overwrite without asking), `--from <path>` (use a local Equipt checkout), `--plugin <name>` (scope `list`), `--yes` (non-interactive `init`).
15
+
16
+ > While the Equipt repo is private, the GitHub source is unavailable — pass `--from <path-to-equipt-checkout>` to install from a local clone.
package/bin/equipt.mjs ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/cli.mjs';
3
+ run().then((code) => { process.exitCode = code ?? 0; });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@equipt/cli",
3
+ "version": "0.1.0",
4
+ "description": "Install curated Equipt skills & agents into your project.",
5
+ "type": "module",
6
+ "bin": {
7
+ "equipt": "./bin/equipt.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=22"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test \"test/**/*.test.mjs\""
19
+ },
20
+ "keywords": [
21
+ "equipt",
22
+ "claude",
23
+ "claude-code",
24
+ "skills",
25
+ "agents",
26
+ "ai",
27
+ "cli",
28
+ "marketplace"
29
+ ],
30
+ "author": "Salah-XD",
31
+ "license": "MIT",
32
+ "homepage": "https://equipt-agent.vercel.app",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/Salah-XD/equipt.git",
36
+ "directory": "cli"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/Salah-XD/equipt/issues"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "@clack/prompts": "^1.5.1",
46
+ "commander": "^15.0.0",
47
+ "picocolors": "^1.1.1",
48
+ "tar": "^7.4.3"
49
+ }
50
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,169 @@
1
+ import { Command, CommanderError } from 'commander';
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { join, dirname } from 'node:path';
5
+ import * as clack from '@clack/prompts';
6
+ import { init } from './commands/init.mjs';
7
+ import { add } from './commands/add.mjs';
8
+ import { list } from './commands/list.mjs';
9
+ import { resolveSource, SourceError } from './lib/source.mjs';
10
+ import { loadCatalog } from './lib/catalog.mjs';
11
+ import { pc, tierColor, statusColor, kindLabel } from './lib/log.mjs';
12
+
13
+ const VERSION = JSON.parse(
14
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'),
15
+ ).version;
16
+
17
+ // Interactive only when attached to a real terminal and not explicitly opted out.
18
+ const isInteractive = (opts) => Boolean(process.stdout.isTTY) && !opts.yes;
19
+
20
+ async function runInit(opts) {
21
+ const { targetDir } = await init({ global: opts.global });
22
+
23
+ if (!isInteractive(opts)) {
24
+ console.log(pc.green(`Initialized Equipt in ${targetDir}`));
25
+ return 0;
26
+ }
27
+
28
+ clack.intro(pc.bold('equipt'));
29
+ let catalog;
30
+ try {
31
+ const { dir } = await resolveSource({ from: opts.from });
32
+ catalog = await loadCatalog(dir);
33
+ } catch (err) {
34
+ clack.note(err instanceof SourceError ? err.message : String(err), 'Source unavailable');
35
+ clack.outro(`Initialized ${targetDir} (no plugins installed).`);
36
+ return 0;
37
+ }
38
+
39
+ const counts = {};
40
+ for (const a of catalog.assets) counts[a.plugin] = (counts[a.plugin] ?? 0) + 1;
41
+ const picks = await clack.multiselect({
42
+ message: 'Install which plugins? (space to toggle, enter to confirm)',
43
+ options: catalog.plugins.map((p) => ({ value: p, label: p, hint: `${counts[p] ?? 0} assets` })),
44
+ required: false,
45
+ });
46
+ if (clack.isCancel(picks)) {
47
+ clack.cancel('Cancelled.');
48
+ return 0;
49
+ }
50
+ for (const p of picks) {
51
+ const out = await add(p, { global: opts.global, from: opts.from, force: true });
52
+ clack.log.success(`${p} — ${out.results.length} assets`);
53
+ }
54
+ clack.outro(pc.green(`Equipt ready in ${targetDir}`));
55
+ return 0;
56
+ }
57
+
58
+ async function runAdd(target, opts) {
59
+ if (!target) {
60
+ console.error('usage: equipt add <plugin|name>');
61
+ return 1;
62
+ }
63
+ const shouldOverwrite =
64
+ isInteractive(opts) && !opts.force
65
+ ? async (asset) => {
66
+ const r = await clack.confirm({ message: `Overwrite ${asset.name}?`, initialValue: false });
67
+ return !clack.isCancel(r) && r === true;
68
+ }
69
+ : undefined;
70
+
71
+ const out = await add(target, {
72
+ global: opts.global,
73
+ force: opts.force,
74
+ from: opts.from,
75
+ shouldOverwrite,
76
+ });
77
+ if (out.error) {
78
+ console.error(pc.red(out.error));
79
+ return 1;
80
+ }
81
+ for (const r of out.results) {
82
+ console.log(` ${statusColor(r.status)} ${kindLabel(r.kind)} ${r.name}`);
83
+ }
84
+ console.log(pc.dim(`Done → ${out.targetDir}`));
85
+ return 0;
86
+ }
87
+
88
+ async function runList(opts) {
89
+ const out = await list({ plugin: opts.plugin, from: opts.from });
90
+ for (const a of out.assets) {
91
+ const score =
92
+ a.readiness != null ? ` ${pc.dim(`R${a.readiness}`)} ${tierColor(a.tier)}` : '';
93
+ console.log(` ${kindLabel(a.kind)} ${pc.bold(`${a.plugin}/${a.name}`)}${score}`);
94
+ }
95
+ return 0;
96
+ }
97
+
98
+ function buildProgram(setCode) {
99
+ const program = new Command();
100
+ program
101
+ .name('equipt')
102
+ .description('Install curated Equipt skills & agents into your project.')
103
+ .version(VERSION, '-v, --version');
104
+
105
+ program
106
+ .command('init')
107
+ .description('scaffold ./.claude + equipt.json (interactive when run in a terminal)')
108
+ .option('--global', 'target ~/.claude instead of ./.claude')
109
+ .option('--from <path>', 'use a local Equipt checkout instead of GitHub')
110
+ .option('--yes', 'non-interactive (just scaffold)')
111
+ .action(async (opts) => {
112
+ const code = await runInit(opts);
113
+ if (code) setCode(code);
114
+ });
115
+
116
+ program
117
+ .command('add')
118
+ .description('install a plugin or a single asset')
119
+ .argument('[target]', 'plugin name or asset name')
120
+ .option('--global', 'target ~/.claude instead of ./.claude')
121
+ .option('--force', 'overwrite existing files without asking')
122
+ .option('--from <path>', 'use a local Equipt checkout instead of GitHub')
123
+ .action(async (target, opts) => {
124
+ const code = await runAdd(target, opts);
125
+ if (code) setCode(code);
126
+ });
127
+
128
+ program
129
+ .command('list')
130
+ .description('list available assets with Readiness + tier')
131
+ .option('--plugin <name>', 'scope to one plugin')
132
+ .option('--from <path>', 'use a local Equipt checkout instead of GitHub')
133
+ .action(async (opts) => {
134
+ const code = await runList(opts);
135
+ if (code) setCode(code);
136
+ });
137
+
138
+ return program;
139
+ }
140
+
141
+ export async function run(argv = process.argv.slice(2)) {
142
+ let code = 0;
143
+ const program = buildProgram((c) => {
144
+ code = c;
145
+ });
146
+ program.exitOverride();
147
+ program.configureOutput({ writeOut: (s) => process.stdout.write(s), writeErr: (s) => process.stderr.write(s) });
148
+
149
+ if (argv.length === 0) {
150
+ program.outputHelp();
151
+ return 1;
152
+ }
153
+
154
+ try {
155
+ await program.parseAsync(argv, { from: 'user' });
156
+ } catch (err) {
157
+ if (err instanceof SourceError) {
158
+ console.error(pc.red(err.message));
159
+ return 2;
160
+ }
161
+ if (err instanceof CommanderError) {
162
+ if (['commander.helpDisplayed', 'commander.help', 'commander.version'].includes(err.code)) return 0;
163
+ return err.exitCode ?? 1;
164
+ }
165
+ console.error(err?.stack || String(err));
166
+ return 1;
167
+ }
168
+ return code;
169
+ }
@@ -0,0 +1,29 @@
1
+ import { resolveSource } from '../lib/source.mjs';
2
+ import { loadCatalog, resolveTarget } from '../lib/catalog.mjs';
3
+ import { resolveTargetDir } from '../lib/paths.mjs';
4
+ import { installAsset } from '../lib/install.mjs';
5
+ import { readManifest, writeManifest } from '../lib/manifest.mjs';
6
+
7
+ export async function add(query, { global = false, force = false, from, cwd = process.cwd(), shouldOverwrite } = {}) {
8
+ if (!query) return { error: 'usage: equipt add <plugin|name>' };
9
+
10
+ const { dir } = await resolveSource({ from, cwd });
11
+ const catalog = await loadCatalog(dir);
12
+ const r = resolveTarget(catalog, query);
13
+
14
+ if (r.kind === 'none') return { error: `no plugin or asset named "${query}". Try: equipt list` };
15
+ if (r.kind === 'ambiguous') return { error: `"${query}" is ambiguous: ${r.matches.join(', ')}. Qualify as <plugin>/<name>.` };
16
+
17
+ const targetDir = resolveTargetDir({ global, cwd });
18
+ const manifest = await readManifest(targetDir);
19
+ const results = [];
20
+ for (const asset of r.matches) {
21
+ const res = await installAsset(asset, { targetDir, force, shouldOverwrite });
22
+ if (res.status !== 'skipped') {
23
+ manifest.installed[asset.name] = { plugin: asset.plugin, kind: asset.kind, addedAt: new Date().toISOString() };
24
+ }
25
+ results.push({ name: asset.name, kind: asset.kind, status: res.status });
26
+ }
27
+ await writeManifest(targetDir, manifest);
28
+ return { targetDir, results };
29
+ }
@@ -0,0 +1,12 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { resolveTargetDir } from '../lib/paths.mjs';
4
+ import { readManifest, writeManifest } from '../lib/manifest.mjs';
5
+
6
+ export async function init({ global = false, cwd = process.cwd() } = {}) {
7
+ const targetDir = resolveTargetDir({ global, cwd });
8
+ await mkdir(join(targetDir, 'skills'), { recursive: true });
9
+ await mkdir(join(targetDir, 'agents'), { recursive: true });
10
+ await writeManifest(targetDir, await readManifest(targetDir));
11
+ return { targetDir };
12
+ }
@@ -0,0 +1,9 @@
1
+ import { resolveSource } from '../lib/source.mjs';
2
+ import { loadCatalog } from '../lib/catalog.mjs';
3
+
4
+ export async function list({ plugin, from, cwd = process.cwd() } = {}) {
5
+ const { dir } = await resolveSource({ from, cwd });
6
+ const catalog = await loadCatalog(dir);
7
+ const assets = plugin ? catalog.assets.filter((a) => a.plugin === plugin) : catalog.assets;
8
+ return { plugins: catalog.plugins, assets };
9
+ }
@@ -0,0 +1,72 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ function nameFromFrontmatter(file, fallback) {
6
+ try {
7
+ const fm = readFileSync(file, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/);
8
+ if (fm) {
9
+ const nm = fm[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
10
+ if (nm) return nm[1].trim();
11
+ }
12
+ } catch {}
13
+ return fallback;
14
+ }
15
+
16
+ async function safeReaddir(dir) {
17
+ try { return await readdir(dir, { withFileTypes: true }); }
18
+ catch (err) { if (err.code === 'ENOENT') return []; throw err; }
19
+ }
20
+
21
+ export async function loadCatalog(sourceDir) {
22
+ const pluginsDir = join(sourceDir, 'plugins');
23
+ let plugins = [];
24
+ try {
25
+ const cfg = JSON.parse(await readFile(join(sourceDir, 'plugins.config.json'), 'utf8'));
26
+ plugins = Object.keys(cfg.plugins ?? {});
27
+ } catch {
28
+ plugins = (await safeReaddir(pluginsDir)).filter((e) => e.isDirectory()).map((e) => e.name);
29
+ }
30
+
31
+ const assets = [];
32
+ for (const plugin of plugins) {
33
+ const skillsBase = join(pluginsDir, plugin, 'skills');
34
+ for (const e of await safeReaddir(skillsBase)) {
35
+ if (!e.isDirectory()) continue;
36
+ const src = join(skillsBase, e.name);
37
+ assets.push({ plugin, name: nameFromFrontmatter(join(src, 'SKILL.md'), e.name), kind: 'skill', src });
38
+ }
39
+ const agentsBase = join(pluginsDir, plugin, 'agents');
40
+ for (const e of await safeReaddir(agentsBase)) {
41
+ if (!e.isFile() || !e.name.endsWith('.md') || e.name === 'README.md') continue;
42
+ const src = join(agentsBase, e.name);
43
+ assets.push({ plugin, name: nameFromFrontmatter(src, e.name.replace(/\.md$/, '')), kind: 'agent', src });
44
+ }
45
+ }
46
+
47
+ try {
48
+ const idx = JSON.parse(await readFile(join(sourceDir, 'scores', 'index.json'), 'utf8'));
49
+ const byKey = new Map(idx.assets.map((a) => [`${a.plugin}/${a.name}`, a]));
50
+ for (const a of assets) {
51
+ const s = byKey.get(`${a.plugin}/${a.name}`);
52
+ if (s) { a.readiness = s.readiness; a.tier = s.tier; }
53
+ }
54
+ } catch {}
55
+
56
+ return { plugins, assets };
57
+ }
58
+
59
+ export function resolveTarget(catalog, query) {
60
+ if (query.includes('/')) {
61
+ const [p, n] = query.split('/');
62
+ const m = catalog.assets.filter((a) => a.plugin === p && a.name === n);
63
+ return m.length ? { kind: 'asset', matches: m } : { kind: 'none', matches: [] };
64
+ }
65
+ if (catalog.plugins.includes(query)) {
66
+ return { kind: 'plugin', matches: catalog.assets.filter((a) => a.plugin === query) };
67
+ }
68
+ const named = catalog.assets.filter((a) => a.name === query);
69
+ if (named.length === 1) return { kind: 'asset', matches: named };
70
+ if (named.length > 1) return { kind: 'ambiguous', matches: named.map((a) => `${a.plugin}/${a.name}`) };
71
+ return { kind: 'none', matches: [] };
72
+ }
@@ -0,0 +1,19 @@
1
+ import { cp, mkdir, access } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ async function exists(p) { try { await access(p); return true; } catch { return false; } }
5
+
6
+ export async function installAsset(asset, { targetDir, force = false, shouldOverwrite }) {
7
+ const parent = join(targetDir, asset.kind === 'skill' ? 'skills' : 'agents');
8
+ const dest = asset.kind === 'skill' ? join(parent, asset.name) : join(parent, `${asset.name}.md`);
9
+
10
+ const already = await exists(dest);
11
+ if (already && !force) {
12
+ // Non-interactive (no callback) preserves the original behavior: skip.
13
+ if (!shouldOverwrite || !(await shouldOverwrite(asset))) return { status: 'skipped', dest };
14
+ }
15
+
16
+ await mkdir(parent, { recursive: true });
17
+ await cp(asset.src, dest, { recursive: true, force: true });
18
+ return { status: already ? 'overwritten' : 'installed', dest };
19
+ }
@@ -0,0 +1,22 @@
1
+ import pc from 'picocolors';
2
+
3
+ // picocolors auto-disables when stdout is not a TTY or NO_COLOR is set,
4
+ // so these are safe to use unconditionally (no manual stripping needed).
5
+ export { pc };
6
+
7
+ export function tierColor(tier) {
8
+ if (tier === 'field-ready') return pc.green('field-ready');
9
+ if (tier === 'certified') return pc.yellow('certified');
10
+ return pc.dim(tier || 'provisional');
11
+ }
12
+
13
+ export function statusColor(status) {
14
+ const label = status.padEnd(11);
15
+ if (status === 'installed') return pc.green(label);
16
+ if (status === 'overwritten') return pc.yellow(label);
17
+ return pc.dim(label);
18
+ }
19
+
20
+ export function kindLabel(kind) {
21
+ return kind === 'skill' ? pc.cyan('skill') : pc.magenta('agent');
22
+ }
@@ -0,0 +1,23 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ const SOURCE = 'Salah-XD/equipt';
5
+
6
+ export function defaultManifest() {
7
+ return { source: SOURCE, installed: {} };
8
+ }
9
+
10
+ export async function readManifest(targetDir) {
11
+ try {
12
+ const m = JSON.parse(await readFile(join(targetDir, 'equipt.json'), 'utf8'));
13
+ return { source: m.source ?? SOURCE, installed: m.installed ?? {} };
14
+ } catch (err) {
15
+ if (err.code === 'ENOENT') return defaultManifest();
16
+ throw err;
17
+ }
18
+ }
19
+
20
+ export async function writeManifest(targetDir, manifest) {
21
+ await mkdir(targetDir, { recursive: true });
22
+ await writeFile(join(targetDir, 'equipt.json'), JSON.stringify(manifest, null, 2) + '\n');
23
+ }
@@ -0,0 +1,6 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+
4
+ export function resolveTargetDir({ global = false, cwd = process.cwd() } = {}) {
5
+ return global ? join(homedir(), '.claude') : join(cwd, '.claude');
6
+ }
@@ -0,0 +1,51 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { x } from 'tar';
6
+
7
+ export class SourceError extends Error {}
8
+
9
+ export function findLocalSource(cwd) {
10
+ let dir = cwd;
11
+ for (let i = 0; i < 12; i++) {
12
+ try { readFileSync(join(dir, 'plugins.config.json')); return dir; } catch {}
13
+ const parent = dirname(dir);
14
+ if (parent === dir) break;
15
+ dir = parent;
16
+ }
17
+ return null;
18
+ }
19
+
20
+ export async function extractTarball(tgzPath, destDir) {
21
+ await mkdir(destDir, { recursive: true });
22
+ await x({ file: tgzPath, cwd: destDir, strip: 1 });
23
+ return destDir;
24
+ }
25
+
26
+ async function defaultDownload(url) {
27
+ const res = await fetch(url);
28
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
29
+ return Buffer.from(await res.arrayBuffer());
30
+ }
31
+
32
+ export async function resolveSource({ from, cwd = process.cwd(), ref = 'main', download = defaultDownload } = {}) {
33
+ if (from) return { dir: from, cleanup: async () => {} };
34
+ const local = findLocalSource(cwd);
35
+ if (local) return { dir: local, cleanup: async () => {} };
36
+
37
+ const url = `https://codeload.github.com/Salah-XD/equipt/tar.gz/refs/heads/${ref}`;
38
+ let buf;
39
+ try { buf = await download(url); }
40
+ catch (err) {
41
+ throw new SourceError(
42
+ `Could not download Equipt from GitHub (${err.message}). The repo may be private — pass --from <path> to a local checkout.`,
43
+ );
44
+ }
45
+ const tgz = join(tmpdir(), `equipt-${ref}.tgz`);
46
+ await mkdir(dirname(tgz), { recursive: true });
47
+ await writeFile(tgz, buf);
48
+ const dir = join(tmpdir(), 'equipt-cli', ref);
49
+ await extractTarball(tgz, dir);
50
+ return { dir, cleanup: async () => {} };
51
+ }