@orbweva/academy 0.2.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/src/cli.js ADDED
@@ -0,0 +1,207 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, join } from 'node:path';
4
+ import { banner } from './banner.js';
5
+ import { confirm, select } from './prompts.js';
6
+ import { detectOS } from './os.js';
7
+ import { installSkills } from './install.js';
8
+ import { runCliSetup } from './cli-tools.js';
9
+ import { runMcpSetup } from './mcp.js';
10
+ import { c } from './color.js';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const manifest = JSON.parse(readFileSync(join(__dirname, '..', 'manifest.json'), 'utf8'));
14
+
15
+ function parseArgs(argv) {
16
+ const flags = { global: false, local: false, yes: false, skillsOnly: false, dryRun: false, noRun: false, track: null, packs: [] };
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const a = argv[i];
19
+ if (a === '--global' || a === '-g') flags.global = true;
20
+ else if (a === '--local' || a === '-l') flags.local = true;
21
+ else if (a === '--yes' || a === '-y') flags.yes = true;
22
+ else if (a === '--skills-only') flags.skillsOnly = true;
23
+ else if (a === '--no-run') flags.noRun = true;
24
+ else if (a === '--dry-run') flags.dryRun = true;
25
+ else if (a === '--help' || a === '-h') { printHelp(); process.exit(0); }
26
+ else if (a === '--track' || a === '-t') flags.track = argv[++i];
27
+ else if (a.startsWith('--track=')) flags.track = a.slice(8);
28
+ else if (a === '--pack' || a === '-p') flags.packs.push(argv[++i]);
29
+ else if (a.startsWith('--pack=')) flags.packs.push(a.slice(7));
30
+ }
31
+ return flags;
32
+ }
33
+
34
+ function printHelp() {
35
+ const tracks = Object.entries(manifest.tracks)
36
+ .map(([k, t]) => ` ${k.padEnd(14)} ${c.dim(t.tagline)}`).join('\n');
37
+ const packs = Object.entries(manifest.packs)
38
+ .map(([k, p]) => ` ${k.padEnd(14)} ${c.dim(p.tagline)}`).join('\n');
39
+
40
+ console.log(`
41
+ ${c.bold('orbweva-academy')} — install ORBWEVA skills into Claude Code
42
+
43
+ Usage:
44
+ npx @orbweva/academy [options]
45
+
46
+ Tracks ${c.dim('(pick one base program)')}:
47
+ ${tracks}
48
+
49
+ Specialization packs ${c.dim('(stackable, pick zero or more)')}:
50
+ ${packs}
51
+
52
+ Options:
53
+ -t, --track <name> Pick a track (skip the menu)
54
+ -p, --pack <name> Add a specialization pack (repeatable)
55
+ -g, --global Install to ~/.claude/skills (all projects)
56
+ -l, --local Install to ./.claude/skills (current project only)
57
+ -y, --yes Accept all defaults and run all CLI/MCP commands — no prompts
58
+ --skills-only Install skills only; skip CLI tools + MCP setup entirely
59
+ --no-run Print CLI / MCP commands (don't run them), skills still install
60
+ --dry-run Show plan, touch nothing
61
+ -h, --help Show this help
62
+
63
+ Examples:
64
+ npx @orbweva/academy --track accelerator --pack loka
65
+ npx @orbweva/academy --track founder
66
+ npx @orbweva/academy --track accelerator --pack marketing --pack web-video --yes
67
+ `);
68
+ }
69
+
70
+ function resolveTrack(key) {
71
+ const t = manifest.tracks[key];
72
+ if (!t) throw new Error(`Unknown track "${key}". Options: ${Object.keys(manifest.tracks).join(', ')}`);
73
+ return t;
74
+ }
75
+
76
+ function resolvePack(key) {
77
+ const p = manifest.packs[key];
78
+ if (!p) throw new Error(`Unknown pack "${key}". Options: ${Object.keys(manifest.packs).join(', ')}`);
79
+ return p;
80
+ }
81
+
82
+ function repoEntry(repoKey) {
83
+ const r = manifest.skillRepos[repoKey];
84
+ if (!r) throw new Error(`Unknown skill repo key "${repoKey}" referenced in manifest`);
85
+ return { key: repoKey, ...r };
86
+ }
87
+
88
+ // Merge repos across track + packs, deduping by repoKey.
89
+ // Returns { required: [...repos], optional: [{repo, source}], planned: [repos with status:planned] }
90
+ function planInstall(track, packs) {
91
+ const requiredKeys = new Set(track.required);
92
+ for (const pack of packs) for (const k of pack.required) requiredKeys.add(k);
93
+
94
+ const optionalEntries = new Map(); // key → { sources: Set<label> }
95
+ for (const k of track.optional) {
96
+ if (!optionalEntries.has(k)) optionalEntries.set(k, new Set());
97
+ optionalEntries.get(k).add(track.label);
98
+ }
99
+ for (const pack of packs) {
100
+ for (const k of pack.optional) {
101
+ if (requiredKeys.has(k)) continue;
102
+ if (!optionalEntries.has(k)) optionalEntries.set(k, new Set());
103
+ optionalEntries.get(k).add(pack.label);
104
+ }
105
+ }
106
+
107
+ const required = [...requiredKeys].map(repoEntry);
108
+ const optional = [...optionalEntries.entries()].map(([k, sources]) => ({
109
+ ...repoEntry(k),
110
+ sources: [...sources],
111
+ }));
112
+ const planned = [...required, ...optional].filter(r => r.status === 'planned');
113
+
114
+ return { required, optional, planned };
115
+ }
116
+
117
+ export async function run(argv) {
118
+ const flags = parseArgs(argv);
119
+ const os = detectOS();
120
+
121
+ console.log(banner());
122
+ console.log(c.dim(`Detected platform: ${os.name} (${os.arch})\n`));
123
+
124
+ // Track
125
+ let trackKey = flags.track;
126
+ if (!trackKey) {
127
+ if (flags.yes) trackKey = 'accelerator';
128
+ else trackKey = await select('Which Academy track?', Object.entries(manifest.tracks).map(([k, t]) => ({
129
+ value: k, label: `${c.bold(t.label)} — ${c.dim(t.tagline)}`,
130
+ })));
131
+ }
132
+ const track = resolveTrack(trackKey);
133
+ console.log(`\n${c.green('→')} Track: ${c.bold(track.label)}`);
134
+
135
+ // Packs
136
+ let packKeys = [...flags.packs];
137
+ if (packKeys.length === 0 && !flags.yes && Object.keys(manifest.packs).length > 0) {
138
+ console.log(`\n${c.bold('Add specialization packs?')} ${c.dim('(optional, stackable)')}`);
139
+ for (const [k, p] of Object.entries(manifest.packs)) {
140
+ const add = await confirm(` ${c.cyan(p.label)} ${c.dim(`— ${p.tagline}`)}?`, false);
141
+ if (add) packKeys.push(k);
142
+ }
143
+ }
144
+ const packs = packKeys.map(resolvePack);
145
+ if (packs.length > 0) console.log(`${c.green('→')} Packs: ${packs.map(p => c.bold(p.label)).join(', ')}`);
146
+
147
+ // Scope
148
+ let scope;
149
+ if (flags.global) scope = 'global';
150
+ else if (flags.local) scope = 'local';
151
+ else if (flags.yes) scope = 'global';
152
+ else scope = await select('Install scope', [
153
+ { value: 'global', label: `Global — ${c.dim('~/.claude/skills/ (recommended, all projects)')}` },
154
+ { value: 'local', label: `Local — ${c.dim('./.claude/skills/ (current project only)')}` },
155
+ ]);
156
+
157
+ // Plan
158
+ const plan = planInstall(track, packs);
159
+
160
+ // Warn about planned (not-yet-created) repos
161
+ if (plan.planned.length > 0) {
162
+ console.log(`\n${c.yellow('⚠')} Some pack repos are planned but not yet published:`);
163
+ for (const r of plan.planned) console.log(` ${c.dim('•')} ${r.repo} (${r.skills.join(', ')})`);
164
+ console.log(c.dim(' These will be skipped during install. They\'ll work once the repos are public.\n'));
165
+ }
166
+
167
+ // Approve optional
168
+ let approvedOptional = [];
169
+ if (!flags.yes && plan.optional.length > 0) {
170
+ console.log(`\n${c.bold('Required skills')} (always installed):`);
171
+ for (const r of plan.required) console.log(` ${c.green('•')} ${r.skills.join(', ')} ${c.dim(`(${r.repo})`)}`);
172
+
173
+ console.log(`\n${c.bold('Optional')}:`);
174
+ for (const r of plan.optional) {
175
+ const src = c.dim(`[${r.sources.join(', ')}]`);
176
+ const include = await confirm(` Install ${c.cyan(r.skills.join(', '))} ${src}?`, true);
177
+ if (include) approvedOptional.push(r);
178
+ }
179
+ } else {
180
+ approvedOptional = plan.optional;
181
+ }
182
+
183
+ const selectedRepos = [...plan.required, ...approvedOptional].filter(r => r.status !== 'planned');
184
+
185
+ // Confirm
186
+ const totalSkills = selectedRepos.reduce((n, r) => n + r.skills.length, 0);
187
+ console.log(`\n${c.bold('Ready to install:')} ${totalSkills} skills from ${selectedRepos.length} repos → ${scope === 'global' ? '~/.claude/skills/' : './.claude/skills/'}`);
188
+
189
+ if (!flags.yes && !flags.dryRun) {
190
+ const go = await confirm('Proceed?', true);
191
+ if (!go) { console.log('Aborted.'); return; }
192
+ }
193
+
194
+ await installSkills(selectedRepos, scope, { dryRun: flags.dryRun });
195
+
196
+ if (flags.skillsOnly || flags.dryRun) {
197
+ console.log(`\n${c.green('✓')} Skills ${flags.dryRun ? 'would be' : 'are'} installed. Open Claude Code to use them.`);
198
+ return;
199
+ }
200
+
201
+ // Interactive CLI + MCP setup (or print-only if --no-run)
202
+ const interactive = !flags.noRun;
203
+ await runCliSetup(os, { assumeYes: flags.yes, interactive });
204
+ await runMcpSetup(manifest.mcpServers, { assumeYes: flags.yes, interactive });
205
+
206
+ console.log(`\n${c.green('✓')} Setup complete. Open Claude Code and try ${c.cyan('/discovery:help')} to verify.\n`);
207
+ }
package/src/color.js ADDED
@@ -0,0 +1,13 @@
1
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
2
+ const wrap = (code, str) => (useColor ? `\x1b[${code}m${str}\x1b[0m` : String(str));
3
+
4
+ export const c = {
5
+ bold: (s) => wrap('1', s),
6
+ dim: (s) => wrap('2', s),
7
+ red: (s) => wrap('31', s),
8
+ green: (s) => wrap('32', s),
9
+ yellow: (s) => wrap('33', s),
10
+ blue: (s) => wrap('34', s),
11
+ magenta: (s) => wrap('35', s),
12
+ cyan: (s) => wrap('36', s),
13
+ };
package/src/exec.js ADDED
@@ -0,0 +1,36 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { c } from './color.js';
3
+ import { confirm } from './prompts.js';
4
+
5
+ function runShell(cmd) {
6
+ return new Promise((resolve) => {
7
+ const p = spawn(cmd, { shell: true, stdio: 'inherit' });
8
+ p.on('close', (code) => resolve({ ok: code === 0, code }));
9
+ p.on('error', (err) => resolve({ ok: false, code: -1, error: err.message }));
10
+ });
11
+ }
12
+
13
+ export async function promptAndRun(steps, { assumeYes } = {}) {
14
+ let ran = 0, skipped = 0, failed = 0;
15
+ for (const step of steps) {
16
+ const { cmd, desc, note } = step;
17
+ console.log('');
18
+ if (desc) console.log(` ${c.bold(desc)}`);
19
+ console.log(` ${c.cyan('$')} ${cmd}`);
20
+ if (note) console.log(` ${c.yellow(note)}`);
21
+ const go = assumeYes ? true : await confirm(` Run now?`, true);
22
+ if (!go) { console.log(` ${c.dim('skipped')}`); skipped++; continue; }
23
+ const { ok, code, error } = await runShell(cmd);
24
+ if (ok) { console.log(` ${c.green('✓ ok')}`); ran++; }
25
+ else { console.log(` ${c.red(`✗ exit ${code}${error ? ' — ' + error : ''}`)}`); failed++; }
26
+ }
27
+ return { ran, skipped, failed };
28
+ }
29
+
30
+ export function summary({ ran, skipped, failed }) {
31
+ const parts = [];
32
+ if (ran) parts.push(c.green(`${ran} ran`));
33
+ if (skipped) parts.push(c.dim(`${skipped} skipped`));
34
+ if (failed) parts.push(c.red(`${failed} failed`));
35
+ return parts.join(', ') || c.dim('nothing to do');
36
+ }
package/src/install.js ADDED
@@ -0,0 +1,74 @@
1
+ import { mkdir, cp, rm, access } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir, tmpdir } from 'node:os';
5
+ import { spawn } from 'node:child_process';
6
+ import { c } from './color.js';
7
+
8
+ function run(cmd, args, opts = {}) {
9
+ return new Promise((resolve, reject) => {
10
+ const p = spawn(cmd, args, { stdio: 'pipe', ...opts });
11
+ let stderr = '';
12
+ p.stderr.on('data', (d) => (stderr += d.toString()));
13
+ p.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}: ${stderr.trim()}`))));
14
+ p.on('error', reject);
15
+ });
16
+ }
17
+
18
+ async function exists(path) {
19
+ try { await access(path, constants.F_OK); return true; } catch { return false; }
20
+ }
21
+
22
+ export async function installSkills(repos, scope, { dryRun } = {}) {
23
+ const destRoot = scope === 'global'
24
+ ? join(homedir(), '.claude', 'skills')
25
+ : join(process.cwd(), '.claude', 'skills');
26
+
27
+ if (dryRun) {
28
+ console.log(`\n${c.yellow('DRY RUN')} — would install to ${destRoot}`);
29
+ for (const r of repos) {
30
+ for (const s of r.skills) console.log(` ${c.dim('•')} ${s} ${c.dim(`(from ${r.repo})`)}`);
31
+ }
32
+ return;
33
+ }
34
+
35
+ await mkdir(destRoot, { recursive: true });
36
+
37
+ const tmpBase = join(tmpdir(), `orbweva-install-${Date.now()}`);
38
+ await mkdir(tmpBase, { recursive: true });
39
+
40
+ console.log('');
41
+ let i = 0;
42
+ for (const r of repos) {
43
+ i++;
44
+ const [owner, name] = r.repo.split('/');
45
+ const cloneDir = join(tmpBase, name);
46
+ const url = `https://github.com/${owner}/${name}.git`;
47
+ process.stdout.write(`${c.cyan(`[${i}/${repos.length}]`)} ${r.repo} ... `);
48
+
49
+ try {
50
+ await run('git', ['clone', '--depth', '1', '--quiet', url, cloneDir]);
51
+ } catch (err) {
52
+ console.log(c.red('clone failed'));
53
+ console.log(c.dim(` ${err.message}`));
54
+ continue;
55
+ }
56
+
57
+ let copied = 0;
58
+ for (const skillName of r.skills) {
59
+ const src = join(cloneDir, 'skills', skillName);
60
+ const dst = join(destRoot, skillName);
61
+ if (!(await exists(src))) {
62
+ console.log(c.yellow(`skill "${skillName}" not found in repo, skipping`));
63
+ continue;
64
+ }
65
+ if (await exists(dst)) await rm(dst, { recursive: true, force: true });
66
+ await cp(src, dst, { recursive: true });
67
+ copied++;
68
+ }
69
+ console.log(c.green(`✓ ${copied} skill${copied === 1 ? '' : 's'}`));
70
+ }
71
+
72
+ await rm(tmpBase, { recursive: true, force: true });
73
+ console.log(`\n${c.green('✓')} Installed to ${c.bold(destRoot)}`);
74
+ }
package/src/mcp.js ADDED
@@ -0,0 +1,28 @@
1
+ import { c } from './color.js';
2
+ import { promptAndRun, summary } from './exec.js';
3
+
4
+ function stepsFromManifest(servers) {
5
+ return Object.entries(servers).map(([name, cfg]) => ({
6
+ cmd: `claude mcp add ${name} ${cfg.cmd}`,
7
+ desc: `Add MCP: ${name}`,
8
+ note: cfg.envKey ? `Needs ${cfg.envKey} in your shell env before Claude Code can actually connect.` : null,
9
+ }));
10
+ }
11
+
12
+ export async function runMcpSetup(servers, { assumeYes, interactive }) {
13
+ console.log(`\n${c.bold('─── MCP servers')}`);
14
+ const steps = stepsFromManifest(servers);
15
+
16
+ if (!interactive) {
17
+ for (const s of steps) {
18
+ console.log(` ${c.cyan('$')} ${s.cmd}${s.note ? c.dim(' # ' + s.note) : ''}`);
19
+ }
20
+ console.log(c.dim(`\n Docs: https://docs.claude.com/en/docs/claude-code/mcp`));
21
+ return null;
22
+ }
23
+
24
+ const result = await promptAndRun(steps, { assumeYes });
25
+ console.log(`\n ${c.bold('MCP servers:')} ${summary(result)}`);
26
+ console.log(c.dim(` Docs: https://docs.claude.com/en/docs/claude-code/mcp`));
27
+ return result;
28
+ }
package/src/os.js ADDED
@@ -0,0 +1,7 @@
1
+ import { platform, arch } from 'node:os';
2
+
3
+ export function detectOS() {
4
+ const p = platform();
5
+ const name = p === 'darwin' ? 'macOS' : p === 'win32' ? 'Windows' : p === 'linux' ? 'Linux' : p;
6
+ return { platform: p, name, arch: arch() };
7
+ }
package/src/prompts.js ADDED
@@ -0,0 +1,31 @@
1
+ import readline from 'node:readline';
2
+ import { c } from './color.js';
3
+
4
+ function ask(question) {
5
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6
+ return new Promise((resolve) => rl.question(question, (a) => { rl.close(); resolve(a.trim()); }));
7
+ }
8
+
9
+ export async function prompt(label, def = '') {
10
+ const hint = def ? c.dim(` (${def})`) : '';
11
+ const a = await ask(`${label}${hint}: `);
12
+ return a || def;
13
+ }
14
+
15
+ export async function confirm(label, def = true) {
16
+ const hint = def ? 'Y/n' : 'y/N';
17
+ const a = (await ask(`${label} ${c.dim(`[${hint}]`)} `)).toLowerCase();
18
+ if (!a) return def;
19
+ return a.startsWith('y');
20
+ }
21
+
22
+ export async function select(label, options) {
23
+ console.log(`\n${c.bold(label)}:`);
24
+ options.forEach((o, i) => console.log(` ${c.cyan(`${i + 1})`)} ${o.label}`));
25
+ while (true) {
26
+ const a = await ask(c.dim(` Choice [1-${options.length}, default 1]: `));
27
+ const n = a === '' ? 1 : parseInt(a, 10);
28
+ if (n >= 1 && n <= options.length) return options[n - 1].value;
29
+ console.log(c.red(' Invalid choice.'));
30
+ }
31
+ }