dojocho 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/dist/index.js +1464 -0
  4. package/package.json +52 -0
package/dist/index.js ADDED
@@ -0,0 +1,1464 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config.ts
4
+ import {
5
+ CLI,
6
+ DOJOS_DIR,
7
+ ManifestValidationError,
8
+ validateManifest,
9
+ parseManifest,
10
+ defineConfig,
11
+ resolveConfig,
12
+ loadConfig,
13
+ validateDojoRc,
14
+ findProjectRoot,
15
+ readDojoRc,
16
+ writeDojoRc,
17
+ readCatalog,
18
+ dojoDir,
19
+ readDojoMd,
20
+ katasPath,
21
+ resolveKata,
22
+ resolveAllKatas,
23
+ listDojos
24
+ } from "@dojocho/config";
25
+
26
+ // src/commands/root.ts
27
+ import { existsSync as existsSync2, writeFileSync } from "fs";
28
+ import { resolve, relative } from "path";
29
+
30
+ // src/state.ts
31
+ import { existsSync } from "fs";
32
+ function kataState(kata2, progress) {
33
+ if (progress?.completed.includes(kata2.name)) return "completed";
34
+ return existsSync(kata2.workspacePath) ? "ongoing" : "not-started";
35
+ }
36
+ function findCurrentKata(katas, current) {
37
+ if (current) {
38
+ const found = katas.find((k) => k.name === current);
39
+ if (found && existsSync(found.workspacePath)) return found;
40
+ }
41
+ return katas.find((k) => existsSync(k.workspacePath)) ?? null;
42
+ }
43
+ function findNextKata(katas, progress) {
44
+ return katas.find((k) => {
45
+ if (progress?.completed.includes(k.name)) return false;
46
+ return !existsSync(k.workspacePath);
47
+ }) ?? null;
48
+ }
49
+ function completedCount(katas, progress) {
50
+ if (progress) {
51
+ return katas.filter((k) => progress.completed.includes(k.name)).length;
52
+ }
53
+ return 0;
54
+ }
55
+ function findKataByIdOrName(katas, query) {
56
+ const byName = katas.find((k) => k.name === query);
57
+ if (byName) return byName;
58
+ const padded = query.padStart(3, "0");
59
+ return katas.find((k) => k.name.startsWith(padded + "-")) ?? null;
60
+ }
61
+
62
+ // src/agent.ts
63
+ var RUNTIMES = [
64
+ { envVar: "CLAUDECODE", runtime: { name: "claude", askTool: "AskUserQuestion" } },
65
+ { envVar: "OPENCODE", runtime: { name: "opencode", askTool: "question" } },
66
+ { envVar: "GEMINI_CLI", runtime: { name: "gemini", askTool: "ask_user" } }
67
+ ];
68
+ var FALLBACK = { name: "unknown", askTool: "AskUserQuestion or similar tool" };
69
+ function detectRuntime() {
70
+ for (const { envVar, runtime } of RUNTIMES) {
71
+ if (process.env[envVar]) return runtime;
72
+ }
73
+ return FALLBACK;
74
+ }
75
+ function askTool() {
76
+ return detectRuntime().askTool;
77
+ }
78
+
79
+ // src/format.ts
80
+ function invokeAsk(variant) {
81
+ const tool = askTool();
82
+ const formatted = tool.includes(" ") ? tool : `\`${tool}\``;
83
+ return variant ? `Invoke ${formatted} (${variant})` : `Invoke ${formatted}`;
84
+ }
85
+ function status(fields) {
86
+ const body = Object.entries(fields).map(([k, v]) => `${k}: ${v}`).join("\n");
87
+ return `<dojo:status>
88
+ ${body}
89
+ </dojo:status>`;
90
+ }
91
+ function sensei(content) {
92
+ return `<dojo:sensei>
93
+ ${content}
94
+ </dojo:sensei>`;
95
+ }
96
+ function prompt(content) {
97
+ return `<dojo:prompt>
98
+ ${content}
99
+ </dojo:prompt>`;
100
+ }
101
+ function learnings(content) {
102
+ return `<dojo:learnings>
103
+ ${content}
104
+ </dojo:learnings>`;
105
+ }
106
+
107
+ // src/commands/root.ts
108
+ var USAGE = `Usage: ${CLI} <command> [flags]
109
+
110
+ Commands:
111
+ (none) Project-level actions (use flags below)
112
+ kata Kata-level actions (sensei, check, scaffold)
113
+ intro Show the active dojo's introduction
114
+ status Show current dojo/kata state
115
+ setup [--agent] Set up a new dojo project and configure agents
116
+ add <source> Add a dojo (training pack)
117
+ remove <name> Remove a dojo
118
+
119
+ Flags:
120
+ --test/--check Show overall progress
121
+ --list List installed dojos
122
+ --open Print the active DOJO.md
123
+ --change <dojo> Switch active dojo`;
124
+ function root(rootDir, args2) {
125
+ const flag = args2.find((a) => a.startsWith("--"));
126
+ if (!flag) {
127
+ console.log(USAGE);
128
+ return;
129
+ }
130
+ switch (flag) {
131
+ case "--check":
132
+ case "--test":
133
+ check(rootDir);
134
+ break;
135
+ case "--list":
136
+ list(rootDir);
137
+ break;
138
+ case "--open":
139
+ open(rootDir);
140
+ break;
141
+ case "--change": {
142
+ const name = args2[args2.indexOf("--change") + 1];
143
+ if (!name) throw new Error("Usage: dojo --change <dojo>");
144
+ change(rootDir, name);
145
+ break;
146
+ }
147
+ case "--help":
148
+ case "-h":
149
+ console.log(USAGE);
150
+ break;
151
+ default:
152
+ throw new Error(`Unknown flag: ${flag}
153
+
154
+ ${USAGE}`);
155
+ }
156
+ }
157
+ function check(root2) {
158
+ const rc = readDojoRc(root2);
159
+ if (!rc.currentDojo) {
160
+ console.log(`No dojo active. Add one with:
161
+ ${CLI} add <source>`);
162
+ return;
163
+ }
164
+ const catalog = readCatalog(root2, rc.currentDojo);
165
+ const katas = resolveAllKatas(root2, rc, catalog);
166
+ const progress = rc.progress?.[rc.currentDojo];
167
+ const current = findCurrentKata(katas, rc.currentKata);
168
+ const completed = completedCount(katas, progress);
169
+ const total = katas.length;
170
+ if (!current) {
171
+ const next = findNextKata(katas, progress);
172
+ if (next) {
173
+ console.log(`${completed}/${total} katas complete. No kata in progress.`);
174
+ console.log(prompt(`${invokeAsk()} to ask the student:
175
+ - "Start next kata" (Begin the next kata in sequence) \u2192 run: ${CLI} kata --start
176
+ - "Pick a kata" (Browse and choose a specific kata) \u2192 run: ${CLI} kata --list`));
177
+ } else {
178
+ console.log(`All ${total} katas complete. The dojo is finished.`);
179
+ }
180
+ return;
181
+ }
182
+ const workspaceRel = relative(root2, current.workspacePath);
183
+ console.log(`Kata: ${current.name} (in progress)
184
+ ${completed}/${total} complete | Workspace: ${workspaceRel}`);
185
+ console.log(prompt(`${invokeAsk()} to ask the student:
186
+ - "Check progress" \u2192 run: ${CLI} kata --check
187
+ - "Keep working" \u2192 encourage them
188
+ - "Switch kata" \u2192 run: ${CLI} kata --list`));
189
+ }
190
+ function list(root2) {
191
+ const rc = readDojoRc(root2);
192
+ const dojos = listDojos(root2);
193
+ if (dojos.length === 0) {
194
+ console.log(`No dojos installed. Add one with:
195
+ ${CLI} add <source>`);
196
+ return;
197
+ }
198
+ console.log("Dojos:\n");
199
+ for (const name of dojos) {
200
+ const marker = name === rc.currentDojo ? "[*]" : "[ ]";
201
+ console.log(` ${marker} ${name}`);
202
+ }
203
+ }
204
+ function open(root2) {
205
+ const rc = readDojoRc(root2);
206
+ const md = readDojoMd(root2, rc.currentDojo);
207
+ if (md) {
208
+ console.log(md);
209
+ } else {
210
+ console.log("No DOJO.md found. Run `dojo setup` first.");
211
+ }
212
+ }
213
+ function change(root2, name) {
214
+ const dojos = listDojos(root2);
215
+ if (!dojos.includes(name)) {
216
+ throw new Error(`Dojo "${name}" not found. Available: ${dojos.join(", ") || "(none)"}`);
217
+ }
218
+ const rc = readDojoRc(root2);
219
+ rc.currentDojo = name;
220
+ rc.currentKata = null;
221
+ writeDojoRc(root2, rc);
222
+ const dojoTsconfigPath = resolve(root2, DOJOS_DIR, name, "tsconfig.json");
223
+ if (existsSync2(dojoTsconfigPath)) {
224
+ const config = loadConfig(root2);
225
+ const katasInclude = `${relative(root2, config.katasPath)}/**/*.ts`;
226
+ const tsconfigPath = resolve(root2, "tsconfig.json");
227
+ const extendsPath = `./${relative(root2, dojoTsconfigPath)}`;
228
+ writeFileSync(
229
+ tsconfigPath,
230
+ JSON.stringify(
231
+ {
232
+ extends: extendsPath,
233
+ compilerOptions: { noEmit: true },
234
+ include: [katasInclude]
235
+ },
236
+ null,
237
+ 2
238
+ ) + "\n"
239
+ );
240
+ }
241
+ console.log(`Switched to dojo "${name}".`);
242
+ }
243
+
244
+ // src/commands/kata.ts
245
+ import { existsSync as existsSync4, readFileSync as readFileSync2, mkdirSync as mkdirSync2, copyFileSync } from "fs";
246
+ import { execSync as execSync2 } from "child_process";
247
+ import { dirname as dirname2, resolve as resolve3, relative as relative3 } from "path";
248
+
249
+ // src/runner.ts
250
+ import { execSync } from "child_process";
251
+ import { relative as relative2 } from "path";
252
+ function parseVitestJson(raw) {
253
+ const json = JSON.parse(raw);
254
+ const tests = [];
255
+ for (const suite of json.testResults) {
256
+ for (const t of suite.assertionResults) {
257
+ tests.push({
258
+ title: t.title,
259
+ status: t.status === "passed" ? "passed" : "failed",
260
+ failureMessages: t.failureMessages
261
+ });
262
+ }
263
+ }
264
+ return {
265
+ total: json.numTotalTests,
266
+ passed: json.numPassedTests,
267
+ failed: json.numFailedTests,
268
+ tests,
269
+ error: null
270
+ };
271
+ }
272
+ var vitestAdapter = {
273
+ prepareCommand(cmd) {
274
+ return `${cmd} --reporter=json`;
275
+ },
276
+ parseOutput(stdout, _stderr, exitCode) {
277
+ try {
278
+ return parseVitestJson(stdout);
279
+ } catch {
280
+ if (exitCode !== 0) {
281
+ return {
282
+ total: 0,
283
+ passed: 0,
284
+ failed: 0,
285
+ tests: [],
286
+ error: _stderr || "Test execution failed"
287
+ };
288
+ }
289
+ return { total: 0, passed: 0, failed: 0, tests: [], error: "Failed to parse vitest output" };
290
+ }
291
+ }
292
+ };
293
+ var exitCodeAdapter = {
294
+ prepareCommand(cmd) {
295
+ return cmd;
296
+ },
297
+ parseOutput(stdout, stderr, exitCode) {
298
+ if (exitCode === 0) {
299
+ return {
300
+ total: 1,
301
+ passed: 1,
302
+ failed: 0,
303
+ tests: [{ title: "all tests", status: "passed", failureMessages: [] }],
304
+ error: null
305
+ };
306
+ }
307
+ return {
308
+ total: 1,
309
+ passed: 0,
310
+ failed: 1,
311
+ tests: [{ title: "all tests", status: "failed", failureMessages: [stderr || stdout || "Tests failed"] }],
312
+ error: null
313
+ };
314
+ }
315
+ };
316
+ function getAdapter(manifest) {
317
+ const adapterName = manifest.runner?.adapter ?? "vitest";
318
+ return adapterName === "exit-code" ? exitCodeAdapter : vitestAdapter;
319
+ }
320
+ function runTests(kata2, catalog, dojoDir2) {
321
+ const adapter = getAdapter(catalog);
322
+ const testRelPath = relative2(dojoDir2, kata2.testPath);
323
+ const testTemplate = kata2.test ?? catalog.test;
324
+ const testCmd = testTemplate.replace("{template}", testRelPath);
325
+ const cmd = adapter.prepareCommand(testCmd);
326
+ try {
327
+ const output = execSync(cmd, {
328
+ cwd: dojoDir2,
329
+ stdio: ["pipe", "pipe", "pipe"],
330
+ timeout: 6e4
331
+ });
332
+ return adapter.parseOutput(output.toString(), "", 0);
333
+ } catch (err) {
334
+ const e = err;
335
+ const stdout = e.stdout?.toString() ?? "";
336
+ const stderr = e.stderr?.toString() ?? "";
337
+ const exitCode = e.status ?? 1;
338
+ const result = adapter.parseOutput(stdout, stderr, exitCode);
339
+ if (result.error === null && result.total === 0 && stdout === "" && stderr !== "") {
340
+ return { total: 0, passed: 0, failed: 0, tests: [], error: stderr || "Test execution failed" };
341
+ }
342
+ return result;
343
+ }
344
+ }
345
+
346
+ // src/journal.ts
347
+ import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync2, mkdirSync } from "fs";
348
+ import { resolve as resolve2, dirname } from "path";
349
+ function journalPath(root2, dojo) {
350
+ return resolve2(dojoDir(root2, dojo), "JOURNAL.md");
351
+ }
352
+ function appendNote(root2, dojo, kata2, note2) {
353
+ const path = journalPath(root2, dojo);
354
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
355
+ let content;
356
+ if (existsSync3(path)) {
357
+ content = readFileSync(path, "utf8");
358
+ } else {
359
+ mkdirSync(dirname(path), { recursive: true });
360
+ content = "# Learning Journal\n";
361
+ }
362
+ const heading = `## ${kata2}`;
363
+ if (content.includes(heading)) {
364
+ const headingIdx = content.indexOf(heading);
365
+ const nextHeading = content.indexOf("\n## ", headingIdx + heading.length);
366
+ const insertIdx = nextHeading === -1 ? content.length : nextHeading;
367
+ const before = content.slice(0, insertIdx).trimEnd();
368
+ const after = content.slice(insertIdx);
369
+ content = before + `
370
+ - ${note2}
371
+ ` + after;
372
+ } else {
373
+ content += `
374
+ ${heading}
375
+ _${date}_
376
+
377
+ - ${note2}
378
+ `;
379
+ }
380
+ writeFileSync2(path, content);
381
+ }
382
+ function readLearnings(root2, dojo) {
383
+ const path = journalPath(root2, dojo);
384
+ if (!existsSync3(path)) return "";
385
+ return readFileSync(path, "utf8");
386
+ }
387
+
388
+ // src/commands/kata.ts
389
+ function getProgress(rc) {
390
+ return rc.progress?.[rc.currentDojo];
391
+ }
392
+ function recordKataIntro(rc, kataName) {
393
+ rc.progress ??= {};
394
+ rc.progress[rc.currentDojo] ??= { completed: [], lastActive: null };
395
+ const progress = rc.progress[rc.currentDojo];
396
+ progress.kataIntros ??= [];
397
+ if (!progress.kataIntros.includes(kataName)) {
398
+ progress.kataIntros.push(kataName);
399
+ }
400
+ }
401
+ var USAGE2 = `Usage: ${CLI} kata [flags]
402
+
403
+ Flags:
404
+ (none) Show SENSEI.md for current kata (smart fallback)
405
+ intro Show current kata's SENSEI.md briefing
406
+ --start Scaffold next kata
407
+ --test/--check Run tests for current kata
408
+ --list List all katas with state
409
+ --change <name> Switch to a specific kata + scaffold
410
+ --open Open kata in editor
411
+ --note "text" Record a learning observation`;
412
+ function recordCompletion(rc, kataName) {
413
+ rc.progress ??= {};
414
+ rc.progress[rc.currentDojo] ??= { completed: [], lastActive: null };
415
+ const progress = rc.progress[rc.currentDojo];
416
+ if (!progress.completed.includes(kataName)) {
417
+ progress.completed.push(kataName);
418
+ }
419
+ progress.lastActive = kataName;
420
+ }
421
+ function kata(root2, args2) {
422
+ const sub = args2.find((a) => !a.startsWith("--"));
423
+ if (sub === "intro") {
424
+ kataIntro(root2, args2);
425
+ return;
426
+ }
427
+ const flag = args2.find((a) => a.startsWith("--"));
428
+ if (!flag) {
429
+ smart(root2, args2);
430
+ return;
431
+ }
432
+ switch (flag) {
433
+ case "--start":
434
+ start(root2);
435
+ break;
436
+ case "--check":
437
+ case "--test":
438
+ check2(root2, args2);
439
+ break;
440
+ case "--list":
441
+ list2(root2);
442
+ break;
443
+ case "--change": {
444
+ const name = args2[args2.indexOf("--change") + 1];
445
+ if (!name) throw new Error("Usage: dojo kata --change <name>");
446
+ change2(root2, name);
447
+ break;
448
+ }
449
+ case "--note": {
450
+ const text = args2[args2.indexOf("--note") + 1];
451
+ if (!text) throw new Error('Usage: dojo kata --note "observation text"');
452
+ note(root2, text);
453
+ break;
454
+ }
455
+ case "--open":
456
+ open2(root2);
457
+ break;
458
+ case "--help":
459
+ case "-h":
460
+ console.log(USAGE2);
461
+ break;
462
+ default:
463
+ throw new Error(`Unknown flag: ${flag}
464
+
465
+ ${USAGE2}`);
466
+ }
467
+ }
468
+ function note(root2, text) {
469
+ const rc = readDojoRc(root2);
470
+ if (!rc.currentDojo) throw new Error("No dojo active.");
471
+ if (!rc.currentKata) throw new Error("No kata in progress.");
472
+ appendNote(root2, rc.currentDojo, rc.currentKata, text);
473
+ console.log(`Noted for ${rc.currentKata}.`);
474
+ }
475
+ function emitLearnings(root2, dojo) {
476
+ const content = readLearnings(root2, dojo);
477
+ if (content) {
478
+ console.log(learnings(content));
479
+ }
480
+ }
481
+ function kataIntro(root2, args2) {
482
+ if (args2.includes("--done")) {
483
+ kataIntroDone(root2);
484
+ return;
485
+ }
486
+ const rc = readDojoRc(root2);
487
+ if (!rc.currentDojo) {
488
+ console.log(`No dojo active. Add one with:
489
+ ${CLI} add <source>`);
490
+ return;
491
+ }
492
+ if (!rc.currentKata) {
493
+ console.log(`No kata in progress.
494
+
495
+ run: ${CLI} kata --start`);
496
+ return;
497
+ }
498
+ const catalog = readCatalog(root2, rc.currentDojo);
499
+ const katas = resolveAllKatas(root2, rc, catalog);
500
+ const target = findCurrentKata(katas, rc.currentKata);
501
+ if (!target) {
502
+ console.log(`Kata "${rc.currentKata}" not found.`);
503
+ return;
504
+ }
505
+ if (existsSync4(target.senseiPath)) {
506
+ console.log(sensei(readFileSync2(target.senseiPath, "utf8")));
507
+ } else {
508
+ console.log(`No SENSEI.md found for ${target.name}.`);
509
+ }
510
+ emitLearnings(root2, rc.currentDojo);
511
+ console.log(prompt(`Present the kata briefing to the student using the <dojo:sensei> content above.
512
+ Explain the goal, what they'll practice, and any key concepts \u2014 in your own words, do NOT paste sensei content verbatim.
513
+
514
+ Then ${invokeAsk()} to ask the student:
515
+ - "Let's code" (Ready to start) \u2192 run: ${CLI} kata intro --done
516
+ - "Tell me more" (Ask questions first) \u2192 answer using the sensei content, then ask again`));
517
+ }
518
+ function kataIntroDone(root2) {
519
+ const rc = readDojoRc(root2);
520
+ if (!rc.currentDojo || !rc.currentKata) {
521
+ console.log("No kata in progress.");
522
+ return;
523
+ }
524
+ const catalog = readCatalog(root2, rc.currentDojo);
525
+ const katas = resolveAllKatas(root2, rc, catalog);
526
+ const target = findCurrentKata(katas, rc.currentKata);
527
+ recordKataIntro(rc, rc.currentKata);
528
+ writeDojoRc(root2, rc);
529
+ console.log(`Kata "${rc.currentKata}" introduction complete.`);
530
+ if (target) {
531
+ const workspaceRel = relative3(root2, target.workspacePath);
532
+ console.log(prompt(`run: ${CLI} kata --open
533
+
534
+ Do NOT read ${workspaceRel} until the student runs /kata again \u2014 let them write the code first.`));
535
+ }
536
+ }
537
+ function smart(root2, args2) {
538
+ const rc = readDojoRc(root2);
539
+ if (!rc.currentDojo) {
540
+ const md = readDojoMd(root2, "");
541
+ if (md) {
542
+ console.log(md);
543
+ } else {
544
+ console.log(`No dojo active. Add one with:
545
+ ${CLI} add <source>`);
546
+ }
547
+ return;
548
+ }
549
+ let catalog;
550
+ try {
551
+ catalog = readCatalog(root2, rc.currentDojo);
552
+ } catch {
553
+ const md = readDojoMd(root2, rc.currentDojo);
554
+ if (md) {
555
+ console.log(md);
556
+ } else {
557
+ console.log(`Dojo "${rc.currentDojo}" has no dojo.json or DOJO.md.`);
558
+ }
559
+ return;
560
+ }
561
+ const katas = resolveAllKatas(root2, rc, catalog);
562
+ const progress = getProgress(rc);
563
+ const query = args2.find((a) => !a.startsWith("--"));
564
+ const target = query ? findKataByIdOrName(katas, query) : findCurrentKata(katas, rc.currentKata);
565
+ if (target) {
566
+ if (existsSync4(target.senseiPath)) {
567
+ console.log(sensei(readFileSync2(target.senseiPath, "utf8")));
568
+ } else {
569
+ console.log(`No SENSEI.md found for ${target.name}.`);
570
+ }
571
+ emitLearnings(root2, rc.currentDojo);
572
+ return;
573
+ }
574
+ const next = findNextKata(katas, progress);
575
+ if (next) {
576
+ console.log("No kata in progress.");
577
+ console.log(prompt(`${invokeAsk()} to ask the student:
578
+ - "Start next kata" (Begin the next kata in sequence) \u2192 run: ${CLI} kata --start
579
+ - "Pick a kata" (Browse and choose a specific kata) \u2192 run: ${CLI} kata --list`));
580
+ } else {
581
+ const md = readDojoMd(root2, rc.currentDojo);
582
+ if (md) console.log(md);
583
+ else console.log("All katas complete. The dojo is finished.");
584
+ }
585
+ }
586
+ function start(root2) {
587
+ const rc = readDojoRc(root2);
588
+ if (!rc.currentDojo) {
589
+ console.log(`No dojo active. Add one with:
590
+ ${CLI} add <source>`);
591
+ return;
592
+ }
593
+ const catalog = readCatalog(root2, rc.currentDojo);
594
+ const katas = resolveAllKatas(root2, rc, catalog);
595
+ const dojoPath = dojoDir(root2, rc.currentDojo);
596
+ const progress = getProgress(rc);
597
+ const target = findNextKata(katas, progress);
598
+ if (!target) {
599
+ console.log("All katas are scaffolded. The dojo is complete.");
600
+ return;
601
+ }
602
+ scaffold(root2, rc, dojoPath, target);
603
+ }
604
+ function check2(root2, args2) {
605
+ const rc = readDojoRc(root2);
606
+ if (!rc.currentDojo) {
607
+ console.log(`No dojo active. Add one with:
608
+ ${CLI} add <source>`);
609
+ return;
610
+ }
611
+ const catalog = readCatalog(root2, rc.currentDojo);
612
+ const katas = resolveAllKatas(root2, rc, catalog);
613
+ const dojoPath = dojoDir(root2, rc.currentDojo);
614
+ const progress = getProgress(rc);
615
+ const query = args2.find((a) => !a.startsWith("--"));
616
+ const target = query ? findKataByIdOrName(katas, query) : findCurrentKata(katas, rc.currentKata);
617
+ if (!target) {
618
+ const next = findNextKata(katas, progress);
619
+ if (next) {
620
+ console.log("No kata in progress.");
621
+ console.log(prompt(`${invokeAsk()} to ask the student:
622
+ - "Start next kata" (Begin the next kata in sequence) \u2192 run: ${CLI} kata --start
623
+ - "Pick a kata" (Browse and choose a specific kata) \u2192 run: ${CLI} kata --list`));
624
+ } else {
625
+ console.log(`All ${katas.length} katas complete. The dojo is finished.`);
626
+ }
627
+ return;
628
+ }
629
+ const result = runTests(target, catalog, dojoPath);
630
+ const workspaceRel = relative3(root2, target.workspacePath);
631
+ if (result.error) {
632
+ throw new Error(`${target.name}: error
633
+
634
+ ${result.error}`);
635
+ }
636
+ const lines = result.tests.map(
637
+ (t) => ` [${t.status === "passed" ? "x" : " "}] ${t.title}`
638
+ );
639
+ if (result.passed === result.total && result.total > 0) {
640
+ recordCompletion(rc, target.name);
641
+ writeDojoRc(root2, rc);
642
+ console.log(`${target.name}: ${result.total}/${result.total} \u2014 complete!
643
+
644
+ ${lines.join("\n")}`);
645
+ console.log(prompt(`Congratulate the student! All tests are passing. Celebrate their achievement before presenting options.
646
+
647
+ Then ${invokeAsk()} to ask the student:
648
+ - "Review" (Get feedback on idiomatic patterns and potential improvements) \u2192 read ${workspaceRel} and run: ${CLI} kata, suggest improvements (Socratic only)
649
+ - "Move on" (Wrap up with key insight, then start next kata) \u2192 run: ${CLI} kata, follow On Completion (insight + bridge), then run: ${CLI} kata --start
650
+ - "Pause" (Take a break, come back anytime) \u2192 give a friendly sign-off and remind them to run /kata when ready to continue`));
651
+ return;
652
+ }
653
+ console.log(`${target.name}: ${result.passed}/${result.total} passing
654
+
655
+ ${lines.join("\n")}`);
656
+ console.log(prompt(`${invokeAsk()} to ask the student:
657
+ - "Help me" (Get hints based on failing tests) \u2192 run: ${CLI} kata, use the Test Map
658
+ - "Keep working" (Continue on your own) \u2192 encourage them
659
+ - "Pause" (Take a break, come back anytime) \u2192 give a friendly sign-off and remind them to run /kata when ready to continue`));
660
+ }
661
+ function list2(root2) {
662
+ const rc = readDojoRc(root2);
663
+ if (!rc.currentDojo) {
664
+ console.log(`No dojo active. Add one with:
665
+ ${CLI} add <source>`);
666
+ return;
667
+ }
668
+ const catalog = readCatalog(root2, rc.currentDojo);
669
+ const katas = resolveAllKatas(root2, rc, catalog);
670
+ const progress = getProgress(rc);
671
+ const completed = completedCount(katas, progress);
672
+ console.log(`Katas (${completed}/${katas.length} complete):
673
+ `);
674
+ for (const k of katas) {
675
+ const state = kataState(k, progress);
676
+ const marker = state === "completed" ? "[x]" : state === "ongoing" ? "[~]" : "[ ]";
677
+ const current = k.name === rc.currentKata ? " (current)" : "";
678
+ console.log(` ${marker} ${k.name}${current}`);
679
+ }
680
+ }
681
+ function change2(root2, name) {
682
+ const rc = readDojoRc(root2);
683
+ if (!rc.currentDojo) {
684
+ console.log(`No dojo active. Add one with:
685
+ ${CLI} add <source>`);
686
+ return;
687
+ }
688
+ const catalog = readCatalog(root2, rc.currentDojo);
689
+ const katas = resolveAllKatas(root2, rc, catalog);
690
+ const dojoPath = dojoDir(root2, rc.currentDojo);
691
+ const target = findKataByIdOrName(katas, name);
692
+ if (!target) throw new Error(`Kata not found: ${name}`);
693
+ if (existsSync4(target.workspacePath)) {
694
+ rc.currentKata = target.name;
695
+ writeDojoRc(root2, rc);
696
+ const workspaceRel = relative3(root2, target.workspacePath);
697
+ console.log(`Switched to ${target.name}.
698
+ Workspace: ${workspaceRel}`);
699
+ console.log(prompt(`${invokeAsk()} to ask the student:
700
+ - "Check progress" \u2192 run: ${CLI} kata --check
701
+ - "Keep working" \u2192 encourage them`));
702
+ return;
703
+ }
704
+ scaffold(root2, rc, dojoPath, target);
705
+ }
706
+ function open2(root2) {
707
+ const rc = readDojoRc(root2);
708
+ if (!rc.currentKata || !rc.currentDojo) {
709
+ console.log("No kata in progress.");
710
+ return;
711
+ }
712
+ const catalog = readCatalog(root2, rc.currentDojo);
713
+ const katas = resolveAllKatas(root2, rc, catalog);
714
+ const target = findCurrentKata(katas, rc.currentKata);
715
+ if (!target) {
716
+ console.log("No kata in progress.");
717
+ return;
718
+ }
719
+ const editor = rc.editor ?? "code";
720
+ const absPath = target.workspacePath;
721
+ console.log(`Opening ${target.name}...`);
722
+ try {
723
+ execSync2(`${editor} ${absPath}`, { stdio: "inherit" });
724
+ } catch {
725
+ console.log(`Could not open with "${editor}". File: ${absPath}`);
726
+ }
727
+ }
728
+ function scaffold(root2, rc, dojoPath, target) {
729
+ const templateSrc = resolve3(dojoPath, target.template);
730
+ if (!existsSync4(templateSrc)) {
731
+ throw new Error(`Template not found: ${target.template}`);
732
+ }
733
+ mkdirSync2(dirname2(target.workspacePath), { recursive: true });
734
+ copyFileSync(templateSrc, target.workspacePath);
735
+ rc.currentKata = target.name;
736
+ writeDojoRc(root2, rc);
737
+ const workspaceRel = relative3(root2, target.workspacePath);
738
+ const absPath = target.workspacePath;
739
+ let plain = `Kata ${target.name} scaffolded.
740
+ Workspace: ${workspaceRel}`;
741
+ if (rc.editor) {
742
+ plain += `
743
+ Open command: ${rc.editor} ${absPath}`;
744
+ }
745
+ console.log(plain);
746
+ console.log(prompt(`run: ${CLI} kata intro
747
+ Present the kata briefing, then follow the instructions from that command.`));
748
+ }
749
+
750
+ // src/commands/intro.ts
751
+ function intro(root2, args2) {
752
+ if (args2.includes("--done")) {
753
+ markDone(root2);
754
+ return;
755
+ }
756
+ present(root2);
757
+ }
758
+ function present(root2) {
759
+ const rc = readDojoRc(root2);
760
+ if (!rc.currentDojo) {
761
+ const md2 = readDojoMd(root2, "");
762
+ if (md2) console.log(sensei(md2));
763
+ else console.log(`No dojo active. Add one with:
764
+ ${CLI} add <source>`);
765
+ return;
766
+ }
767
+ const md = readDojoMd(root2, rc.currentDojo);
768
+ if (md) console.log(sensei(md));
769
+ else console.log(`Dojo "${rc.currentDojo}" has no DOJO.md.`);
770
+ const dojos = listDojos(root2);
771
+ const others = dojos.filter((d) => d !== rc.currentDojo);
772
+ const switchOptions = others.length > 0 ? "\n" + others.map((d) => `- "${d}" (Switch to this dojo) \u2192 run: ${CLI} --change ${d}`).join("\n") : "";
773
+ console.log(prompt(`Introduce the dojo to the student using the <dojo:sensei> content above.
774
+ Explain what they'll be learning, how katas work (short exercises with tests to guide them), and what to expect.
775
+ Do NOT paste the sensei content verbatim \u2014 summarize and present it in your own words.
776
+
777
+ Then ${invokeAsk()} to ask the student:
778
+ - "Let's begin" (Start the first kata) \u2192 run: ${CLI} intro --done
779
+ - "Tell me more" (Ask questions about the dojo before starting) \u2192 answer using the sensei content, then ask again${switchOptions}`));
780
+ }
781
+ function markDone(root2) {
782
+ const rc = readDojoRc(root2);
783
+ if (!rc.currentDojo) {
784
+ console.log(`No dojo active.`);
785
+ return;
786
+ }
787
+ rc.progress ??= {};
788
+ rc.progress[rc.currentDojo] ??= { completed: [], lastActive: null };
789
+ rc.progress[rc.currentDojo].introduced = true;
790
+ writeDojoRc(root2, rc);
791
+ console.log(`Dojo "${rc.currentDojo}" introduction complete.`);
792
+ }
793
+
794
+ // src/commands/setup.ts
795
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, lstatSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
796
+ import { execSync as execSync3 } from "child_process";
797
+ import { resolve as resolve5 } from "path";
798
+
799
+ // src/pm.ts
800
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
801
+ import { dirname as dirname3, resolve as resolve4 } from "path";
802
+ function detectAt(dir) {
803
+ const pkgPath = resolve4(dir, "package.json");
804
+ if (existsSync5(pkgPath)) {
805
+ try {
806
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
807
+ if (typeof pkg.packageManager === "string") {
808
+ const name = pkg.packageManager.split("@")[0];
809
+ if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm") {
810
+ return name;
811
+ }
812
+ }
813
+ } catch {
814
+ }
815
+ }
816
+ if (existsSync5(resolve4(dir, "pnpm-lock.yaml"))) return "pnpm";
817
+ if (existsSync5(resolve4(dir, "yarn.lock"))) return "yarn";
818
+ if (existsSync5(resolve4(dir, "bun.lockb")) || existsSync5(resolve4(dir, "bun.lock"))) return "bun";
819
+ if (existsSync5(resolve4(dir, "package-lock.json"))) return "npm";
820
+ return null;
821
+ }
822
+ function detectPackageManager(root2) {
823
+ let dir = resolve4(root2);
824
+ const fsRoot = dirname3(dir) === dir ? dir : void 0;
825
+ while (true) {
826
+ const found = detectAt(dir);
827
+ if (found) return found;
828
+ const parent = dirname3(dir);
829
+ if (parent === dir || parent === fsRoot) break;
830
+ dir = parent;
831
+ }
832
+ return "npm";
833
+ }
834
+ function pmCommands(root2) {
835
+ const pm = detectPackageManager(root2);
836
+ switch (pm) {
837
+ case "pnpm":
838
+ return {
839
+ name: pm,
840
+ installSilent: "pnpm install --ignore-workspace --silent",
841
+ add: (pkg) => `pnpm add ${pkg}`
842
+ };
843
+ case "yarn":
844
+ return {
845
+ name: pm,
846
+ installSilent: "yarn install --silent",
847
+ add: (pkg) => `yarn add ${pkg}`
848
+ };
849
+ case "bun":
850
+ return {
851
+ name: pm,
852
+ installSilent: "bun install --silent",
853
+ add: (pkg) => `bun add ${pkg}`
854
+ };
855
+ case "npm":
856
+ default:
857
+ return {
858
+ name: pm,
859
+ installSilent: "npm install --silent",
860
+ add: (pkg) => `npm install ${pkg}`
861
+ };
862
+ }
863
+ }
864
+
865
+ // src/commands/setup.ts
866
+ var AGENTS = {
867
+ claude: { dir: ".claude", hasSettings: true },
868
+ opencode: { dir: ".opencode", hasSettings: false },
869
+ codex: { dir: ".codex", hasSettings: false },
870
+ gemini: { dir: ".gemini", hasSettings: false }
871
+ };
872
+ var CLAUDE_SETTINGS = {
873
+ permissions: {
874
+ allow: [
875
+ "Bash(dojo *)",
876
+ "Bash(dojo)"
877
+ ],
878
+ deny: [
879
+ "Read(.dojos/**)",
880
+ "Glob(.dojos/**)",
881
+ "Grep(.dojos/**)"
882
+ ]
883
+ }
884
+ };
885
+ var DEFAULT_DOJO_MD = `!\`dojo $ARGUMENTS\`
886
+
887
+ Follow any \`<dojo:prompt>\` instructions in the output.
888
+ `;
889
+ var DEFAULT_KATA_MD = `!\`dojo status\`
890
+
891
+ ## Protocol
892
+
893
+ CLI output uses XML tags to separate directives from student content:
894
+
895
+ - \`<dojo:status>\` \u2014 Machine state. Parse the \`run:\` line and execute it.
896
+ - \`<dojo:prompt>\` \u2014 Interaction spec. Follow the instructions inside.
897
+ - \`<dojo:sensei>\` \u2014 Teaching material. Internalize but never show verbatim.
898
+ - \`<dojo:learnings>\` \u2014 Prior student observations. Use to personalize teaching.
899
+ - **Unwrapped text** \u2014 Student-facing. Display as-is.
900
+
901
+ ## Flow
902
+
903
+ 1. Parse \`<dojo:status>\` above.
904
+ 2. If state is \`complete\`, congratulate the student.
905
+ 3. If state is \`no-dojo\`, tell them to run \`dojo add <source>\`.
906
+ 4. Otherwise, execute the \`run:\` command.
907
+ 5. After running, follow any \`<dojo:prompt>\` instructions.
908
+ 6. Use \`<dojo:sensei>\` content to guide teaching \u2014 never paste it to the student.
909
+ 7. If \`<dojo:learnings>\` is present, use it to personalize teaching based on prior observations.
910
+ `;
911
+ var DEFAULT_KATA_MD_CLAUDE = `!\`dojo status\`
912
+
913
+ ## Identity
914
+
915
+ You are a kata sensei. Teach through Socratic dialogue \u2014 questions, hints, nudges \u2014 never give solutions directly.
916
+
917
+ ## Protocol
918
+
919
+ CLI output uses XML tags to separate directives from student content:
920
+
921
+ - \`<dojo:status>\` \u2014 Machine state. Parse the \`run:\` line and execute it.
922
+ - \`<dojo:prompt>\` \u2014 Interaction spec. Follow the instructions inside.
923
+ - \`<dojo:sensei>\` \u2014 Teaching material. Internalize but **never** show verbatim to the student.
924
+ - \`<dojo:learnings>\` \u2014 Prior student observations. Use to personalize teaching.
925
+ - **Unwrapped text** \u2014 Student-facing. Display as-is.
926
+
927
+ ## Flow
928
+
929
+ 1. Parse \`<dojo:status>\` above.
930
+ 2. If state is \`complete\`, congratulate the student.
931
+ 3. If state is \`no-dojo\`, tell them to run \`dojo add <source>\`.
932
+ 4. Otherwise, execute the \`run:\` command via Bash.
933
+ 5. Parse the output. Display any unwrapped student-facing text.
934
+ 6. Internalize \`<dojo:sensei>\` as your teaching material \u2014 teach exclusively from it, do NOT rely on outside knowledge.
935
+ 7. If \`<dojo:learnings>\` is present, personalize: build on what the student knows, skip mastered concepts, address past struggles.
936
+ 8. Follow \`<dojo:prompt>\` instructions. If there is no \`<dojo:prompt>\`, present the content and ask what the student would like to do.
937
+ 9. **Use AskUserQuestion** for ALL student interactions \u2014 present choices, ask questions, gather responses. Never list numbered options as plain text.
938
+ 10. When the task or student responses require CLI commands (\`dojo kata --check\`, \`dojo kata intro --done\`, \`dojo kata --open\`, etc.), run them via Bash.
939
+ 11. If the teaching material contains Reference URLs, surface them to the student.
940
+ 12. Before finishing, record 1\u20133 key observations about the student by running \`dojo kata --note "observation"\` for each.
941
+ 13. Drive the full session: AskUserQuestion \u2192 process response \u2192 run commands if needed \u2192 AskUserQuestion again. Do not stop after one exchange.
942
+ `;
943
+ var ROOT_DOJO_MD = `# Welcome to Dojocho
944
+
945
+ Your dojo is set up and ready. You just need a dojo (training pack) to start practicing.
946
+
947
+ ## Add a dojo
948
+
949
+ \`\`\`bash
950
+ dojo add <source>
951
+ \`\`\`
952
+
953
+ Source can be:
954
+ - A local path: \`dojo add ./path/to/dojo\`
955
+ - A git repo: \`dojo add org/repo\`
956
+ - Official dojos: \`dojo add effect-ts\`
957
+
958
+ ## Start practicing
959
+
960
+ Once a dojo is added, use \`/kata\` in your coding agent to begin.
961
+ `;
962
+ var DOJO_CONFIG = `import { defineConfig } from "@dojocho/config"
963
+
964
+ export default defineConfig()
965
+ `;
966
+ var DEFAULT_RC = {
967
+ currentDojo: "",
968
+ currentKata: null,
969
+ editor: "code",
970
+ progress: {}
971
+ };
972
+ function setup(root2, args2) {
973
+ const explicit = Object.keys(AGENTS).filter(
974
+ (a) => args2.includes(`--${a}`)
975
+ );
976
+ if (explicit.length === 0) {
977
+ promptAgents();
978
+ return;
979
+ }
980
+ scaffold2(root2);
981
+ setupAgents(root2, explicit);
982
+ const kataCmd = explicit.length === 1 ? `${explicit[0]} "/kata"` : "/kata in your agent prompt";
983
+ console.log(`Dojo ready.
984
+
985
+ Add a dojo with: ${CLI} add <source>
986
+ Then use: ${kataCmd}`);
987
+ }
988
+ function promptAgents() {
989
+ const options = [
990
+ `- "Claude Code" \u2192 --claude`,
991
+ `- "OpenCode" \u2192 --opencode`,
992
+ `- "Codex" \u2192 --codex`,
993
+ `- "Gemini CLI" \u2192 --gemini`
994
+ ].join("\n");
995
+ console.log(prompt(`${invokeAsk("multiSelect")} to ask the student:
996
+ Which coding agents do you use?
997
+ ${options}
998
+
999
+ Then run: ${CLI} setup --<agent1> --<agent2> ...`));
1000
+ }
1001
+ function scaffold2(root2) {
1002
+ const rcPath = resolve5(root2, ".dojorc");
1003
+ if (!existsSync6(rcPath)) {
1004
+ writeDojoRc(root2, DEFAULT_RC);
1005
+ }
1006
+ mkdirSync3(resolve5(root2, DOJOS_DIR), { recursive: true });
1007
+ const dojoMdPath = resolve5(root2, DOJOS_DIR, "DOJO.md");
1008
+ if (!existsSync6(dojoMdPath)) {
1009
+ writeFileSync3(dojoMdPath, ROOT_DOJO_MD);
1010
+ }
1011
+ const configPath = resolve5(root2, "dojo.config.ts");
1012
+ if (!existsSync6(configPath)) {
1013
+ writeFileSync3(configPath, DOJO_CONFIG);
1014
+ }
1015
+ const tsconfigPath = resolve5(root2, "tsconfig.json");
1016
+ if (!existsSync6(tsconfigPath)) {
1017
+ writeFileSync3(
1018
+ tsconfigPath,
1019
+ JSON.stringify(
1020
+ {
1021
+ compilerOptions: {
1022
+ target: "ES2022",
1023
+ module: "ES2022",
1024
+ moduleResolution: "bundler",
1025
+ strict: true,
1026
+ noEmit: true
1027
+ },
1028
+ include: ["katas/**/*.ts"]
1029
+ },
1030
+ null,
1031
+ 2
1032
+ ) + "\n"
1033
+ );
1034
+ }
1035
+ const pkgPath = resolve5(root2, "package.json");
1036
+ if (!existsSync6(pkgPath)) {
1037
+ writeFileSync3(
1038
+ pkgPath,
1039
+ JSON.stringify({ type: "module", private: true }, null, 2) + "\n"
1040
+ );
1041
+ }
1042
+ const pm = pmCommands(root2);
1043
+ console.log("Installing @dojocho/config...");
1044
+ execSync3(pm.add("@dojocho/config"), { cwd: root2, stdio: "pipe" });
1045
+ }
1046
+ function setupAgents(root2, agents) {
1047
+ for (const agent of agents) {
1048
+ const dir = AGENTS[agent].dir;
1049
+ mkdirSync3(resolve5(root2, dir, "commands"), { recursive: true });
1050
+ mkdirSync3(resolve5(root2, dir, "skills"), { recursive: true });
1051
+ const dojoMd = resolve5(root2, dir, "commands", "dojo.md");
1052
+ try {
1053
+ if (lstatSync(dojoMd).isSymbolicLink()) unlinkSync(dojoMd);
1054
+ } catch {
1055
+ }
1056
+ if (!existsSync6(dojoMd)) {
1057
+ writeFileSync3(dojoMd, DEFAULT_DOJO_MD);
1058
+ }
1059
+ const kataMdContent = agent === "claude" ? DEFAULT_KATA_MD_CLAUDE : DEFAULT_KATA_MD;
1060
+ const kataMd = resolve5(root2, dir, "commands", "kata.md");
1061
+ try {
1062
+ if (lstatSync(kataMd).isSymbolicLink()) unlinkSync(kataMd);
1063
+ } catch {
1064
+ }
1065
+ if (!existsSync6(kataMd)) {
1066
+ writeFileSync3(kataMd, kataMdContent);
1067
+ }
1068
+ if (AGENTS[agent].hasSettings) {
1069
+ const settingsPath = resolve5(root2, dir, "settings.json");
1070
+ writeFileSync3(settingsPath, JSON.stringify(CLAUDE_SETTINGS, null, 2) + "\n");
1071
+ }
1072
+ }
1073
+ }
1074
+ function configuredAgents(root2) {
1075
+ return Object.keys(AGENTS).filter(
1076
+ (a) => existsSync6(resolve5(root2, AGENTS[a].dir))
1077
+ );
1078
+ }
1079
+
1080
+ // src/commands/add.ts
1081
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, cpSync, renameSync, unlinkSync as unlinkSync3, symlinkSync, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
1082
+ import { execSync as execSync4, execFileSync } from "child_process";
1083
+ import { resolve as resolve7, relative as relative4 } from "path";
1084
+ import { tmpdir } from "os";
1085
+
1086
+ // src/commands/remove.ts
1087
+ import { existsSync as existsSync7, rmSync, readdirSync, lstatSync as lstatSync2, readlinkSync, unlinkSync as unlinkSync2 } from "fs";
1088
+ import { resolve as resolve6 } from "path";
1089
+ function remove(root2, args2) {
1090
+ const name = args2.find((a) => !a.startsWith("--"));
1091
+ if (!name) throw new Error("Usage: dojo remove <name>");
1092
+ const dojoPath = resolve6(root2, DOJOS_DIR, name);
1093
+ if (!existsSync7(dojoPath)) {
1094
+ throw new Error(`Dojo "${name}" not found at ${DOJOS_DIR}/${name}`);
1095
+ }
1096
+ runLifecycleScript(root2, dojoPath, "teardown.sh");
1097
+ rmSync(dojoPath, { recursive: true, force: true });
1098
+ for (const agent of configuredAgents(root2)) {
1099
+ const dir = AGENTS[agent].dir;
1100
+ for (const sub of ["commands", "skills"]) {
1101
+ const subDir = resolve6(root2, dir, sub);
1102
+ if (!existsSync7(subDir)) continue;
1103
+ for (const entry of readdirSync(subDir)) {
1104
+ const link = resolve6(subDir, entry);
1105
+ try {
1106
+ if (!lstatSync2(link).isSymbolicLink()) continue;
1107
+ const target = readlinkSync(link);
1108
+ if (target.includes(`${DOJOS_DIR}/${name}/`) || target.includes(`${DOJOS_DIR}/${name}\\`)) {
1109
+ unlinkSync2(link);
1110
+ }
1111
+ } catch {
1112
+ }
1113
+ }
1114
+ }
1115
+ }
1116
+ try {
1117
+ const rc = readDojoRc(root2);
1118
+ if (rc.currentDojo === name) {
1119
+ rc.currentDojo = "";
1120
+ rc.currentKata = null;
1121
+ writeDojoRc(root2, rc);
1122
+ }
1123
+ } catch {
1124
+ }
1125
+ console.log(`Dojo "${name}" removed.`);
1126
+ }
1127
+
1128
+ // src/commands/add.ts
1129
+ async function add(root2, args2) {
1130
+ const source = args2.find((a) => !a.startsWith("--"));
1131
+ const force = args2.includes("--force");
1132
+ if (!source) {
1133
+ throw new Error(`Usage: dojo add <source>
1134
+
1135
+ Source can be:
1136
+ Local path: dojo add ./path/to/dojo
1137
+ npm package: dojo add @dojocho/effect-ts
1138
+ Registry: dojo add effect-ts
1139
+ URL: dojo add https://example.com/dojo.tgz
1140
+
1141
+ Flags:
1142
+ --force Overwrite existing dojo`);
1143
+ }
1144
+ const sourceType = classifySource(source);
1145
+ switch (sourceType) {
1146
+ case "local":
1147
+ addLocal(root2, source, force);
1148
+ break;
1149
+ case "npm":
1150
+ addNpm(root2, source, force);
1151
+ break;
1152
+ case "url":
1153
+ addUrl(root2, source, force);
1154
+ break;
1155
+ case "registry":
1156
+ await addFromRegistry(root2, source, force);
1157
+ break;
1158
+ }
1159
+ }
1160
+ function classifySource(source) {
1161
+ if (source.startsWith(".") || source.startsWith("/")) return "local";
1162
+ if (source.startsWith("https://") || source.startsWith("http://")) return "url";
1163
+ if (source.startsWith("@") || source.includes("/")) return "npm";
1164
+ return "registry";
1165
+ }
1166
+ function safeExtract(tarball, cwd) {
1167
+ const listing = execFileSync("tar", ["-tzf", tarball], { cwd, encoding: "utf8" });
1168
+ const unsafe = listing.split("\n").some((e) => e.startsWith("/") || e.includes(".."));
1169
+ if (unsafe) {
1170
+ throw new Error("Refusing to extract: tarball contains unsafe paths (absolute or ../)");
1171
+ }
1172
+ execFileSync("tar", ["xzf", tarball], { cwd, stdio: "pipe" });
1173
+ }
1174
+ function moveDir(src, dest) {
1175
+ try {
1176
+ renameSync(src, dest);
1177
+ } catch {
1178
+ cpSync(src, dest, { recursive: true });
1179
+ }
1180
+ }
1181
+ function handleExisting(root2, name, force) {
1182
+ const targetPath = dojoDir(root2, name);
1183
+ if (!existsSync8(targetPath)) return;
1184
+ if (!force) {
1185
+ throw new Error(`Dojo "${name}" already exists at ${DOJOS_DIR}/${name}
1186
+
1187
+ To update: dojo add ${name} --force
1188
+ To remove: dojo remove ${name}`);
1189
+ }
1190
+ remove(root2, [name]);
1191
+ }
1192
+ function addLocal(root2, source, force) {
1193
+ const sourcePath = resolve7(source);
1194
+ if (!existsSync8(sourcePath)) {
1195
+ throw new Error(`Source not found: ${sourcePath}`);
1196
+ }
1197
+ const tmpDir = resolve7(tmpdir(), `dojocho-${Date.now()}`);
1198
+ mkdirSync4(tmpDir, { recursive: true });
1199
+ try {
1200
+ const sourcePm = detectPackageManager(sourcePath);
1201
+ execSync4(`${sourcePm} pack --pack-destination ${tmpDir}`, { cwd: sourcePath, stdio: "pipe" });
1202
+ const tarballs = readdirSync2(tmpDir).filter((f) => f.endsWith(".tgz"));
1203
+ if (tarballs.length === 0) throw new Error(`Failed to pack ${sourcePath}`);
1204
+ safeExtract(tarballs[0], tmpDir);
1205
+ installExtracted(root2, resolve7(tmpDir, "package"), source, force);
1206
+ } finally {
1207
+ rmSync2(tmpDir, { recursive: true, force: true });
1208
+ }
1209
+ }
1210
+ function installExtracted(root2, extractedDir, source, force) {
1211
+ const dojoJsonPath = resolve7(extractedDir, "dojo.json");
1212
+ if (!existsSync8(dojoJsonPath)) {
1213
+ throw new Error(`${source} is not a dojo \u2014 missing dojo.json`);
1214
+ }
1215
+ const manifest = parseManifest(readFileSync4(dojoJsonPath, "utf8"), dojoJsonPath);
1216
+ const name = manifest.name.includes("/") ? manifest.name.split("/").pop() : manifest.name;
1217
+ const pm = pmCommands(root2);
1218
+ const pkgPath = resolve7(extractedDir, "package.json");
1219
+ if (existsSync8(pkgPath)) {
1220
+ console.log(`Installing ${name} dependencies...`);
1221
+ execSync4(pm.installSilent, { cwd: extractedDir, stdio: "pipe" });
1222
+ }
1223
+ handleExisting(root2, name, force);
1224
+ const targetPath = dojoDir(root2, name);
1225
+ mkdirSync4(resolve7(root2, DOJOS_DIR), { recursive: true });
1226
+ moveDir(extractedDir, targetPath);
1227
+ finalize(root2, name, targetPath);
1228
+ }
1229
+ function addNpm(root2, source, force) {
1230
+ const tmpDir = resolve7(tmpdir(), `dojocho-${Date.now()}`);
1231
+ mkdirSync4(tmpDir, { recursive: true });
1232
+ try {
1233
+ console.log(`Fetching ${source}...`);
1234
+ execSync4(`npm pack ${source} --pack-destination .`, { cwd: tmpDir, stdio: "pipe" });
1235
+ const tarballs = readdirSync2(tmpDir).filter((f) => f.endsWith(".tgz"));
1236
+ if (tarballs.length === 0) throw new Error(`Failed to download ${source}`);
1237
+ safeExtract(tarballs[0], tmpDir);
1238
+ installExtracted(root2, resolve7(tmpDir, "package"), source, force);
1239
+ } finally {
1240
+ rmSync2(tmpDir, { recursive: true, force: true });
1241
+ }
1242
+ }
1243
+ function validateRegistryItem(data) {
1244
+ if (typeof data !== "object" || data === null) {
1245
+ throw new Error("Invalid registry response: expected a JSON object");
1246
+ }
1247
+ const obj = data;
1248
+ if (typeof obj.name !== "string" || typeof obj.version !== "string" || typeof obj.description !== "string") {
1249
+ throw new Error("Invalid registry item: missing name, version, or description");
1250
+ }
1251
+ if (typeof obj.source !== "object" || obj.source === null) {
1252
+ throw new Error("Invalid registry item: missing source");
1253
+ }
1254
+ const src = obj.source;
1255
+ if (src.type === "npm" && typeof src.package === "string") {
1256
+ return obj;
1257
+ }
1258
+ if (src.type === "tarball" && typeof src.url === "string") {
1259
+ return obj;
1260
+ }
1261
+ throw new Error(`Invalid registry item: source must be npm or tarball`);
1262
+ }
1263
+ async function addFromRegistry(root2, name, force) {
1264
+ const config = loadConfig(root2, { command: "add" });
1265
+ for (const [registryName, urlTemplate] of Object.entries(config.registries)) {
1266
+ const url = urlTemplate.replace("{name}", name);
1267
+ try {
1268
+ const res = await fetch(url);
1269
+ if (!res.ok) continue;
1270
+ const item = validateRegistryItem(await res.json());
1271
+ if (item.source.type === "npm") {
1272
+ return addNpm(root2, item.source.package, force);
1273
+ }
1274
+ return addUrl(root2, item.source.url, force);
1275
+ } catch (err) {
1276
+ if (err instanceof Error && err.message.startsWith("Invalid registry")) throw err;
1277
+ console.log(`Registry "${registryName}" unreachable: ${url}`);
1278
+ }
1279
+ }
1280
+ throw new Error(`"${name}" not found in any registry.
1281
+
1282
+ Try:
1283
+ npm package: dojo add @dojocho/${name}
1284
+ Local path: dojo add ./path/to/${name}`);
1285
+ }
1286
+ function addUrl(root2, url, force) {
1287
+ const tmpDir = resolve7(tmpdir(), `dojocho-${Date.now()}`);
1288
+ mkdirSync4(tmpDir, { recursive: true });
1289
+ try {
1290
+ console.log(`Fetching ${url}...`);
1291
+ execFileSync("curl", ["-fsSL", "-o", "dojo.tgz", url], { cwd: tmpDir, stdio: "pipe" });
1292
+ safeExtract("dojo.tgz", tmpDir);
1293
+ const extractedDir = existsSync8(resolve7(tmpDir, "package", "dojo.json")) ? resolve7(tmpDir, "package") : tmpDir;
1294
+ installExtracted(root2, extractedDir, url, force);
1295
+ } finally {
1296
+ rmSync2(tmpDir, { recursive: true, force: true });
1297
+ }
1298
+ }
1299
+ function finalize(root2, name, targetPath) {
1300
+ const rc = readDojoRc(root2);
1301
+ rc.currentDojo = name;
1302
+ writeDojoRc(root2, rc);
1303
+ const dojoPkgPath = resolve7(targetPath, "package.json");
1304
+ const dojoTsconfigPath = resolve7(targetPath, "tsconfig.json");
1305
+ if (existsSync8(dojoPkgPath) && existsSync8(dojoTsconfigPath)) {
1306
+ const dojoPkg = JSON.parse(readFileSync4(dojoPkgPath, "utf8"));
1307
+ const dojoTsconfig = JSON.parse(readFileSync4(dojoTsconfigPath, "utf8"));
1308
+ const deps = Object.keys(dojoPkg.dependencies ?? {});
1309
+ if (deps.length > 0) {
1310
+ dojoTsconfig.compilerOptions ??= {};
1311
+ const paths = dojoTsconfig.compilerOptions.paths ?? {};
1312
+ for (const dep of deps) {
1313
+ paths[dep] = [`./node_modules/${dep}`];
1314
+ paths[`${dep}/*`] = [`./node_modules/${dep}/*`];
1315
+ }
1316
+ dojoTsconfig.compilerOptions.paths = paths;
1317
+ writeFileSync4(dojoTsconfigPath, JSON.stringify(dojoTsconfig, null, 2) + "\n");
1318
+ }
1319
+ }
1320
+ const dojo = loadConfig(root2);
1321
+ const katasInclude = `${relative4(root2, dojo.katasPath)}/**/*.ts`;
1322
+ const tsconfigPath = resolve7(root2, "tsconfig.json");
1323
+ const extendsPath = `./${relative4(root2, resolve7(targetPath, "tsconfig.json"))}`;
1324
+ writeFileSync4(
1325
+ tsconfigPath,
1326
+ JSON.stringify(
1327
+ {
1328
+ extends: extendsPath,
1329
+ compilerOptions: { noEmit: true },
1330
+ include: [katasInclude]
1331
+ },
1332
+ null,
1333
+ 2
1334
+ ) + "\n"
1335
+ );
1336
+ symlinkDojo(root2, targetPath);
1337
+ const agents = configuredAgents(root2);
1338
+ const kataCmd = agents.length === 1 ? `${agents[0]} "/kata"` : "/kata";
1339
+ console.log(`Dojo "${name}" added.
1340
+
1341
+ Location: ${DOJOS_DIR}/${name}
1342
+ Active: ${name}
1343
+ Command: ${kataCmd}`);
1344
+ runLifecycleScript(root2, targetPath, "prepare.sh");
1345
+ }
1346
+ function symlinkDir(sourceDir, targetDir, filter) {
1347
+ if (!existsSync8(sourceDir)) return;
1348
+ mkdirSync4(targetDir, { recursive: true });
1349
+ for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
1350
+ if (!filter(entry)) continue;
1351
+ const link = resolve7(targetDir, entry.name);
1352
+ if (existsSync8(link)) unlinkSync3(link);
1353
+ symlinkSync(relative4(targetDir, resolve7(sourceDir, entry.name)), link);
1354
+ }
1355
+ }
1356
+ function runLifecycleScript(root2, dojoPath, script) {
1357
+ const scriptPath = resolve7(dojoPath, script);
1358
+ if (!existsSync8(scriptPath)) return;
1359
+ try {
1360
+ execSync4(`bash ${scriptPath}`, {
1361
+ cwd: dojoPath,
1362
+ stdio: "inherit",
1363
+ env: { ...process.env, PROJECT_ROOT: root2 }
1364
+ });
1365
+ } catch {
1366
+ console.log(`Warning: ${script} exited with errors.`);
1367
+ }
1368
+ }
1369
+ function symlinkDojo(root2, dojoPath) {
1370
+ for (const agent of configuredAgents(root2)) {
1371
+ const dir = AGENTS[agent].dir;
1372
+ symlinkDir(resolve7(dojoPath, "commands"), resolve7(root2, dir, "commands"), (e) => e.name.endsWith(".md"));
1373
+ symlinkDir(resolve7(dojoPath, "skills"), resolve7(root2, dir, "skills"), (e) => e.isDirectory());
1374
+ }
1375
+ }
1376
+
1377
+ // src/commands/status.ts
1378
+ function status2(root2, _args) {
1379
+ const rc = readDojoRc(root2);
1380
+ if (!rc.currentDojo) {
1381
+ console.log(status({ state: "no-dojo" }));
1382
+ return;
1383
+ }
1384
+ const progress = rc.progress?.[rc.currentDojo];
1385
+ if (!progress?.introduced) {
1386
+ const fields = {
1387
+ state: "intro",
1388
+ dojo: rc.currentDojo,
1389
+ run: `${CLI} intro`
1390
+ };
1391
+ const dojos = listDojos(root2);
1392
+ if (dojos.length > 1) {
1393
+ fields.dojos = dojos.join(", ");
1394
+ }
1395
+ console.log(status(fields));
1396
+ return;
1397
+ }
1398
+ let catalog;
1399
+ try {
1400
+ catalog = readCatalog(root2, rc.currentDojo);
1401
+ } catch {
1402
+ console.log(status({
1403
+ state: "intro",
1404
+ dojo: rc.currentDojo,
1405
+ run: `${CLI} intro`
1406
+ }));
1407
+ return;
1408
+ }
1409
+ const katas = resolveAllKatas(root2, rc, catalog);
1410
+ const completed = completedCount(katas, progress);
1411
+ const total = katas.length;
1412
+ const current = findCurrentKata(katas, rc.currentKata);
1413
+ if (!current) {
1414
+ const next = findNextKata(katas, progress);
1415
+ if (!next) {
1416
+ console.log(status({
1417
+ state: "complete",
1418
+ dojo: rc.currentDojo,
1419
+ progress: `${completed}/${total}`
1420
+ }));
1421
+ } else {
1422
+ console.log(status({
1423
+ state: "no-kata",
1424
+ dojo: rc.currentDojo,
1425
+ progress: `${completed}/${total}`,
1426
+ run: `${CLI} kata --start`
1427
+ }));
1428
+ }
1429
+ return;
1430
+ }
1431
+ const briefed = progress?.kataIntros?.includes(current.name) === true;
1432
+ console.log(status({
1433
+ state: briefed ? "practicing" : "kata-intro",
1434
+ dojo: rc.currentDojo,
1435
+ kata: current.name,
1436
+ progress: `${completed}/${total}`,
1437
+ run: `${CLI} kata ${briefed ? "--check" : "intro"}`
1438
+ }));
1439
+ }
1440
+
1441
+ // src/index.ts
1442
+ var [command, ...args] = process.argv.slice(2);
1443
+ async function main() {
1444
+ if (command === "kata") {
1445
+ kata(findProjectRoot(), args);
1446
+ } else if (command === "intro") {
1447
+ intro(findProjectRoot(), args);
1448
+ } else if (command === "setup") {
1449
+ setup(process.cwd(), args);
1450
+ } else if (command === "add") {
1451
+ await add(process.cwd(), args);
1452
+ } else if (command === "remove") {
1453
+ remove(findProjectRoot(), args);
1454
+ } else if (command === "status") {
1455
+ status2(findProjectRoot(), args);
1456
+ } else {
1457
+ root(process.cwd(), [command, ...args].filter(Boolean));
1458
+ }
1459
+ }
1460
+ main().catch((err) => {
1461
+ const message = err instanceof Error ? err.message : String(err);
1462
+ console.error(message);
1463
+ process.exit(1);
1464
+ });