@dojocho/cli 0.0.1
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/dist/index.js +1020 -0
- package/package.json +20 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1020 @@
|
|
|
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, lstatSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
|
|
28
|
+
import { execSync } from "child_process";
|
|
29
|
+
import { resolve, relative } from "path";
|
|
30
|
+
|
|
31
|
+
// src/state.ts
|
|
32
|
+
import { existsSync } from "fs";
|
|
33
|
+
function kataState(kata2, progress) {
|
|
34
|
+
if (progress?.completed.includes(kata2.name)) return "completed";
|
|
35
|
+
return existsSync(kata2.workspacePath) ? "ongoing" : "not-started";
|
|
36
|
+
}
|
|
37
|
+
function findCurrentKata(katas, current) {
|
|
38
|
+
if (current) {
|
|
39
|
+
const found = katas.find((k) => k.name === current);
|
|
40
|
+
if (found && existsSync(found.workspacePath)) return found;
|
|
41
|
+
}
|
|
42
|
+
return katas.find((k) => existsSync(k.workspacePath)) ?? null;
|
|
43
|
+
}
|
|
44
|
+
function findNextKata(katas, progress) {
|
|
45
|
+
return katas.find((k) => {
|
|
46
|
+
if (progress?.completed.includes(k.name)) return false;
|
|
47
|
+
return !existsSync(k.workspacePath);
|
|
48
|
+
}) ?? null;
|
|
49
|
+
}
|
|
50
|
+
function completedCount(katas, progress) {
|
|
51
|
+
if (progress) {
|
|
52
|
+
return katas.filter((k) => progress.completed.includes(k.name)).length;
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
function findKataByIdOrName(katas, query) {
|
|
57
|
+
const byName = katas.find((k) => k.name === query);
|
|
58
|
+
if (byName) return byName;
|
|
59
|
+
const padded = query.padStart(3, "0");
|
|
60
|
+
return katas.find((k) => k.name.startsWith(padded + "-")) ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/commands/root.ts
|
|
64
|
+
var USAGE = `Usage: ${CLI} <command> [flags]
|
|
65
|
+
|
|
66
|
+
Commands:
|
|
67
|
+
(none) Project-level actions (use flags below)
|
|
68
|
+
kata Kata-level actions (sensei, check, scaffold)
|
|
69
|
+
add <source> Add a dojo (training pack)
|
|
70
|
+
remove <name> Remove a dojo
|
|
71
|
+
|
|
72
|
+
Flags:
|
|
73
|
+
--start Initialize a new dojo project
|
|
74
|
+
--test/--check Show overall progress
|
|
75
|
+
--list List installed dojos
|
|
76
|
+
--open Print the active DOJO.md
|
|
77
|
+
--change <dojo> Switch active dojo`;
|
|
78
|
+
var ROOT_DOJO_MD = `# Welcome to Dojocho
|
|
79
|
+
|
|
80
|
+
Your dojo is set up and ready. You just need a dojo (training pack) to start practicing.
|
|
81
|
+
|
|
82
|
+
## Add a dojo
|
|
83
|
+
|
|
84
|
+
\`\`\`bash
|
|
85
|
+
dojo add <source>
|
|
86
|
+
\`\`\`
|
|
87
|
+
|
|
88
|
+
Source can be:
|
|
89
|
+
- A local path: \`dojo add ./path/to/dojo\`
|
|
90
|
+
- A git repo: \`dojo add org/repo\`
|
|
91
|
+
- Official dojos: \`dojo add effect-ts\`
|
|
92
|
+
|
|
93
|
+
## Start practicing
|
|
94
|
+
|
|
95
|
+
Once a dojo is added, use \`/kata\` in your coding agent to begin.
|
|
96
|
+
`;
|
|
97
|
+
var DOJO_CONFIG = `import { defineConfig } from "@dojocho/config"
|
|
98
|
+
|
|
99
|
+
export default defineConfig()
|
|
100
|
+
`;
|
|
101
|
+
var CLAUDE_SETTINGS = {
|
|
102
|
+
permissions: {
|
|
103
|
+
allow: [
|
|
104
|
+
"Bash(dojo *)",
|
|
105
|
+
"Bash(dojo)",
|
|
106
|
+
"AskUserQuestion"
|
|
107
|
+
],
|
|
108
|
+
deny: [
|
|
109
|
+
"Read(.dojos/**)",
|
|
110
|
+
"Glob(.dojos/**)",
|
|
111
|
+
"Grep(.dojos/**)"
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var DEFAULT_KATA_MD = `!\`dojo kata\`
|
|
116
|
+
`;
|
|
117
|
+
var DEFAULT_RC = {
|
|
118
|
+
currentDojo: "",
|
|
119
|
+
currentKata: null,
|
|
120
|
+
editor: "code",
|
|
121
|
+
progress: {}
|
|
122
|
+
};
|
|
123
|
+
function root(rootDir, args2) {
|
|
124
|
+
const flag = args2.find((a) => a.startsWith("--"));
|
|
125
|
+
if (!flag) {
|
|
126
|
+
console.log(USAGE);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
switch (flag) {
|
|
130
|
+
case "--start":
|
|
131
|
+
start(rootDir);
|
|
132
|
+
break;
|
|
133
|
+
case "--check":
|
|
134
|
+
case "--test":
|
|
135
|
+
check(rootDir);
|
|
136
|
+
break;
|
|
137
|
+
case "--list":
|
|
138
|
+
list(rootDir);
|
|
139
|
+
break;
|
|
140
|
+
case "--open":
|
|
141
|
+
open(rootDir);
|
|
142
|
+
break;
|
|
143
|
+
case "--change": {
|
|
144
|
+
const name = args2[args2.indexOf("--change") + 1];
|
|
145
|
+
if (!name) throw new Error("Usage: dojo --change <dojo>");
|
|
146
|
+
change(rootDir, name);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case "--help":
|
|
150
|
+
case "-h":
|
|
151
|
+
console.log(USAGE);
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
throw new Error(`Unknown flag: ${flag}
|
|
155
|
+
|
|
156
|
+
${USAGE}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function start(root2) {
|
|
160
|
+
const rcPath = resolve(root2, ".dojorc");
|
|
161
|
+
if (!existsSync2(rcPath)) {
|
|
162
|
+
writeDojoRc(root2, DEFAULT_RC);
|
|
163
|
+
}
|
|
164
|
+
mkdirSync(resolve(root2, DOJOS_DIR), { recursive: true });
|
|
165
|
+
const dojoMdPath = resolve(root2, DOJOS_DIR, "DOJO.md");
|
|
166
|
+
if (!existsSync2(dojoMdPath)) {
|
|
167
|
+
writeFileSync(dojoMdPath, ROOT_DOJO_MD);
|
|
168
|
+
}
|
|
169
|
+
const configPath = resolve(root2, "dojo.config.ts");
|
|
170
|
+
if (!existsSync2(configPath)) {
|
|
171
|
+
writeFileSync(configPath, DOJO_CONFIG);
|
|
172
|
+
}
|
|
173
|
+
const tsconfigPath = resolve(root2, "tsconfig.json");
|
|
174
|
+
if (!existsSync2(tsconfigPath)) {
|
|
175
|
+
writeFileSync(
|
|
176
|
+
tsconfigPath,
|
|
177
|
+
JSON.stringify(
|
|
178
|
+
{
|
|
179
|
+
compilerOptions: {
|
|
180
|
+
target: "ES2022",
|
|
181
|
+
module: "ES2022",
|
|
182
|
+
moduleResolution: "bundler",
|
|
183
|
+
strict: true,
|
|
184
|
+
noEmit: true
|
|
185
|
+
},
|
|
186
|
+
include: ["katas/**/*.ts"]
|
|
187
|
+
},
|
|
188
|
+
null,
|
|
189
|
+
2
|
|
190
|
+
) + "\n"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const pkgPath = resolve(root2, "package.json");
|
|
194
|
+
if (!existsSync2(pkgPath)) {
|
|
195
|
+
writeFileSync(
|
|
196
|
+
pkgPath,
|
|
197
|
+
JSON.stringify({ type: "module", private: true }, null, 2) + "\n"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
console.log("Installing @dojocho/config...");
|
|
201
|
+
try {
|
|
202
|
+
execSync("pnpm add @dojocho/config", { cwd: root2, stdio: "pipe" });
|
|
203
|
+
} catch {
|
|
204
|
+
console.log(
|
|
205
|
+
" Could not install @dojocho/config from registry.\n If not yet published, link it manually:\n pnpm link <path-to-dojocho>/packages/config"
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const agentDirs = [".claude", ".opencode", ".codex"];
|
|
209
|
+
for (const dir of agentDirs) {
|
|
210
|
+
mkdirSync(resolve(root2, dir, "commands"), { recursive: true });
|
|
211
|
+
mkdirSync(resolve(root2, dir, "skills"), { recursive: true });
|
|
212
|
+
const kataMd = resolve(root2, dir, "commands", "kata.md");
|
|
213
|
+
try {
|
|
214
|
+
if (lstatSync(kataMd).isSymbolicLink()) unlinkSync(kataMd);
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
if (!existsSync2(kataMd)) {
|
|
218
|
+
writeFileSync(kataMd, DEFAULT_KATA_MD);
|
|
219
|
+
}
|
|
220
|
+
if (dir === ".claude") {
|
|
221
|
+
const settingsPath = resolve(root2, dir, "settings.json");
|
|
222
|
+
writeFileSync(settingsPath, JSON.stringify(CLAUDE_SETTINGS, null, 2) + "\n");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
console.log(`Dojo ready.
|
|
226
|
+
|
|
227
|
+
Add a dojo with: dojo add <source>
|
|
228
|
+
Then use: /kata`);
|
|
229
|
+
}
|
|
230
|
+
function check(root2) {
|
|
231
|
+
const rc = readDojoRc(root2);
|
|
232
|
+
if (!rc.currentDojo) {
|
|
233
|
+
console.log(`No dojo active. Add one with:
|
|
234
|
+
${CLI} add <source>`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const catalog = readCatalog(root2, rc.currentDojo);
|
|
238
|
+
const katas = resolveAllKatas(root2, rc, catalog);
|
|
239
|
+
const progress = rc.progress?.[rc.currentDojo];
|
|
240
|
+
const current = findCurrentKata(katas, rc.currentKata);
|
|
241
|
+
const completed = completedCount(katas, progress);
|
|
242
|
+
const total = katas.length;
|
|
243
|
+
if (!current) {
|
|
244
|
+
const next = findNextKata(katas, progress);
|
|
245
|
+
if (next) {
|
|
246
|
+
console.log(`${completed}/${total} katas complete. No kata in progress.
|
|
247
|
+
|
|
248
|
+
Invoke AskUserQuestion (or similar tool) to ask the student:
|
|
249
|
+
- "Start next kata" (Begin the next kata in sequence) \u2192 run: ${CLI} kata --start
|
|
250
|
+
- "Pick a kata" (Browse and choose a specific kata) \u2192 run: ${CLI} kata --list`);
|
|
251
|
+
} else {
|
|
252
|
+
console.log(`All ${total} katas complete. The dojo is finished.`);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const workspaceRel = relative(root2, current.workspacePath);
|
|
257
|
+
console.log(`Kata: ${current.name} (in progress)
|
|
258
|
+
${completed}/${total} complete | Workspace: ${workspaceRel}
|
|
259
|
+
|
|
260
|
+
Invoke AskUserQuestion (or similar tool) to ask the student:
|
|
261
|
+
- "Check progress" \u2192 run: ${CLI} kata --check
|
|
262
|
+
- "Keep working" \u2192 encourage them
|
|
263
|
+
- "Switch kata" \u2192 run: ${CLI} kata --list`);
|
|
264
|
+
}
|
|
265
|
+
function list(root2) {
|
|
266
|
+
const rc = readDojoRc(root2);
|
|
267
|
+
const dojos = listDojos(root2);
|
|
268
|
+
if (dojos.length === 0) {
|
|
269
|
+
console.log(`No dojos installed. Add one with:
|
|
270
|
+
${CLI} add <source>`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
console.log("Dojos:\n");
|
|
274
|
+
for (const name of dojos) {
|
|
275
|
+
const marker = name === rc.currentDojo ? "[*]" : "[ ]";
|
|
276
|
+
console.log(` ${marker} ${name}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function open(root2) {
|
|
280
|
+
const rc = readDojoRc(root2);
|
|
281
|
+
const md = readDojoMd(root2, rc.currentDojo);
|
|
282
|
+
if (md) {
|
|
283
|
+
console.log(md);
|
|
284
|
+
} else {
|
|
285
|
+
console.log("No DOJO.md found. Run `dojo --start` first.");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function change(root2, name) {
|
|
289
|
+
const dojos = listDojos(root2);
|
|
290
|
+
if (!dojos.includes(name)) {
|
|
291
|
+
throw new Error(`Dojo "${name}" not found. Available: ${dojos.join(", ") || "(none)"}`);
|
|
292
|
+
}
|
|
293
|
+
const rc = readDojoRc(root2);
|
|
294
|
+
rc.currentDojo = name;
|
|
295
|
+
rc.currentKata = null;
|
|
296
|
+
writeDojoRc(root2, rc);
|
|
297
|
+
const dojoTsconfigPath = resolve(root2, DOJOS_DIR, name, "tsconfig.json");
|
|
298
|
+
if (existsSync2(dojoTsconfigPath)) {
|
|
299
|
+
const config = loadConfig(root2);
|
|
300
|
+
const katasInclude = `${relative(root2, config.katasPath)}/**/*.ts`;
|
|
301
|
+
const tsconfigPath = resolve(root2, "tsconfig.json");
|
|
302
|
+
const extendsPath = `./${relative(root2, dojoTsconfigPath)}`;
|
|
303
|
+
writeFileSync(
|
|
304
|
+
tsconfigPath,
|
|
305
|
+
JSON.stringify(
|
|
306
|
+
{
|
|
307
|
+
extends: extendsPath,
|
|
308
|
+
compilerOptions: { noEmit: true },
|
|
309
|
+
include: [katasInclude]
|
|
310
|
+
},
|
|
311
|
+
null,
|
|
312
|
+
2
|
|
313
|
+
) + "\n"
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
console.log(`Switched to dojo "${name}".`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/commands/kata.ts
|
|
320
|
+
import { existsSync as existsSync3, readFileSync, mkdirSync as mkdirSync2, copyFileSync } from "fs";
|
|
321
|
+
import { execSync as execSync3 } from "child_process";
|
|
322
|
+
import { dirname, resolve as resolve2, relative as relative3 } from "path";
|
|
323
|
+
|
|
324
|
+
// src/runner.ts
|
|
325
|
+
import { execSync as execSync2 } from "child_process";
|
|
326
|
+
import { relative as relative2 } from "path";
|
|
327
|
+
function parseVitestJson(raw) {
|
|
328
|
+
const json = JSON.parse(raw);
|
|
329
|
+
const tests = [];
|
|
330
|
+
for (const suite of json.testResults) {
|
|
331
|
+
for (const t of suite.assertionResults) {
|
|
332
|
+
tests.push({
|
|
333
|
+
title: t.title,
|
|
334
|
+
status: t.status === "passed" ? "passed" : "failed",
|
|
335
|
+
failureMessages: t.failureMessages
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
total: json.numTotalTests,
|
|
341
|
+
passed: json.numPassedTests,
|
|
342
|
+
failed: json.numFailedTests,
|
|
343
|
+
tests,
|
|
344
|
+
error: null
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
var vitestAdapter = {
|
|
348
|
+
prepareCommand(cmd) {
|
|
349
|
+
return `${cmd} --reporter=json`;
|
|
350
|
+
},
|
|
351
|
+
parseOutput(stdout, _stderr, exitCode) {
|
|
352
|
+
try {
|
|
353
|
+
return parseVitestJson(stdout);
|
|
354
|
+
} catch {
|
|
355
|
+
if (exitCode !== 0) {
|
|
356
|
+
return {
|
|
357
|
+
total: 0,
|
|
358
|
+
passed: 0,
|
|
359
|
+
failed: 0,
|
|
360
|
+
tests: [],
|
|
361
|
+
error: _stderr || "Test execution failed"
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return { total: 0, passed: 0, failed: 0, tests: [], error: "Failed to parse vitest output" };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
var exitCodeAdapter = {
|
|
369
|
+
prepareCommand(cmd) {
|
|
370
|
+
return cmd;
|
|
371
|
+
},
|
|
372
|
+
parseOutput(stdout, stderr, exitCode) {
|
|
373
|
+
if (exitCode === 0) {
|
|
374
|
+
return {
|
|
375
|
+
total: 1,
|
|
376
|
+
passed: 1,
|
|
377
|
+
failed: 0,
|
|
378
|
+
tests: [{ title: "all tests", status: "passed", failureMessages: [] }],
|
|
379
|
+
error: null
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
total: 1,
|
|
384
|
+
passed: 0,
|
|
385
|
+
failed: 1,
|
|
386
|
+
tests: [{ title: "all tests", status: "failed", failureMessages: [stderr || stdout || "Tests failed"] }],
|
|
387
|
+
error: null
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
function getAdapter(manifest) {
|
|
392
|
+
const adapterName = manifest.runner?.adapter ?? "vitest";
|
|
393
|
+
return adapterName === "exit-code" ? exitCodeAdapter : vitestAdapter;
|
|
394
|
+
}
|
|
395
|
+
function runTests(kata2, catalog, dojoDir2) {
|
|
396
|
+
const adapter = getAdapter(catalog);
|
|
397
|
+
const testRelPath = relative2(dojoDir2, kata2.testPath);
|
|
398
|
+
const testTemplate = kata2.test ?? catalog.test;
|
|
399
|
+
const testCmd = testTemplate.replace("{template}", testRelPath);
|
|
400
|
+
const cmd = adapter.prepareCommand(testCmd);
|
|
401
|
+
try {
|
|
402
|
+
const output = execSync2(cmd, {
|
|
403
|
+
cwd: dojoDir2,
|
|
404
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
405
|
+
timeout: 6e4
|
|
406
|
+
});
|
|
407
|
+
return adapter.parseOutput(output.toString(), "", 0);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
const e = err;
|
|
410
|
+
const stdout = e.stdout?.toString() ?? "";
|
|
411
|
+
const stderr = e.stderr?.toString() ?? "";
|
|
412
|
+
const exitCode = e.status ?? 1;
|
|
413
|
+
const result = adapter.parseOutput(stdout, stderr, exitCode);
|
|
414
|
+
if (result.error === null && result.total === 0 && stdout === "" && stderr !== "") {
|
|
415
|
+
return { total: 0, passed: 0, failed: 0, tests: [], error: stderr || "Test execution failed" };
|
|
416
|
+
}
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/commands/kata.ts
|
|
422
|
+
var USAGE2 = `Usage: ${CLI} kata [flags]
|
|
423
|
+
|
|
424
|
+
Flags:
|
|
425
|
+
(none) Show SENSEI.md for current kata (smart fallback)
|
|
426
|
+
--start Scaffold next kata
|
|
427
|
+
--test/--check Run tests for current kata
|
|
428
|
+
--list List all katas with state
|
|
429
|
+
--change <name> Switch to a specific kata + scaffold
|
|
430
|
+
--open Open kata in editor`;
|
|
431
|
+
function getProgress(rc) {
|
|
432
|
+
return rc.progress?.[rc.currentDojo];
|
|
433
|
+
}
|
|
434
|
+
function recordCompletion(rc, kataName) {
|
|
435
|
+
rc.progress ??= {};
|
|
436
|
+
rc.progress[rc.currentDojo] ??= { completed: [], lastActive: null };
|
|
437
|
+
const progress = rc.progress[rc.currentDojo];
|
|
438
|
+
if (!progress.completed.includes(kataName)) {
|
|
439
|
+
progress.completed.push(kataName);
|
|
440
|
+
}
|
|
441
|
+
progress.lastActive = kataName;
|
|
442
|
+
}
|
|
443
|
+
function kata(root2, args2) {
|
|
444
|
+
const flag = args2.find((a) => a.startsWith("--"));
|
|
445
|
+
if (!flag) {
|
|
446
|
+
smart(root2, args2);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
switch (flag) {
|
|
450
|
+
case "--start":
|
|
451
|
+
start2(root2);
|
|
452
|
+
break;
|
|
453
|
+
case "--check":
|
|
454
|
+
case "--test":
|
|
455
|
+
check2(root2, args2);
|
|
456
|
+
break;
|
|
457
|
+
case "--list":
|
|
458
|
+
list2(root2);
|
|
459
|
+
break;
|
|
460
|
+
case "--change": {
|
|
461
|
+
const name = args2[args2.indexOf("--change") + 1];
|
|
462
|
+
if (!name) throw new Error("Usage: dojo kata --change <name>");
|
|
463
|
+
change2(root2, name);
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
case "--open":
|
|
467
|
+
open2(root2);
|
|
468
|
+
break;
|
|
469
|
+
case "--help":
|
|
470
|
+
case "-h":
|
|
471
|
+
console.log(USAGE2);
|
|
472
|
+
break;
|
|
473
|
+
default:
|
|
474
|
+
throw new Error(`Unknown flag: ${flag}
|
|
475
|
+
|
|
476
|
+
${USAGE2}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function smart(root2, args2) {
|
|
480
|
+
const rc = readDojoRc(root2);
|
|
481
|
+
if (!rc.currentDojo) {
|
|
482
|
+
const md = readDojoMd(root2, "");
|
|
483
|
+
if (md) {
|
|
484
|
+
console.log(md);
|
|
485
|
+
} else {
|
|
486
|
+
console.log(`No dojo active. Add one with:
|
|
487
|
+
${CLI} add <source>`);
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
let catalog;
|
|
492
|
+
try {
|
|
493
|
+
catalog = readCatalog(root2, rc.currentDojo);
|
|
494
|
+
} catch {
|
|
495
|
+
const md = readDojoMd(root2, rc.currentDojo);
|
|
496
|
+
if (md) {
|
|
497
|
+
console.log(md);
|
|
498
|
+
} else {
|
|
499
|
+
console.log(`Dojo "${rc.currentDojo}" has no dojo.json or DOJO.md.`);
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const katas = resolveAllKatas(root2, rc, catalog);
|
|
504
|
+
const progress = getProgress(rc);
|
|
505
|
+
const query = args2.find((a) => !a.startsWith("--"));
|
|
506
|
+
const target = query ? findKataByIdOrName(katas, query) : findCurrentKata(katas, rc.currentKata);
|
|
507
|
+
if (target) {
|
|
508
|
+
if (existsSync3(target.senseiPath)) {
|
|
509
|
+
console.log(readFileSync(target.senseiPath, "utf8"));
|
|
510
|
+
} else {
|
|
511
|
+
console.log(`No SENSEI.md found for ${target.name}.`);
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const next = findNextKata(katas, progress);
|
|
516
|
+
if (next) {
|
|
517
|
+
console.log(`No kata in progress.
|
|
518
|
+
|
|
519
|
+
Invoke AskUserQuestion (or similar tool) to ask the student:
|
|
520
|
+
- "Start next kata" (Begin the next kata in sequence) \u2192 run: ${CLI} kata --start
|
|
521
|
+
- "Pick a kata" (Browse and choose a specific kata) \u2192 run: ${CLI} kata --list`);
|
|
522
|
+
} else {
|
|
523
|
+
const md = readDojoMd(root2, rc.currentDojo);
|
|
524
|
+
if (md) console.log(md);
|
|
525
|
+
else console.log("All katas complete. The dojo is finished.");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function start2(root2) {
|
|
529
|
+
const rc = readDojoRc(root2);
|
|
530
|
+
if (!rc.currentDojo) {
|
|
531
|
+
console.log(`No dojo active. Add one with:
|
|
532
|
+
${CLI} add <source>`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const catalog = readCatalog(root2, rc.currentDojo);
|
|
536
|
+
const katas = resolveAllKatas(root2, rc, catalog);
|
|
537
|
+
const dojoPath = dojoDir(root2, rc.currentDojo);
|
|
538
|
+
const progress = getProgress(rc);
|
|
539
|
+
const target = findNextKata(katas, progress);
|
|
540
|
+
if (!target) {
|
|
541
|
+
console.log("All katas are scaffolded. The dojo is complete.");
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
scaffold(root2, rc, dojoPath, target);
|
|
545
|
+
}
|
|
546
|
+
function check2(root2, args2) {
|
|
547
|
+
const rc = readDojoRc(root2);
|
|
548
|
+
if (!rc.currentDojo) {
|
|
549
|
+
console.log(`No dojo active. Add one with:
|
|
550
|
+
${CLI} add <source>`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const catalog = readCatalog(root2, rc.currentDojo);
|
|
554
|
+
const katas = resolveAllKatas(root2, rc, catalog);
|
|
555
|
+
const dojoPath = dojoDir(root2, rc.currentDojo);
|
|
556
|
+
const progress = getProgress(rc);
|
|
557
|
+
const query = args2.find((a) => !a.startsWith("--"));
|
|
558
|
+
const target = query ? findKataByIdOrName(katas, query) : findCurrentKata(katas, rc.currentKata);
|
|
559
|
+
if (!target) {
|
|
560
|
+
const next = findNextKata(katas, progress);
|
|
561
|
+
if (next) {
|
|
562
|
+
console.log(`No kata in progress.
|
|
563
|
+
|
|
564
|
+
Invoke AskUserQuestion (or similar tool) to ask the student:
|
|
565
|
+
- "Start next kata" (Begin the next kata in sequence) \u2192 run: ${CLI} kata --start
|
|
566
|
+
- "Pick a kata" (Browse and choose a specific kata) \u2192 run: ${CLI} kata --list`);
|
|
567
|
+
} else {
|
|
568
|
+
console.log(`All ${katas.length} katas complete. The dojo is finished.`);
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const result = runTests(target, catalog, dojoPath);
|
|
573
|
+
const workspaceRel = relative3(root2, target.workspacePath);
|
|
574
|
+
if (result.error) {
|
|
575
|
+
throw new Error(`${target.name}: error
|
|
576
|
+
|
|
577
|
+
${result.error}`);
|
|
578
|
+
}
|
|
579
|
+
const lines = result.tests.map(
|
|
580
|
+
(t) => ` [${t.status === "passed" ? "x" : " "}] ${t.title}`
|
|
581
|
+
);
|
|
582
|
+
if (result.passed === result.total && result.total > 0) {
|
|
583
|
+
recordCompletion(rc, target.name);
|
|
584
|
+
writeDojoRc(root2, rc);
|
|
585
|
+
console.log(`${target.name}: ${result.total}/${result.total} \u2014 complete!
|
|
586
|
+
|
|
587
|
+
${lines.join("\n")}
|
|
588
|
+
|
|
589
|
+
Invoke AskUserQuestion (or similar tool) to ask the student:
|
|
590
|
+
- "Review" (Get feedback on idiomatic patterns and potential improvements) \u2192 read ${workspaceRel} and run: ${CLI} kata, suggest improvements (Socratic only)
|
|
591
|
+
- "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
|
|
592
|
+
- "Pause" (Take a break, come back anytime) \u2192 give a friendly sign-off and remind them to run /kata when ready to continue`);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
console.log(`${target.name}: ${result.passed}/${result.total} passing
|
|
596
|
+
|
|
597
|
+
${lines.join("\n")}
|
|
598
|
+
|
|
599
|
+
Invoke AskUserQuestion (or similar tool) to ask the student:
|
|
600
|
+
- "Help me" (Get hints based on failing tests) \u2192 run: ${CLI} kata, use the Test Map
|
|
601
|
+
- "Keep working" (Continue on your own) \u2192 encourage them
|
|
602
|
+
- "Pause" (Take a break, come back anytime) \u2192 give a friendly sign-off and remind them to run /kata when ready to continue`);
|
|
603
|
+
}
|
|
604
|
+
function list2(root2) {
|
|
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 progress = getProgress(rc);
|
|
614
|
+
const completed = completedCount(katas, progress);
|
|
615
|
+
console.log(`Katas (${completed}/${katas.length} complete):
|
|
616
|
+
`);
|
|
617
|
+
for (const k of katas) {
|
|
618
|
+
const state = kataState(k, progress);
|
|
619
|
+
const marker = state === "completed" ? "[x]" : state === "ongoing" ? "[~]" : "[ ]";
|
|
620
|
+
const current = k.name === rc.currentKata ? " (current)" : "";
|
|
621
|
+
console.log(` ${marker} ${k.name}${current}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function change2(root2, name) {
|
|
625
|
+
const rc = readDojoRc(root2);
|
|
626
|
+
if (!rc.currentDojo) {
|
|
627
|
+
console.log(`No dojo active. Add one with:
|
|
628
|
+
${CLI} add <source>`);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const catalog = readCatalog(root2, rc.currentDojo);
|
|
632
|
+
const katas = resolveAllKatas(root2, rc, catalog);
|
|
633
|
+
const dojoPath = dojoDir(root2, rc.currentDojo);
|
|
634
|
+
const target = findKataByIdOrName(katas, name);
|
|
635
|
+
if (!target) throw new Error(`Kata not found: ${name}`);
|
|
636
|
+
if (existsSync3(target.workspacePath)) {
|
|
637
|
+
rc.currentKata = target.name;
|
|
638
|
+
writeDojoRc(root2, rc);
|
|
639
|
+
const workspaceRel = relative3(root2, target.workspacePath);
|
|
640
|
+
console.log(`Switched to ${target.name}.
|
|
641
|
+
Workspace: ${workspaceRel}
|
|
642
|
+
|
|
643
|
+
Invoke AskUserQuestion (or similar tool) to ask the student:
|
|
644
|
+
- "Check progress" \u2192 run: ${CLI} kata --check
|
|
645
|
+
- "Keep working" \u2192 encourage them`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
scaffold(root2, rc, dojoPath, target);
|
|
649
|
+
}
|
|
650
|
+
function open2(root2) {
|
|
651
|
+
const rc = readDojoRc(root2);
|
|
652
|
+
if (!rc.currentKata || !rc.currentDojo) {
|
|
653
|
+
console.log("No kata in progress.");
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const catalog = readCatalog(root2, rc.currentDojo);
|
|
657
|
+
const katas = resolveAllKatas(root2, rc, catalog);
|
|
658
|
+
const target = findCurrentKata(katas, rc.currentKata);
|
|
659
|
+
if (!target) {
|
|
660
|
+
console.log("No kata in progress.");
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const editor = rc.editor ?? "code";
|
|
664
|
+
const absPath = target.workspacePath;
|
|
665
|
+
console.log(`Opening ${target.name}...`);
|
|
666
|
+
try {
|
|
667
|
+
execSync3(`${editor} ${absPath}`, { stdio: "inherit" });
|
|
668
|
+
} catch {
|
|
669
|
+
console.log(`Could not open with "${editor}". File: ${absPath}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function scaffold(root2, rc, dojoPath, target) {
|
|
673
|
+
const templateSrc = resolve2(dojoPath, target.template);
|
|
674
|
+
if (!existsSync3(templateSrc)) {
|
|
675
|
+
throw new Error(`Template not found: ${target.template}`);
|
|
676
|
+
}
|
|
677
|
+
mkdirSync2(dirname(target.workspacePath), { recursive: true });
|
|
678
|
+
copyFileSync(templateSrc, target.workspacePath);
|
|
679
|
+
rc.currentKata = target.name;
|
|
680
|
+
writeDojoRc(root2, rc);
|
|
681
|
+
const workspaceRel = relative3(root2, target.workspacePath);
|
|
682
|
+
const absPath = target.workspacePath;
|
|
683
|
+
let output = `Kata ${target.name} scaffolded.
|
|
684
|
+
Workspace: ${workspaceRel}`;
|
|
685
|
+
if (rc.editor) {
|
|
686
|
+
output += `
|
|
687
|
+
Open command: ${rc.editor} ${absPath}`;
|
|
688
|
+
}
|
|
689
|
+
output += `
|
|
690
|
+
|
|
691
|
+
run: ${CLI} kata
|
|
692
|
+
Present the briefing from SENSEI.
|
|
693
|
+
|
|
694
|
+
Invoke AskUserQuestion (or similar tool) to ask the student:
|
|
695
|
+
- "Open the file" \u2192 run: ${CLI} kata --open
|
|
696
|
+
- "I have questions" \u2192 answer using SENSEI guidance`;
|
|
697
|
+
console.log(output);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/commands/add.ts
|
|
701
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, cpSync, unlinkSync as unlinkSync3, symlinkSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, realpathSync, rmSync as rmSync2 } from "fs";
|
|
702
|
+
import { execSync as execSync4, execFileSync } from "child_process";
|
|
703
|
+
import { resolve as resolve4, basename, relative as relative4 } from "path";
|
|
704
|
+
import { tmpdir } from "os";
|
|
705
|
+
|
|
706
|
+
// src/commands/remove.ts
|
|
707
|
+
import { existsSync as existsSync4, rmSync, readdirSync, lstatSync as lstatSync2, readlinkSync, unlinkSync as unlinkSync2 } from "fs";
|
|
708
|
+
import { resolve as resolve3 } from "path";
|
|
709
|
+
function remove(root2, args2) {
|
|
710
|
+
const name = args2.find((a) => !a.startsWith("--"));
|
|
711
|
+
if (!name) throw new Error("Usage: dojo remove <name>");
|
|
712
|
+
const dojoPath = resolve3(root2, DOJOS_DIR, name);
|
|
713
|
+
if (!existsSync4(dojoPath)) {
|
|
714
|
+
throw new Error(`Dojo "${name}" not found at ${DOJOS_DIR}/${name}`);
|
|
715
|
+
}
|
|
716
|
+
rmSync(dojoPath, { recursive: true, force: true });
|
|
717
|
+
const agentDirs = [".claude", ".opencode", ".codex"];
|
|
718
|
+
for (const dir of agentDirs) {
|
|
719
|
+
for (const sub of ["commands", "skills"]) {
|
|
720
|
+
const subDir = resolve3(root2, dir, sub);
|
|
721
|
+
if (!existsSync4(subDir)) continue;
|
|
722
|
+
for (const entry of readdirSync(subDir)) {
|
|
723
|
+
const link = resolve3(subDir, entry);
|
|
724
|
+
try {
|
|
725
|
+
if (!lstatSync2(link).isSymbolicLink()) continue;
|
|
726
|
+
const target = readlinkSync(link);
|
|
727
|
+
if (target.includes(`${DOJOS_DIR}/${name}/`) || target.includes(`${DOJOS_DIR}/${name}\\`)) {
|
|
728
|
+
unlinkSync2(link);
|
|
729
|
+
}
|
|
730
|
+
} catch {
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
const rc = readDojoRc(root2);
|
|
737
|
+
if (rc.currentDojo === name) {
|
|
738
|
+
rc.currentDojo = "";
|
|
739
|
+
rc.currentKata = null;
|
|
740
|
+
writeDojoRc(root2, rc);
|
|
741
|
+
}
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
console.log(`Dojo "${name}" removed.`);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/commands/add.ts
|
|
748
|
+
async function add(root2, args2) {
|
|
749
|
+
const source = args2.find((a) => !a.startsWith("--"));
|
|
750
|
+
const force = args2.includes("--force");
|
|
751
|
+
if (!source) {
|
|
752
|
+
throw new Error(`Usage: dojo add <source>
|
|
753
|
+
|
|
754
|
+
Source can be:
|
|
755
|
+
Local path: dojo add ./path/to/dojo
|
|
756
|
+
npm package: dojo add @dojocho/effect-ts
|
|
757
|
+
Registry: dojo add effect-ts
|
|
758
|
+
URL: dojo add https://example.com/dojo.tgz
|
|
759
|
+
|
|
760
|
+
Flags:
|
|
761
|
+
--force Overwrite existing dojo`);
|
|
762
|
+
}
|
|
763
|
+
const sourceType = classifySource(source);
|
|
764
|
+
switch (sourceType) {
|
|
765
|
+
case "local":
|
|
766
|
+
addLocal(root2, source, force);
|
|
767
|
+
break;
|
|
768
|
+
case "npm":
|
|
769
|
+
addNpm(root2, source, force);
|
|
770
|
+
break;
|
|
771
|
+
case "url":
|
|
772
|
+
addUrl(root2, source, force);
|
|
773
|
+
break;
|
|
774
|
+
case "registry":
|
|
775
|
+
await addFromRegistry(root2, source, force);
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function classifySource(source) {
|
|
780
|
+
if (source.startsWith(".") || source.startsWith("/")) return "local";
|
|
781
|
+
if (source.startsWith("https://") || source.startsWith("http://")) return "url";
|
|
782
|
+
if (source.startsWith("@") || source.includes("/")) return "npm";
|
|
783
|
+
return "registry";
|
|
784
|
+
}
|
|
785
|
+
function safeExtract(tarball, cwd) {
|
|
786
|
+
const listing = execFileSync("tar", ["-tzf", tarball], { cwd, encoding: "utf8" });
|
|
787
|
+
const unsafe = listing.split("\n").some((e) => e.startsWith("/") || e.includes(".."));
|
|
788
|
+
if (unsafe) {
|
|
789
|
+
throw new Error("Refusing to extract: tarball contains unsafe paths (absolute or ../)");
|
|
790
|
+
}
|
|
791
|
+
execFileSync("tar", ["xzf", tarball], { cwd, stdio: "pipe" });
|
|
792
|
+
}
|
|
793
|
+
function handleExisting(root2, name, force) {
|
|
794
|
+
const targetPath = dojoDir(root2, name);
|
|
795
|
+
if (!existsSync5(targetPath)) return;
|
|
796
|
+
if (!force) {
|
|
797
|
+
throw new Error(`Dojo "${name}" already exists at ${DOJOS_DIR}/${name}
|
|
798
|
+
|
|
799
|
+
To update: dojo add ${name} --force
|
|
800
|
+
To remove: dojo remove ${name}`);
|
|
801
|
+
}
|
|
802
|
+
remove(root2, [name]);
|
|
803
|
+
}
|
|
804
|
+
function addLocal(root2, source, force) {
|
|
805
|
+
const sourcePath = resolve4(source);
|
|
806
|
+
if (!existsSync5(sourcePath)) {
|
|
807
|
+
throw new Error(`Source not found: ${sourcePath}`);
|
|
808
|
+
}
|
|
809
|
+
const name = basename(sourcePath);
|
|
810
|
+
handleExisting(root2, name, force);
|
|
811
|
+
const targetPath = dojoDir(root2, name);
|
|
812
|
+
mkdirSync3(resolve4(root2, DOJOS_DIR), { recursive: true });
|
|
813
|
+
cpSync(sourcePath, targetPath, {
|
|
814
|
+
recursive: true,
|
|
815
|
+
filter: (src) => !src.includes("node_modules")
|
|
816
|
+
});
|
|
817
|
+
const pkgPath = resolve4(targetPath, "package.json");
|
|
818
|
+
if (existsSync5(pkgPath)) {
|
|
819
|
+
const linked = linkWorkspaceDeps(root2, sourcePath, pkgPath);
|
|
820
|
+
console.log(`Installing ${name} dependencies...`);
|
|
821
|
+
execSync4("pnpm install --ignore-workspace --silent", {
|
|
822
|
+
cwd: targetPath,
|
|
823
|
+
stdio: "pipe"
|
|
824
|
+
});
|
|
825
|
+
for (const pkgDir of linked) {
|
|
826
|
+
execSync4(`pnpm link ${pkgDir}`, { cwd: targetPath, stdio: "pipe" });
|
|
827
|
+
execSync4(`pnpm link ${pkgDir}`, { cwd: root2, stdio: "pipe" });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
finalize(root2, name, targetPath);
|
|
831
|
+
}
|
|
832
|
+
function installExtracted(root2, extractedDir, source, force) {
|
|
833
|
+
const dojoJsonPath = resolve4(extractedDir, "dojo.json");
|
|
834
|
+
if (!existsSync5(dojoJsonPath)) {
|
|
835
|
+
throw new Error(`${source} is not a dojo \u2014 missing dojo.json`);
|
|
836
|
+
}
|
|
837
|
+
const manifest = parseManifest(readFileSync2(dojoJsonPath, "utf8"), dojoJsonPath);
|
|
838
|
+
const name = manifest.name.includes("/") ? manifest.name.split("/").pop() : manifest.name;
|
|
839
|
+
handleExisting(root2, name, force);
|
|
840
|
+
const targetPath = dojoDir(root2, name);
|
|
841
|
+
mkdirSync3(resolve4(root2, DOJOS_DIR), { recursive: true });
|
|
842
|
+
cpSync(extractedDir, targetPath, { recursive: true });
|
|
843
|
+
const pkgPath = resolve4(targetPath, "package.json");
|
|
844
|
+
if (existsSync5(pkgPath)) {
|
|
845
|
+
console.log(`Installing ${name} dependencies...`);
|
|
846
|
+
execSync4("pnpm install --ignore-workspace --silent", { cwd: targetPath, stdio: "pipe" });
|
|
847
|
+
}
|
|
848
|
+
finalize(root2, name, targetPath);
|
|
849
|
+
}
|
|
850
|
+
function addNpm(root2, source, force) {
|
|
851
|
+
const tmpDir = resolve4(tmpdir(), `dojocho-${Date.now()}`);
|
|
852
|
+
mkdirSync3(tmpDir, { recursive: true });
|
|
853
|
+
try {
|
|
854
|
+
console.log(`Fetching ${source}...`);
|
|
855
|
+
execSync4(`npm pack ${source} --pack-destination .`, { cwd: tmpDir, stdio: "pipe" });
|
|
856
|
+
const tarballs = readdirSync2(tmpDir).filter((f) => f.endsWith(".tgz"));
|
|
857
|
+
if (tarballs.length === 0) throw new Error(`Failed to download ${source}`);
|
|
858
|
+
safeExtract(tarballs[0], tmpDir);
|
|
859
|
+
installExtracted(root2, resolve4(tmpDir, "package"), source, force);
|
|
860
|
+
} finally {
|
|
861
|
+
rmSync2(tmpDir, { recursive: true, force: true });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function validateRegistryItem(data) {
|
|
865
|
+
if (typeof data !== "object" || data === null) {
|
|
866
|
+
throw new Error("Invalid registry response: expected a JSON object");
|
|
867
|
+
}
|
|
868
|
+
const obj = data;
|
|
869
|
+
if (typeof obj.name !== "string" || typeof obj.version !== "string" || typeof obj.description !== "string") {
|
|
870
|
+
throw new Error("Invalid registry item: missing name, version, or description");
|
|
871
|
+
}
|
|
872
|
+
if (typeof obj.source !== "object" || obj.source === null) {
|
|
873
|
+
throw new Error("Invalid registry item: missing source");
|
|
874
|
+
}
|
|
875
|
+
const src = obj.source;
|
|
876
|
+
if (src.type === "npm" && typeof src.package === "string") {
|
|
877
|
+
return obj;
|
|
878
|
+
}
|
|
879
|
+
if (src.type === "tarball" && typeof src.url === "string") {
|
|
880
|
+
return obj;
|
|
881
|
+
}
|
|
882
|
+
throw new Error(`Invalid registry item: source must be npm or tarball`);
|
|
883
|
+
}
|
|
884
|
+
async function addFromRegistry(root2, name, force) {
|
|
885
|
+
const config = loadConfig(root2, { command: "add" });
|
|
886
|
+
for (const [registryName, urlTemplate] of Object.entries(config.registries)) {
|
|
887
|
+
const url = urlTemplate.replace("{name}", name);
|
|
888
|
+
try {
|
|
889
|
+
const res = await fetch(url);
|
|
890
|
+
if (!res.ok) continue;
|
|
891
|
+
const item = validateRegistryItem(await res.json());
|
|
892
|
+
if (item.source.type === "npm") {
|
|
893
|
+
return addNpm(root2, item.source.package, force);
|
|
894
|
+
}
|
|
895
|
+
return addUrl(root2, item.source.url, force);
|
|
896
|
+
} catch (err) {
|
|
897
|
+
if (err instanceof Error && err.message.startsWith("Invalid registry")) throw err;
|
|
898
|
+
console.log(`Registry "${registryName}" unreachable: ${url}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
throw new Error(`"${name}" not found in any registry.
|
|
902
|
+
|
|
903
|
+
Try:
|
|
904
|
+
npm package: dojo add @dojocho/${name}
|
|
905
|
+
Local path: dojo add ./path/to/${name}`);
|
|
906
|
+
}
|
|
907
|
+
function addUrl(root2, url, force) {
|
|
908
|
+
const tmpDir = resolve4(tmpdir(), `dojocho-${Date.now()}`);
|
|
909
|
+
mkdirSync3(tmpDir, { recursive: true });
|
|
910
|
+
try {
|
|
911
|
+
console.log(`Fetching ${url}...`);
|
|
912
|
+
execFileSync("curl", ["-fsSL", "-o", "dojo.tgz", url], { cwd: tmpDir, stdio: "pipe" });
|
|
913
|
+
safeExtract("dojo.tgz", tmpDir);
|
|
914
|
+
const extractedDir = existsSync5(resolve4(tmpDir, "package", "dojo.json")) ? resolve4(tmpDir, "package") : tmpDir;
|
|
915
|
+
installExtracted(root2, extractedDir, url, force);
|
|
916
|
+
} finally {
|
|
917
|
+
rmSync2(tmpDir, { recursive: true, force: true });
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function finalize(root2, name, targetPath) {
|
|
921
|
+
const rc = readDojoRc(root2);
|
|
922
|
+
rc.currentDojo = name;
|
|
923
|
+
writeDojoRc(root2, rc);
|
|
924
|
+
const dojoPkgPath = resolve4(targetPath, "package.json");
|
|
925
|
+
const dojoTsconfigPath = resolve4(targetPath, "tsconfig.json");
|
|
926
|
+
if (existsSync5(dojoPkgPath) && existsSync5(dojoTsconfigPath)) {
|
|
927
|
+
const dojoPkg = JSON.parse(readFileSync2(dojoPkgPath, "utf8"));
|
|
928
|
+
const dojoTsconfig = JSON.parse(readFileSync2(dojoTsconfigPath, "utf8"));
|
|
929
|
+
const deps = Object.keys(dojoPkg.dependencies ?? {});
|
|
930
|
+
if (deps.length > 0) {
|
|
931
|
+
dojoTsconfig.compilerOptions ??= {};
|
|
932
|
+
const paths = dojoTsconfig.compilerOptions.paths ?? {};
|
|
933
|
+
for (const dep of deps) {
|
|
934
|
+
paths[dep] = [`./node_modules/${dep}`];
|
|
935
|
+
paths[`${dep}/*`] = [`./node_modules/${dep}/*`];
|
|
936
|
+
}
|
|
937
|
+
dojoTsconfig.compilerOptions.paths = paths;
|
|
938
|
+
writeFileSync2(dojoTsconfigPath, JSON.stringify(dojoTsconfig, null, 2) + "\n");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const dojo = loadConfig(root2);
|
|
942
|
+
const katasInclude = `${relative4(root2, dojo.katasPath)}/**/*.ts`;
|
|
943
|
+
const tsconfigPath = resolve4(root2, "tsconfig.json");
|
|
944
|
+
const extendsPath = `./${relative4(root2, resolve4(targetPath, "tsconfig.json"))}`;
|
|
945
|
+
writeFileSync2(
|
|
946
|
+
tsconfigPath,
|
|
947
|
+
JSON.stringify(
|
|
948
|
+
{
|
|
949
|
+
extends: extendsPath,
|
|
950
|
+
compilerOptions: { noEmit: true },
|
|
951
|
+
include: [katasInclude]
|
|
952
|
+
},
|
|
953
|
+
null,
|
|
954
|
+
2
|
|
955
|
+
) + "\n"
|
|
956
|
+
);
|
|
957
|
+
symlinkDojo(root2, targetPath);
|
|
958
|
+
console.log(`Dojo "${name}" added.
|
|
959
|
+
|
|
960
|
+
Location: ${DOJOS_DIR}/${name}
|
|
961
|
+
Active: ${name}
|
|
962
|
+
Command: /kata`);
|
|
963
|
+
}
|
|
964
|
+
function linkWorkspaceDeps(root2, sourcePath, pkgPath) {
|
|
965
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
|
|
966
|
+
const linked = [];
|
|
967
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
968
|
+
if (!pkg[field]) continue;
|
|
969
|
+
for (const [name, version] of Object.entries(pkg[field])) {
|
|
970
|
+
if (typeof version !== "string" || !version.startsWith("workspace:")) continue;
|
|
971
|
+
const srcPkg = resolve4(sourcePath, "node_modules", ...name.split("/"));
|
|
972
|
+
if (existsSync5(srcPkg)) {
|
|
973
|
+
linked.push(realpathSync(srcPkg));
|
|
974
|
+
}
|
|
975
|
+
delete pkg[field][name];
|
|
976
|
+
}
|
|
977
|
+
if (Object.keys(pkg[field]).length === 0) delete pkg[field];
|
|
978
|
+
}
|
|
979
|
+
writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
980
|
+
const rootPkgPath = resolve4(root2, "package.json");
|
|
981
|
+
if (!existsSync5(rootPkgPath)) {
|
|
982
|
+
writeFileSync2(rootPkgPath, JSON.stringify({ type: "module", private: true }, null, 2) + "\n");
|
|
983
|
+
}
|
|
984
|
+
return linked;
|
|
985
|
+
}
|
|
986
|
+
function symlinkDir(sourceDir, targetDir, filter) {
|
|
987
|
+
if (!existsSync5(sourceDir)) return;
|
|
988
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
989
|
+
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
990
|
+
if (!filter(entry)) continue;
|
|
991
|
+
const link = resolve4(targetDir, entry.name);
|
|
992
|
+
if (existsSync5(link)) unlinkSync3(link);
|
|
993
|
+
symlinkSync(relative4(targetDir, resolve4(sourceDir, entry.name)), link);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function symlinkDojo(root2, dojoPath) {
|
|
997
|
+
for (const dir of [".claude", ".opencode", ".codex"]) {
|
|
998
|
+
symlinkDir(resolve4(dojoPath, "commands"), resolve4(root2, dir, "commands"), (e) => e.name.endsWith(".md"));
|
|
999
|
+
symlinkDir(resolve4(dojoPath, "skills"), resolve4(root2, dir, "skills"), (e) => e.isDirectory());
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/index.ts
|
|
1004
|
+
var [command, ...args] = process.argv.slice(2);
|
|
1005
|
+
async function main() {
|
|
1006
|
+
if (command === "kata") {
|
|
1007
|
+
kata(findProjectRoot(), args);
|
|
1008
|
+
} else if (command === "add") {
|
|
1009
|
+
await add(process.cwd(), args);
|
|
1010
|
+
} else if (command === "remove") {
|
|
1011
|
+
remove(findProjectRoot(), args);
|
|
1012
|
+
} else {
|
|
1013
|
+
root(process.cwd(), [command, ...args].filter(Boolean));
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
main().catch((err) => {
|
|
1017
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1018
|
+
console.error(message);
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dojocho/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": { "access": "public" },
|
|
6
|
+
"files": ["dist"],
|
|
7
|
+
"bin": {
|
|
8
|
+
"dojo": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@dojocho/config": "workspace:*"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"tsup": "^8.0.0",
|
|
18
|
+
"typescript": "^5.7.0"
|
|
19
|
+
}
|
|
20
|
+
}
|