dojocho 1.0.0 → 1.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +127 -66
  3. package/package.json +8 -9
package/README.md CHANGED
@@ -17,7 +17,7 @@ After install, the CLI is available as `dojo`.
17
17
  ## Usage
18
18
 
19
19
  ```sh
20
- dojo setup --claude # set up your dojo (creates dojo.config.ts, wires up your agent)
20
+ dojo setup # set up your dojo (auto-detects your agent via env vars)
21
21
  dojo add effect-ts # install a training pack (a "dojo")
22
22
  claude /kata # start practicing
23
23
  ```
package/dist/index.js CHANGED
@@ -792,9 +792,9 @@ function markDone(root2) {
792
792
  }
793
793
 
794
794
  // src/commands/setup.ts
795
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, lstatSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
795
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, lstatSync, unlinkSync, writeFileSync as writeFileSync3, symlinkSync } from "fs";
796
796
  import { execSync as execSync3 } from "child_process";
797
- import { resolve as resolve5 } from "path";
797
+ import { resolve as resolve5, relative as relative4 } from "path";
798
798
 
799
799
  // src/pm.ts
800
800
  import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
@@ -837,26 +837,26 @@ function pmCommands(root2) {
837
837
  case "pnpm":
838
838
  return {
839
839
  name: pm,
840
- installSilent: "pnpm install --ignore-workspace --silent",
840
+ installSilent: "pnpm install --ignore-workspace --reporter=append-only",
841
841
  add: (pkg) => `pnpm add ${pkg}`
842
842
  };
843
843
  case "yarn":
844
844
  return {
845
845
  name: pm,
846
- installSilent: "yarn install --silent",
846
+ installSilent: "yarn install",
847
847
  add: (pkg) => `yarn add ${pkg}`
848
848
  };
849
849
  case "bun":
850
850
  return {
851
851
  name: pm,
852
- installSilent: "bun install --silent",
852
+ installSilent: "bun install",
853
853
  add: (pkg) => `bun add ${pkg}`
854
854
  };
855
855
  case "npm":
856
856
  default:
857
857
  return {
858
858
  name: pm,
859
- installSilent: "npm install --silent",
859
+ installSilent: "npm install --no-progress",
860
860
  add: (pkg) => `npm install ${pkg}`
861
861
  };
862
862
  }
@@ -864,11 +864,18 @@ function pmCommands(root2) {
864
864
 
865
865
  // src/commands/setup.ts
866
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 }
867
+ claude: { dir: ".claude", commandsDir: "commands", hasSettings: true, envVars: ["CLAUDECODE"] },
868
+ opencode: { dir: ".opencode", commandsDir: "commands", hasSettings: false, envVars: ["OPENCODE"] },
869
+ codex: { dir: ".codex", commandsDir: "commands", hasSettings: false, envVars: ["CODEX_THREAD_ID"] },
870
+ gemini: { dir: ".gemini", commandsDir: "commands", hasSettings: false, envVars: ["GEMINI_CLI"] },
871
+ pi: { dir: ".pi", commandsDir: "prompts", hasSettings: false, envVars: ["PI_CODING_AGENT"] }
871
872
  };
