@shaykec/claude-teach 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/README.md +69 -0
- package/commands/teach.md +78 -0
- package/engine/authoring.md +150 -0
- package/engine/teach-command.md +76 -0
- package/package.json +28 -0
- package/src/author.js +355 -0
- package/src/bridge-server.js +60 -0
- package/src/cli.js +512 -0
- package/src/gamification.js +176 -0
- package/src/gamification.test.js +509 -0
- package/src/marketplace.js +349 -0
- package/src/progress.js +157 -0
- package/src/progress.test.js +360 -0
- package/src/registry.js +201 -0
- package/src/registry.test.js +309 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { readFileSync, existsSync } from 'fs';
|
|
5
|
+
import { resolve, join } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname } from 'path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { buildRegistry, loadRegistry, getModule } from './registry.js';
|
|
10
|
+
import { loadProgress, saveProgress, getStats } from './progress.js';
|
|
11
|
+
import { getBelt, formatDashboard, suggestNext } from './gamification.js';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
const ROOT = resolve(__dirname, '..', '..', '..');
|
|
16
|
+
|
|
17
|
+
const program = new Command();
|
|
18
|
+
|
|
19
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.name('claude-teach')
|
|
23
|
+
.description(`Socratic AI teaching platform with gamification and visual ecosystem.
|
|
24
|
+
|
|
25
|
+
Learn through guided dialogue — the AI asks YOU questions, tracks your
|
|
26
|
+
progress with XP and belt ranks, and enhances lessons with browser-based
|
|
27
|
+
diagrams, interactive quizzes, and code playgrounds.
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
$ claude-teach list List available modules
|
|
31
|
+
$ claude-teach list --difficulty beginner Filter by difficulty
|
|
32
|
+
$ claude-teach get git Start the Git walkthrough
|
|
33
|
+
$ claude-teach get git --quick Show Git quick reference
|
|
34
|
+
$ claude-teach stats View your XP and belt progress
|
|
35
|
+
$ claude-teach install user/my-pack Install a module pack from GitHub
|
|
36
|
+
$ claude-teach search docker Search registries for packs
|
|
37
|
+
$ claude-teach author init my-pack Scaffold a new module pack
|
|
38
|
+
$ claude-teach author validate ./my-pack Validate a pack
|
|
39
|
+
$ claude-teach serve Start the bridge server for visuals`)
|
|
40
|
+
.version(pkg.version);
|
|
41
|
+
|
|
42
|
+
// =====================================================================
|
|
43
|
+
// Learning commands
|
|
44
|
+
// =====================================================================
|
|
45
|
+
|
|
46
|
+
// --- list ---
|
|
47
|
+
program
|
|
48
|
+
.command('list')
|
|
49
|
+
.description('List available learning modules (filter with --category, --difficulty, --tag, --source)')
|
|
50
|
+
.option('-c, --category <category>', 'Filter by category')
|
|
51
|
+
.option('-d, --difficulty <level>', 'Filter by difficulty (beginner|intermediate|advanced)')
|
|
52
|
+
.option('-t, --tag <tag>', 'Filter by tag')
|
|
53
|
+
.option('-s, --source <source>', 'Filter by source (built-in|pack:<name>|local)')
|
|
54
|
+
.action(async (opts) => {
|
|
55
|
+
const registry = await loadRegistry(ROOT);
|
|
56
|
+
let modules = registry.modules;
|
|
57
|
+
|
|
58
|
+
if (opts.category) {
|
|
59
|
+
modules = modules.filter(m => m.category === opts.category);
|
|
60
|
+
}
|
|
61
|
+
if (opts.difficulty) {
|
|
62
|
+
modules = modules.filter(m => m.difficulty === opts.difficulty);
|
|
63
|
+
}
|
|
64
|
+
if (opts.tag) {
|
|
65
|
+
modules = modules.filter(m => m.tags?.includes(opts.tag));
|
|
66
|
+
}
|
|
67
|
+
if (opts.source) {
|
|
68
|
+
modules = modules.filter(m => m.source === opts.source);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (modules.length === 0) {
|
|
72
|
+
console.log('No modules found matching your filters.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const progress = loadProgress(ROOT);
|
|
77
|
+
const difficultyIcon = { beginner: '🌱', intermediate: '🌿', advanced: '🌳' };
|
|
78
|
+
|
|
79
|
+
console.log(`\n Available Modules (${modules.length})\n`);
|
|
80
|
+
for (const mod of modules) {
|
|
81
|
+
const icon = difficultyIcon[mod.difficulty] || ' ';
|
|
82
|
+
const status = progress.modules?.[mod.slug]?.status;
|
|
83
|
+
const statusMark = status === 'completed' ? ' ✓' : status === 'in-progress' ? ' ◐' : '';
|
|
84
|
+
const caps = [];
|
|
85
|
+
if (mod.capabilities.includes('walkthrough')) caps.push('guided');
|
|
86
|
+
if (mod.capabilities.includes('exercises')) caps.push('exercises');
|
|
87
|
+
if (mod.capabilities.includes('quiz')) caps.push('quiz');
|
|
88
|
+
if (mod.capabilities.includes('quick-ref')) caps.push('quick');
|
|
89
|
+
const capsStr = caps.length ? ` [${caps.join(', ')}]` : '';
|
|
90
|
+
const sourceTag = mod.source !== 'built-in' ? chalk.dim(` (${mod.source})`) : '';
|
|
91
|
+
console.log(` ${icon} ${mod.slug}${statusMark} — ${mod.title}${capsStr}${sourceTag}`);
|
|
92
|
+
}
|
|
93
|
+
console.log();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// --- get ---
|
|
97
|
+
program
|
|
98
|
+
.command('get <module>')
|
|
99
|
+
.description('Get module content — walkthrough by default, or --quick, --quiz-only, --status, --reset')
|
|
100
|
+
.option('--quick', 'Show quick reference')
|
|
101
|
+
.option('--quiz-only', 'Run quiz only')
|
|
102
|
+
.option('--status', 'Show progress for this module')
|
|
103
|
+
.option('--reset', 'Reset progress for this module')
|
|
104
|
+
.action(async (slug, opts) => {
|
|
105
|
+
const registry = await loadRegistry(ROOT);
|
|
106
|
+
const mod = getModule(registry, slug);
|
|
107
|
+
if (!mod) {
|
|
108
|
+
console.error(`Module "${slug}" not found. Run "claude-teach list" to see available modules.`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (opts.status) {
|
|
113
|
+
const progress = loadProgress(ROOT);
|
|
114
|
+
const entry = progress.modules?.[slug];
|
|
115
|
+
if (!entry) {
|
|
116
|
+
console.log(`No progress recorded for "${slug}".`);
|
|
117
|
+
} else {
|
|
118
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (opts.reset) {
|
|
124
|
+
const progress = loadProgress(ROOT);
|
|
125
|
+
delete progress.modules?.[slug];
|
|
126
|
+
saveProgress(ROOT, progress);
|
|
127
|
+
console.log(`Progress reset for "${slug}".`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const modulePath = mod._path;
|
|
132
|
+
|
|
133
|
+
if (opts.quick) {
|
|
134
|
+
const quickPath = join(modulePath, 'quick-ref.md');
|
|
135
|
+
if (!existsSync(quickPath)) {
|
|
136
|
+
console.error(`No quick reference available for "${slug}".`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
console.log(readFileSync(quickPath, 'utf-8'));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (opts.quizOnly) {
|
|
144
|
+
const quizPath = join(modulePath, 'quiz.md');
|
|
145
|
+
if (!existsSync(quizPath)) {
|
|
146
|
+
console.error(`No quiz available for "${slug}".`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
console.log(readFileSync(quizPath, 'utf-8'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Default: walkthrough if available, else content
|
|
154
|
+
const walkthroughPath = join(modulePath, 'walkthrough.md');
|
|
155
|
+
const contentPath = join(modulePath, 'content.md');
|
|
156
|
+
if (existsSync(walkthroughPath)) {
|
|
157
|
+
console.log(readFileSync(walkthroughPath, 'utf-8'));
|
|
158
|
+
} else if (existsSync(contentPath)) {
|
|
159
|
+
console.log(readFileSync(contentPath, 'utf-8'));
|
|
160
|
+
} else {
|
|
161
|
+
console.error(`No content found for module "${slug}".`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// --- stats ---
|
|
167
|
+
program
|
|
168
|
+
.command('stats')
|
|
169
|
+
.description('Show gamification dashboard — XP, belt, progress')
|
|
170
|
+
.action(async () => {
|
|
171
|
+
const progress = loadProgress(ROOT);
|
|
172
|
+
const registry = await loadRegistry(ROOT);
|
|
173
|
+
const dashboard = formatDashboard(progress, registry);
|
|
174
|
+
console.log(dashboard);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// --- level-up ---
|
|
178
|
+
program
|
|
179
|
+
.command('level-up')
|
|
180
|
+
.description('See your belt roadmap and get suggestions for what to learn next')
|
|
181
|
+
.action(async () => {
|
|
182
|
+
const progress = loadProgress(ROOT);
|
|
183
|
+
const registry = await loadRegistry(ROOT);
|
|
184
|
+
const suggestion = suggestNext(progress, registry);
|
|
185
|
+
console.log(suggestion);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// =====================================================================
|
|
189
|
+
// Visual Ecosystem commands
|
|
190
|
+
// =====================================================================
|
|
191
|
+
|
|
192
|
+
// --- serve (bridge server) ---
|
|
193
|
+
program
|
|
194
|
+
.command('serve')
|
|
195
|
+
.description('Start the bridge server for visual canvas — diagrams, quizzes, dashboard (default port: 3456)')
|
|
196
|
+
.option('-p, --port <port>', 'Port number', '3456')
|
|
197
|
+
.action(async (opts) => {
|
|
198
|
+
const { startServer } = await import('@shaykec/bridge');
|
|
199
|
+
const progress = loadProgress(ROOT);
|
|
200
|
+
startServer({
|
|
201
|
+
port: parseInt(opts.port, 10),
|
|
202
|
+
progressProvider: {
|
|
203
|
+
getProgress: () => loadProgress(ROOT),
|
|
204
|
+
},
|
|
205
|
+
onTierChange: (oldTier, newTier) => {
|
|
206
|
+
const labels = { 1: 'Full', 2: 'Canvas', 3: 'Terminal' };
|
|
207
|
+
console.log(` Tier changed: ${labels[oldTier]} -> ${labels[newTier]}`);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// --- inbox ---
|
|
213
|
+
program
|
|
214
|
+
.command('inbox')
|
|
215
|
+
.description('Show content captured from the browser bridge')
|
|
216
|
+
.action(async () => {
|
|
217
|
+
const { showInbox } = await import('./bridge-server.js');
|
|
218
|
+
showInbox();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// =====================================================================
|
|
222
|
+
// System commands
|
|
223
|
+
// =====================================================================
|
|
224
|
+
|
|
225
|
+
// --- registry:build ---
|
|
226
|
+
program
|
|
227
|
+
.command('registry:build')
|
|
228
|
+
.description('Rebuild the module registry from all sources (built-in, packs, local)')
|
|
229
|
+
.action(async () => {
|
|
230
|
+
const registry = await buildRegistry(ROOT);
|
|
231
|
+
const count = registry.modules.length;
|
|
232
|
+
const sources = {};
|
|
233
|
+
for (const m of registry.modules) {
|
|
234
|
+
const key = m.source.startsWith('pack:') ? 'pack' : m.source;
|
|
235
|
+
sources[key] = (sources[key] || 0) + 1;
|
|
236
|
+
}
|
|
237
|
+
console.log(chalk.green(`Registry rebuilt: ${count} module(s).`));
|
|
238
|
+
for (const [src, n] of Object.entries(sources)) {
|
|
239
|
+
console.log(` ${src}: ${n}`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// =====================================================================
|
|
244
|
+
// Marketplace commands — install, manage, and discover module packs
|
|
245
|
+
// =====================================================================
|
|
246
|
+
|
|
247
|
+
// --- install ---
|
|
248
|
+
program
|
|
249
|
+
.command('install <url>')
|
|
250
|
+
.description('Install a module pack from a git URL or GitHub shorthand (e.g., user/repo)')
|
|
251
|
+
.action(async (url) => {
|
|
252
|
+
try {
|
|
253
|
+
const { installPack } = await import('./marketplace.js');
|
|
254
|
+
await installPack(url);
|
|
255
|
+
// Rebuild registry after install
|
|
256
|
+
await buildRegistry(ROOT);
|
|
257
|
+
console.log(chalk.green('Registry rebuilt.'));
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// --- packs ---
|
|
265
|
+
program
|
|
266
|
+
.command('packs')
|
|
267
|
+
.description('List installed module packs')
|
|
268
|
+
.action(async () => {
|
|
269
|
+
try {
|
|
270
|
+
const { listPacks } = await import('./marketplace.js');
|
|
271
|
+
const packs = await listPacks();
|
|
272
|
+
|
|
273
|
+
if (packs.length === 0) {
|
|
274
|
+
console.log('No module packs installed.');
|
|
275
|
+
console.log(` Install one with: ${chalk.cyan('claude-teach install <url>')}`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log(`\n Installed Packs (${packs.length})\n`);
|
|
280
|
+
for (const pack of packs) {
|
|
281
|
+
console.log(` ${chalk.bold(pack.name)} v${pack.version} — ${pack.description || '(no description)'}`);
|
|
282
|
+
console.log(` Author: ${pack.author || 'unknown'} | Modules: ${pack.modules.length}`);
|
|
283
|
+
console.log(` Path: ${chalk.dim(pack.path)}`);
|
|
284
|
+
console.log();
|
|
285
|
+
}
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// --- update ---
|
|
293
|
+
program
|
|
294
|
+
.command('update <pack>')
|
|
295
|
+
.description('Update an installed module pack (git pull)')
|
|
296
|
+
.action(async (packName) => {
|
|
297
|
+
try {
|
|
298
|
+
const { updatePack } = await import('./marketplace.js');
|
|
299
|
+
await updatePack(packName);
|
|
300
|
+
await buildRegistry(ROOT);
|
|
301
|
+
console.log(chalk.green('Registry rebuilt.'));
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// --- remove ---
|
|
309
|
+
program
|
|
310
|
+
.command('remove <pack>')
|
|
311
|
+
.description('Remove an installed module pack')
|
|
312
|
+
.action(async (packName) => {
|
|
313
|
+
try {
|
|
314
|
+
const { removePack } = await import('./marketplace.js');
|
|
315
|
+
await removePack(packName);
|
|
316
|
+
await buildRegistry(ROOT);
|
|
317
|
+
console.log(chalk.green('Registry rebuilt.'));
|
|
318
|
+
} catch (err) {
|
|
319
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// --- search ---
|
|
325
|
+
program
|
|
326
|
+
.command('search <query>')
|
|
327
|
+
.description('Search configured registries for module packs')
|
|
328
|
+
.action(async (query) => {
|
|
329
|
+
try {
|
|
330
|
+
const { searchPacks } = await import('./marketplace.js');
|
|
331
|
+
const results = await searchPacks(query);
|
|
332
|
+
|
|
333
|
+
if (results.length === 0) {
|
|
334
|
+
console.log(`No packs found for "${query}".`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log(`\n Search Results (${results.length})\n`);
|
|
339
|
+
for (const pack of results) {
|
|
340
|
+
console.log(` ${chalk.bold(pack.name)} — ${pack.description || '(no description)'}`);
|
|
341
|
+
if (pack.author) console.log(` Author: ${pack.author}`);
|
|
342
|
+
if (pack.tags?.length) console.log(` Tags: ${pack.tags.join(', ')}`);
|
|
343
|
+
if (pack.repo) console.log(` Install: ${chalk.cyan(`claude-teach install ${pack.repo}`)}`);
|
|
344
|
+
console.log(` Registry: ${chalk.dim(pack.registry)}`);
|
|
345
|
+
console.log();
|
|
346
|
+
}
|
|
347
|
+
} catch (err) {
|
|
348
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// --- registry subcommand ---
|
|
354
|
+
const registryCmd = program
|
|
355
|
+
.command('registry')
|
|
356
|
+
.description('Manage module pack registries');
|
|
357
|
+
|
|
358
|
+
registryCmd
|
|
359
|
+
.command('add <url>')
|
|
360
|
+
.description('Add a registry by URL or GitHub shorthand')
|
|
361
|
+
.action(async (url) => {
|
|
362
|
+
try {
|
|
363
|
+
const { addRegistry } = await import('./marketplace.js');
|
|
364
|
+
addRegistry(url);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
registryCmd
|
|
372
|
+
.command('list')
|
|
373
|
+
.description('List configured registries')
|
|
374
|
+
.action(async () => {
|
|
375
|
+
try {
|
|
376
|
+
const { getRegistries } = await import('./marketplace.js');
|
|
377
|
+
const regs = getRegistries();
|
|
378
|
+
|
|
379
|
+
if (regs.length === 0) {
|
|
380
|
+
console.log('No registries configured.');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
console.log(`\n Configured Registries (${regs.length})\n`);
|
|
385
|
+
for (const reg of regs) {
|
|
386
|
+
const status = reg.enabled !== false ? chalk.green('enabled') : chalk.dim('disabled');
|
|
387
|
+
console.log(` ${chalk.bold(reg.name || '(unnamed)')} [${status}]`);
|
|
388
|
+
console.log(` ${reg.url}`);
|
|
389
|
+
console.log();
|
|
390
|
+
}
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
registryCmd
|
|
398
|
+
.command('remove <name>')
|
|
399
|
+
.description('Remove a registry by name or URL')
|
|
400
|
+
.action(async (nameOrUrl) => {
|
|
401
|
+
try {
|
|
402
|
+
const { removeRegistry } = await import('./marketplace.js');
|
|
403
|
+
removeRegistry(nameOrUrl);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// =====================================================================
|
|
411
|
+
// Authoring commands — create, validate, and preview learning modules
|
|
412
|
+
// =====================================================================
|
|
413
|
+
|
|
414
|
+
const authorCmd = program
|
|
415
|
+
.command('author')
|
|
416
|
+
.description('Module authoring tools — scaffold packs, add modules, validate, and preview');
|
|
417
|
+
|
|
418
|
+
authorCmd
|
|
419
|
+
.command('init <name>')
|
|
420
|
+
.description('Scaffold a new module pack')
|
|
421
|
+
.action(async (name) => {
|
|
422
|
+
try {
|
|
423
|
+
const { initPack } = await import('./author.js');
|
|
424
|
+
await initPack(name);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
authorCmd
|
|
432
|
+
.command('add <pack-and-module>')
|
|
433
|
+
.description('Add a module to a pack (format: pack-path/module-name)')
|
|
434
|
+
.action(async (packAndModule) => {
|
|
435
|
+
try {
|
|
436
|
+
const slashIdx = packAndModule.lastIndexOf('/');
|
|
437
|
+
if (slashIdx === -1) {
|
|
438
|
+
console.error(chalk.red('Usage: claude-teach author add <pack-path>/<module-name>'));
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
const packPath = packAndModule.slice(0, slashIdx);
|
|
442
|
+
const moduleName = packAndModule.slice(slashIdx + 1);
|
|
443
|
+
if (!packPath || !moduleName) {
|
|
444
|
+
console.error(chalk.red('Usage: claude-teach author add <pack-path>/<module-name>'));
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const { addModule } = await import('./author.js');
|
|
449
|
+
await addModule(packPath, moduleName);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
authorCmd
|
|
457
|
+
.command('validate <path>')
|
|
458
|
+
.description('Validate a module pack')
|
|
459
|
+
.action(async (packPath) => {
|
|
460
|
+
try {
|
|
461
|
+
const { validatePack } = await import('./author.js');
|
|
462
|
+
// Provide built-in slugs for prerequisite validation
|
|
463
|
+
const registry = await loadRegistry(ROOT);
|
|
464
|
+
const builtinSlugs = registry.modules
|
|
465
|
+
.filter(m => m.source === 'built-in')
|
|
466
|
+
.map(m => m.slug);
|
|
467
|
+
|
|
468
|
+
const result = await validatePack(packPath, builtinSlugs);
|
|
469
|
+
|
|
470
|
+
if (result.errors.length > 0) {
|
|
471
|
+
console.log(chalk.red(`\n Errors (${result.errors.length}):\n`));
|
|
472
|
+
for (const err of result.errors) {
|
|
473
|
+
console.log(` ${chalk.red('✗')} ${err}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (result.warnings.length > 0) {
|
|
478
|
+
console.log(chalk.yellow(`\n Warnings (${result.warnings.length}):\n`));
|
|
479
|
+
for (const warn of result.warnings) {
|
|
480
|
+
console.log(` ${chalk.yellow('!')} ${warn}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (result.valid) {
|
|
485
|
+
console.log(chalk.green('\n Pack is valid.\n'));
|
|
486
|
+
} else {
|
|
487
|
+
console.log(chalk.red('\n Pack has errors. Fix them and try again.\n'));
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
} catch (err) {
|
|
491
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
authorCmd
|
|
497
|
+
.command('preview <path>')
|
|
498
|
+
.description('Preview a module locally (adds to registry temporarily)')
|
|
499
|
+
.action(async (modulePath) => {
|
|
500
|
+
try {
|
|
501
|
+
const { previewModule } = await import('./author.js');
|
|
502
|
+
await previewModule(modulePath);
|
|
503
|
+
// Rebuild registry so the previewed module appears
|
|
504
|
+
await buildRegistry(ROOT);
|
|
505
|
+
console.log(chalk.green('Registry rebuilt.'));
|
|
506
|
+
} catch (err) {
|
|
507
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
program.parse();
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamification engine — XP scoring, belt thresholds, level progression.
|
|
3
|
+
* Ported from claude-code-guide's game-mode + level-up skills.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const BELTS = [
|
|
7
|
+
{ name: 'White', minXP: 0, badge: '⬜' },
|
|
8
|
+
{ name: 'Yellow', minXP: 50, badge: '🟨' },
|
|
9
|
+
{ name: 'Green', minXP: 150, badge: '🟩' },
|
|
10
|
+
{ name: 'Blue', minXP: 400, badge: '🟦' },
|
|
11
|
+
{ name: 'Purple', minXP: 800, badge: '🟪' },
|
|
12
|
+
{ name: 'Brown', minXP: 1500, badge: '🟫' },
|
|
13
|
+
{ name: 'Black', minXP: 3000, badge: '⬛' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the current belt for a given XP total.
|
|
18
|
+
*/
|
|
19
|
+
export function getBelt(xp) {
|
|
20
|
+
let belt = BELTS[0];
|
|
21
|
+
for (const b of BELTS) {
|
|
22
|
+
if (xp >= b.minXP) belt = b;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Black belt dans
|
|
26
|
+
if (xp >= 3000) {
|
|
27
|
+
const dans = [
|
|
28
|
+
{ dan: 1, minXP: 3000 },
|
|
29
|
+
{ dan: 2, minXP: 6000 },
|
|
30
|
+
{ dan: 3, minXP: 12000 },
|
|
31
|
+
{ dan: 4, minXP: 24000 },
|
|
32
|
+
{ dan: 5, minXP: 48000 },
|
|
33
|
+
];
|
|
34
|
+
let currentDan = 0;
|
|
35
|
+
for (const d of dans) {
|
|
36
|
+
if (xp >= d.minXP) currentDan = d.dan;
|
|
37
|
+
}
|
|
38
|
+
if (currentDan > 0) {
|
|
39
|
+
return { ...belt, dan: currentDan, display: `⬛ Black Belt (${ordinal(currentDan)} Dan)` };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { ...belt, display: `${belt.badge} ${belt.name} Belt` };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get the next belt target.
|
|
48
|
+
*/
|
|
49
|
+
export function getNextBelt(xp) {
|
|
50
|
+
for (const b of BELTS) {
|
|
51
|
+
if (xp < b.minXP) return b;
|
|
52
|
+
}
|
|
53
|
+
// Already at Black — show next dan
|
|
54
|
+
const dans = [6000, 12000, 24000, 48000];
|
|
55
|
+
for (const threshold of dans) {
|
|
56
|
+
if (xp < threshold) return { name: 'Black (next Dan)', minXP: threshold, badge: '⬛' };
|
|
57
|
+
}
|
|
58
|
+
return null; // Maxed out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format a full dashboard string.
|
|
63
|
+
*/
|
|
64
|
+
export function formatDashboard(progress, registry) {
|
|
65
|
+
const xp = progress.user?.xp || 0;
|
|
66
|
+
const belt = getBelt(xp);
|
|
67
|
+
const next = getNextBelt(xp);
|
|
68
|
+
const modulesCompleted = progress.user?.modules_completed || 0;
|
|
69
|
+
const totalModules = registry?.modules?.length || 0;
|
|
70
|
+
|
|
71
|
+
let out = `\n ╔══════════════════════════════════════╗\n`;
|
|
72
|
+
out += ` ║ ClaudeTeach Dashboard ║\n`;
|
|
73
|
+
out += ` ╠══════════════════════════════════════╣\n`;
|
|
74
|
+
out += ` ║ ${belt.display.padEnd(35)}║\n`;
|
|
75
|
+
out += ` ║ XP: ${String(xp).padEnd(31)}║\n`;
|
|
76
|
+
|
|
77
|
+
if (next) {
|
|
78
|
+
const remaining = next.minXP - xp;
|
|
79
|
+
const progressBar = makeProgressBar(xp, belt.minXP, next.minXP, 20);
|
|
80
|
+
out += ` ║ Next: ${next.badge} ${next.name} (${remaining} XP to go)`.padEnd(39) + `║\n`;
|
|
81
|
+
out += ` ║ ${progressBar.padEnd(35)}║\n`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
out += ` ║ Modules: ${modulesCompleted}/${totalModules} completed`.padEnd(39) + `║\n`;
|
|
85
|
+
out += ` ╚══════════════════════════════════════╝\n`;
|
|
86
|
+
|
|
87
|
+
// Per-module breakdown
|
|
88
|
+
if (progress.modules && Object.keys(progress.modules).length > 0) {
|
|
89
|
+
out += `\n Module Progress:\n`;
|
|
90
|
+
for (const [slug, data] of Object.entries(progress.modules)) {
|
|
91
|
+
const statusIcon = data.status === 'completed' ? '✓' : '◐';
|
|
92
|
+
out += ` ${statusIcon} ${slug} — ${data.xp_earned || 0} XP`;
|
|
93
|
+
if (data.quiz_score) out += ` (quiz: ${data.quiz_score})`;
|
|
94
|
+
out += `\n`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Suggest the next module to learn based on progress.
|
|
103
|
+
*/
|
|
104
|
+
export function suggestNext(progress, registry) {
|
|
105
|
+
const completedSlugs = new Set(
|
|
106
|
+
Object.entries(progress.modules || {})
|
|
107
|
+
.filter(([_, v]) => v.status === 'completed')
|
|
108
|
+
.map(([k]) => k)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const inProgressSlugs = Object.entries(progress.modules || {})
|
|
112
|
+
.filter(([_, v]) => v.status === 'in-progress')
|
|
113
|
+
.map(([k]) => k);
|
|
114
|
+
|
|
115
|
+
const xp = progress.user?.xp || 0;
|
|
116
|
+
const belt = getBelt(xp);
|
|
117
|
+
const next = getNextBelt(xp);
|
|
118
|
+
|
|
119
|
+
let out = `\n ${belt.display}\n`;
|
|
120
|
+
out += ` Total XP: ${xp}\n`;
|
|
121
|
+
if (next) {
|
|
122
|
+
out += ` Next belt: ${next.badge} ${next.name} at ${next.minXP} XP (${next.minXP - xp} to go)\n`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// In-progress modules first
|
|
126
|
+
if (inProgressSlugs.length > 0) {
|
|
127
|
+
out += `\n Continue where you left off:\n`;
|
|
128
|
+
for (const slug of inProgressSlugs) {
|
|
129
|
+
const mod = registry.modules?.find(m => m.slug === slug);
|
|
130
|
+
const data = progress.modules[slug];
|
|
131
|
+
if (mod) {
|
|
132
|
+
out += ` → ${slug}: "${mod.title}"`;
|
|
133
|
+
if (data.walkthrough_step) out += ` (step ${data.walkthrough_step})`;
|
|
134
|
+
out += `\n`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Suggest uncompleted modules sorted by difficulty
|
|
140
|
+
const available = (registry.modules || [])
|
|
141
|
+
.filter(m => !completedSlugs.has(m.slug) && !inProgressSlugs.includes(m.slug))
|
|
142
|
+
.filter(m => {
|
|
143
|
+
// Check prerequisites
|
|
144
|
+
if (!m.prerequisites || m.prerequisites.length === 0) return true;
|
|
145
|
+
return m.prerequisites.every(p => completedSlugs.has(p));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const diffOrder = { beginner: 0, intermediate: 1, advanced: 2 };
|
|
149
|
+
available.sort((a, b) => (diffOrder[a.difficulty] || 0) - (diffOrder[b.difficulty] || 0));
|
|
150
|
+
|
|
151
|
+
if (available.length > 0) {
|
|
152
|
+
out += `\n Recommended next:\n`;
|
|
153
|
+
const suggestions = available.slice(0, 3);
|
|
154
|
+
for (const mod of suggestions) {
|
|
155
|
+
const icon = { beginner: '🌱', intermediate: '🌿', advanced: '🌳' }[mod.difficulty] || ' ';
|
|
156
|
+
const xpInfo = mod.xp ? ` (${Object.values(mod.xp).reduce((a, b) => a + b, 0)} XP)` : '';
|
|
157
|
+
out += ` ${icon} ${mod.slug}: "${mod.title}"${xpInfo}\n`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- helpers ---
|
|
165
|
+
|
|
166
|
+
function makeProgressBar(current, rangeStart, rangeEnd, width) {
|
|
167
|
+
const pct = Math.min(1, Math.max(0, (current - rangeStart) / (rangeEnd - rangeStart)));
|
|
168
|
+
const filled = Math.round(pct * width);
|
|
169
|
+
return '[' + '█'.repeat(filled) + '░'.repeat(width - filled) + '] ' + Math.round(pct * 100) + '%';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function ordinal(n) {
|
|
173
|
+
const s = ['th', 'st', 'nd', 'rd'];
|
|
174
|
+
const v = n % 100;
|
|
175
|
+
return n + (s[(v - 20) % 10] || s[v] || s[0]);
|
|
176
|
+
}
|