873
+ var AGENTS_COMMANDS_DIR = ".agents/commands";
874
+ function detectAgentsFromEnv() {
875
+ return Object.keys(AGENTS).filter(
876
+ (a) => AGENTS[a].envVars.some((v) => Boolean(process.env[v]))
877
+ );
878
+ }
872
879
  var CLAUDE_SETTINGS = {
873
880
  permissions: {
874
881
  allow: [
@@ -886,28 +893,6 @@ var DEFAULT_DOJO_MD = `!\`dojo $ARGUMENTS\`
886
893
 
887
894
  Follow any \`<dojo:prompt>\` instructions in the output.
888
895
  `;
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
896
  var DEFAULT_KATA_MD_CLAUDE = `!\`dojo status\`
912
897
 
913
898
  ## Identity
@@ -973,14 +958,16 @@ function setup(root2, args2) {
973
958
  const explicit = Object.keys(AGENTS).filter(
974
959
  (a) => args2.includes(`--${a}`)
975
960
  );
976
- if (explicit.length === 0) {
961
+ const detected = explicit.length > 0 ? explicit : detectAgentsFromEnv();
962
+ if (detected.length === 0) {
977
963
  promptAgents();
978
964
  return;
979
965
  }
980
966
  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.
967
+ setupAgents(root2, detected);
968
+ const kataCmd = detected.length === 1 ? `${detected[0]} "/kata"` : "/kata in your agent prompt";
969
+ const suffix = explicit.length === 0 ? ` (detected from env: ${detected.join(", ")})` : "";
970
+ console.log(`Dojo ready${suffix}.
984
971
 
985
972
  Add a dojo with: ${CLI} add <source>
986
973
  Then use: ${kataCmd}`);
@@ -990,7 +977,8 @@ function promptAgents() {
990
977
  `- "Claude Code" \u2192 --claude`,
991
978
  `- "OpenCode" \u2192 --opencode`,
992
979
  `- "Codex" \u2192 --codex`,
993
- `- "Gemini CLI" \u2192 --gemini`
980
+ `- "Gemini CLI" \u2192 --gemini`,
981
+ `- "Pi" \u2192 --pi`
994
982
  ].join("\n");
995
983
  console.log(prompt(`${invokeAsk("multiSelect")} to ask the student:
996
984
  Which coding agents do you use?
@@ -1041,32 +1029,46 @@ function scaffold2(root2) {
1041
1029
  }
1042
1030
  const pm = pmCommands(root2);
1043
1031
  console.log("Installing @dojocho/config...");
1044
- execSync3(pm.add("@dojocho/config"), { cwd: root2, stdio: "pipe" });
1032
+ try {
1033
+ execSync3(pm.add("@dojocho/config"), { cwd: root2, stdio: "pipe" });
1034
+ } catch (err) {
1035
+ const msg = err instanceof Error ? err.message : String(err);
1036
+ const firstLine = msg.split("\n").find((l) => l.includes("ERR_") || l.includes("error")) ?? msg.split("\n")[0];
1037
+ console.warn(`! Could not install @dojocho/config: ${firstLine.trim()}`);
1038
+ console.warn(` Continuing setup. Install it manually later (e.g. when authoring katas):`);
1039
+ console.warn(` ${pm.add("@dojocho/config")}`);
1040
+ }
1041
+ }
1042
+ function symlinkCanonical(root2, agent, name) {
1043
+ const cfg = AGENTS[agent];
1044
+ const targetDir = resolve5(root2, cfg.dir, cfg.commandsDir);
1045
+ const target = resolve5(targetDir, `${name}.md`);
1046
+ const canonical = resolve5(root2, AGENTS_COMMANDS_DIR, `${name}.md`);
1047
+ try {
1048
+ lstatSync(target);
1049
+ unlinkSync(target);
1050
+ } catch {
1051
+ }
1052
+ symlinkSync(relative4(targetDir, canonical), target);
1053
+ }
1054
+ function writeCanonicalCommands(root2) {
1055
+ const canonicalDir = resolve5(root2, AGENTS_COMMANDS_DIR);
1056
+ mkdirSync3(canonicalDir, { recursive: true });
1057
+ const canonicalDojo = resolve5(canonicalDir, "dojo.md");
1058
+ if (!existsSync6(canonicalDojo)) writeFileSync3(canonicalDojo, DEFAULT_DOJO_MD);
1059
+ const canonicalKata = resolve5(canonicalDir, "kata.md");
1060
+ if (!existsSync6(canonicalKata)) writeFileSync3(canonicalKata, DEFAULT_KATA_MD_CLAUDE);
1045
1061
  }
1046
1062
  function setupAgents(root2, agents) {
1063
+ writeCanonicalCommands(root2);
1047
1064
  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");
1065
+ const cfg = AGENTS[agent];
1066
+ mkdirSync3(resolve5(root2, cfg.dir, cfg.commandsDir), { recursive: true });
1067
+ mkdirSync3(resolve5(root2, cfg.dir, "skills"), { recursive: true });
1068
+ symlinkCanonical(root2, agent, "dojo");
1069
+ symlinkCanonical(root2, agent, "kata");
1070
+ if (cfg.hasSettings) {
1071
+ const settingsPath = resolve5(root2, cfg.dir, "settings.json");
1070
1072
  writeFileSync3(settingsPath, JSON.stringify(CLAUDE_SETTINGS, null, 2) + "\n");
1071
1073
  }
1072
1074
  }
@@ -1078,9 +1080,9 @@ function configuredAgents(root2) {
1078
1080
  }
1079
1081
 
1080
1082
  // 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";
1083
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, cpSync, renameSync, unlinkSync as unlinkSync3, symlinkSync as symlinkSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
1082
1084
  import { execSync as execSync4, execFileSync } from "child_process";
1083
- import { resolve as resolve7, relative as relative4 } from "path";
1085
+ import { resolve as resolve7, relative as relative5 } from "path";
1084
1086
  import { tmpdir } from "os";
1085
1087
 
1086
1088
  // src/commands/remove.ts
@@ -1218,7 +1220,15 @@ function installExtracted(root2, extractedDir, source, force) {
1218
1220
  const pkgPath = resolve7(extractedDir, "package.json");
1219
1221
  if (existsSync8(pkgPath)) {
1220
1222
  console.log(`Installing ${name} dependencies...`);
1221
- execSync4(pm.installSilent, { cwd: extractedDir, stdio: "pipe" });
1223
+ try {
1224
+ execSync4(pm.installSilent, { cwd: extractedDir, stdio: "pipe" });
1225
+ } catch (err) {
1226
+ const e = err;
1227
+ const detail = e.stderr?.toString().trim() || e.stdout?.toString().trim() || e.message || "unknown error";
1228
+ throw new Error(`Failed to install ${name} dependencies via ${pm.name}:
1229
+
1230
+ ${detail}`);
1231
+ }
1222
1232
  }
1223
1233
  handleExisting(root2, name, force);
1224
1234
  const targetPath = dojoDir(root2, name);
@@ -1318,9 +1328,9 @@ function finalize(root2, name, targetPath) {
1318
1328
  }
1319
1329
  }
1320
1330
  const dojo = loadConfig(root2);
1321
- const katasInclude = `${relative4(root2, dojo.katasPath)}/**/*.ts`;
1331
+ const katasInclude = `${relative5(root2, dojo.katasPath)}/**/*.ts`;
1322
1332
  const tsconfigPath = resolve7(root2, "tsconfig.json");
1323
- const extendsPath = `./${relative4(root2, resolve7(targetPath, "tsconfig.json"))}`;
1333
+ const extendsPath = `./${relative5(root2, resolve7(targetPath, "tsconfig.json"))}`;
1324
1334
  writeFileSync4(
1325
1335
  tsconfigPath,
1326
1336
  JSON.stringify(
@@ -1350,7 +1360,7 @@ function symlinkDir(sourceDir, targetDir, filter) {
1350
1360
  if (!filter(entry)) continue;
1351
1361
  const link = resolve7(targetDir, entry.name);
1352
1362
  if (existsSync8(link)) unlinkSync3(link);
1353
- symlinkSync(relative4(targetDir, resolve7(sourceDir, entry.name)), link);
1363
+ symlinkSync2(relative5(targetDir, resolve7(sourceDir, entry.name)), link);
1354
1364
  }
1355
1365
  }
1356
1366
  function runLifecycleScript(root2, dojoPath, script) {
@@ -1438,6 +1448,55 @@ function status2(root2, _args) {
1438
1448
  }));
1439
1449
  }
1440
1450
 
1451
+ // src/commands/ui.ts
1452
+ import { existsSync as existsSync9 } from "fs";
1453
+ import { spawn } from "child_process";
1454
+ import { dirname as dirname4, join } from "path";
1455
+ import { fileURLToPath } from "url";
1456
+ function ui(_cwd, args2) {
1457
+ const port = process.env.DOJO_UI_PORT ?? "4567";
1458
+ const uiDir = resolveUiDir();
1459
+ if (!uiDir) {
1460
+ console.error("Could not locate apps/ui/. The dojo UI currently requires running");
1461
+ console.error("`dojo` from inside (or near) the dojocho monorepo.");
1462
+ process.exit(1);
1463
+ }
1464
+ console.error(`\u2192 starting dojo UI from ${uiDir}`);
1465
+ console.error(` http://localhost:${port}
1466
+ `);
1467
+ const env = { ...process.env, PORT: port };
1468
+ const child = spawn("pnpm", ["dev", ...args2], {
1469
+ cwd: uiDir,
1470
+ stdio: "inherit",
1471
+ env
1472
+ });
1473
+ child.on("error", (err) => {
1474
+ if (err.code === "ENOENT") {
1475
+ console.error("Could not find `pnpm` on PATH.");
1476
+ console.error("Install pnpm: https://pnpm.io/installation");
1477
+ process.exit(127);
1478
+ }
1479
+ console.error(`Failed to launch UI: ${err.message}`);
1480
+ process.exit(1);
1481
+ });
1482
+ child.on("exit", (code, signal) => {
1483
+ if (signal) process.kill(process.pid, signal);
1484
+ else process.exit(code ?? 0);
1485
+ });
1486
+ }
1487
+ function resolveUiDir() {
1488
+ const here = fileURLToPath(import.meta.url);
1489
+ let dir = dirname4(here);
1490
+ for (let i = 0; i < 10; i++) {
1491
+ const candidate = join(dir, "apps", "ui");
1492
+ if (existsSync9(join(candidate, "package.json"))) return candidate;
1493
+ const parent = dirname4(dir);
1494
+ if (parent === dir) break;
1495
+ dir = parent;
1496
+ }
1497
+ return null;
1498
+ }
1499
+
1441
1500
  // src/index.ts
1442
1501
  var [command, ...args] = process.argv.slice(2);
1443
1502
  async function main() {
@@ -1453,6 +1512,8 @@ async function main() {
1453
1512
  remove(findProjectRoot(), args);
1454
1513
  } else if (command === "status") {
1455
1514
  status2(findProjectRoot(), args);
1515
+ } else if (command === "ui") {
1516
+ ui(process.cwd(), args);
1456
1517
  } else {
1457
1518
  root(process.cwd(), [command, ...args].filter(Boolean));
1458
1519
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dojocho",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Installable coding dojos that turn your AI agent into a sensei.",
5
5
  "keywords": [
6
6
  "cli",
@@ -35,18 +35,17 @@
35
35
  "bin": {
36
36
  "dojo": "./dist/index.js"
37
37
  },
38
- "scripts": {
39
- "build": "tsup",
40
- "test": "vitest run",
41
- "test:watch": "vitest",
42
- "prepublishOnly": "pnpm build"
43
- },
44
38
  "dependencies": {
45
- "@dojocho/config": "workspace:*"
39
+ "@dojocho/config": "0.0.3"
46
40
  },
47
41
  "devDependencies": {
48
42
  "tsup": "^8.0.0",
49
43
  "typescript": "^5.7.0",
50
44
  "vitest": "^4.0.18"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup",
48
+ "test": "vitest run",
49
+ "test:watch": "vitest"
51
50
  }
52
- }
51
+ }