@ionivetech/mugiwara 0.5.3 → 0.5.5

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 (66) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +2 -2
  4. package/.cursor-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +2 -2
  6. package/.opencode/plugins/mugiwara.mjs +2 -1
  7. package/AGENTS.md +1 -1
  8. package/README.md +155 -263
  9. package/content/agents/zoro-execution.md +1 -2
  10. package/content/skills/using-mugiwara/SKILL.md +1 -1
  11. package/dist/mugiwara.js +113 -60
  12. package/docs/{modes.md → concepts/modes.md} +1 -2
  13. package/docs/{skills.md → concepts/skills.md} +1 -1
  14. package/docs/getting-started.md +8 -8
  15. package/docs/index.md +22 -22
  16. package/docs/{install-antigravity.md → install/antigravity.md} +1 -1
  17. package/docs/{install-claude.md → install/claude.md} +4 -2
  18. package/docs/{install-cli.md → install/cli.md} +19 -7
  19. package/docs/{install-codex.md → install/codex.md} +1 -1
  20. package/docs/{install-copilot.md → install/copilot.md} +1 -1
  21. package/docs/{install-cursor.md → install/cursor.md} +1 -1
  22. package/docs/{install-gemini.md → install/gemini.md} +1 -1
  23. package/docs/{install.md → install/index.md} +11 -11
  24. package/docs/{install-kimi.md → install/kimi.md} +1 -1
  25. package/docs/{install-opencode.md → install/opencode.md} +20 -6
  26. package/docs/{install-pi.md → install/pi.md} +1 -1
  27. package/docs/{adoption-guide.md → reference/adoption-guide.md} +2 -2
  28. package/docs/{harness-matrix.md → reference/harness-matrix.md} +2 -2
  29. package/gemini-extension.json +1 -1
  30. package/package.json +2 -2
  31. package/plugin.json +2 -2
  32. package/scripts/evidence.sh +1 -1
  33. package/scripts/mission-report.sh +60 -73
  34. package/scripts/retrieval-eval.ts +1 -2
  35. package/scripts/run-evals.ts +1 -1
  36. package/scripts/savepoint.sh +1 -1
  37. package/scripts/validate-content.ts +7 -7
  38. package/src/args.ts +1 -1
  39. package/src/cli.ts +49 -22
  40. package/src/frontmatter.ts +3 -3
  41. package/src/installer.ts +10 -8
  42. package/src/targets/opencode.ts +22 -1
  43. package/docs/claude-setup.md +0 -43
  44. package/docs/codex-setup.md +0 -26
  45. package/docs/copilot-setup.md +0 -29
  46. package/docs/cursor-setup.md +0 -25
  47. package/docs/gemini-setup.md +0 -27
  48. package/docs/opencode-setup.md +0 -54
  49. package/docs/rule-based-setup.md +0 -31
  50. package/docs/windsurf-setup.md +0 -18
  51. /package/.opencode/{plugins/mugiwara-helpers.mjs → mugiwara-helpers.mjs} +0 -0
  52. /package/docs/{agents.md → concepts/agents.md} +0 -0
  53. /package/docs/{audit-trail.md → concepts/audit-trail.md} +0 -0
  54. /package/docs/{comparison.md → concepts/comparison.md} +0 -0
  55. /package/docs/{config.md → concepts/config.md} +0 -0
  56. /package/docs/{cost.md → concepts/cost.md} +0 -0
  57. /package/docs/{execution-model.md → concepts/execution-model.md} +0 -0
  58. /package/docs/{git-strategy.md → concepts/git-strategy.md} +0 -0
  59. /package/docs/{lanes.md → concepts/lanes.md} +0 -0
  60. /package/docs/{pr-summary.md → concepts/pr-summary.md} +0 -0
  61. /package/docs/{workflow.md → concepts/workflow.md} +0 -0
  62. /package/docs/{agent-anatomy.md → reference/agent-anatomy.md} +0 -0
  63. /package/docs/{compliance-matrix.md → reference/compliance-matrix.md} +0 -0
  64. /package/docs/{developer-onboarding.md → reference/developer-onboarding.md} +0 -0
  65. /package/docs/{enforcement.md → reference/enforcement.md} +0 -0
  66. /package/docs/{skill-anatomy.md → reference/skill-anatomy.md} +0 -0
@@ -42,4 +42,4 @@ Reinstall:
42
42
  ## Configuration
43
43
 
44
44
  After install, configure mugiwara in `.mugiwara/config` (project) or
45
- `~/.mugiwara/config` (global). See [docs/config.md](config.md).
45
+ `~/.mugiwara/config` (global). See [config.md](../concepts/config.md).
@@ -29,7 +29,7 @@ Restart OpenCode.
29
29
 
30
30
  ## How it works
31
31
 
32
- The plugin (`mugiwara.mjs`) does three things:
32
+ The plugin (`mugiwara.mjs`) does four things:
33
33
 
34
34
  1. **Config hook** — registers `content/skills/` as a skills path and
35
35
  `content/agents/` as agents so OpenCode discovers the full crew
@@ -38,12 +38,27 @@ The plugin (`mugiwara.mjs`) does three things:
38
38
  2. **System transform hook** — injects the crew announce header and active
39
39
  autonomy mode into every session's system prompt.
40
40
 
41
- 3. **Chat message hook** — intercepts `/mugiwara-mode` commands and
41
+ 3. **Chat message hook** — intercepts `/mugiwara` commands and
42
42
  natural-language mode switches, writing `.mugiwara/config`.
43
43
 
44
+ 4. **Commands** — the file-based installer copies slash commands
45
+ (`/mugiwara`, `/using-mugiwara`, `/mugiwara-plan`, etc.) to
46
+ `.opencode/commands/`.
47
+
44
48
  Skills are loaded on-demand via OpenCode's native `skill` tool. Agents appear
45
49
  as subagents with full config (color, temperature, permissions, step limits).
46
50
 
51
+ ## Mode switching
52
+
53
+ Switch autonomy mode at runtime:
54
+
55
+ ```
56
+ /mugiwara guided # human decides every GO
57
+ /mugiwara semi # auto branch + commit, plan needs GO
58
+ /mugiwara auto # hands-off except high-risk
59
+ /mugiwara # show current mode
60
+ ```
61
+
47
62
  ## Verify
48
63
 
49
64
  Ask your agent:
@@ -77,9 +92,10 @@ then restart OpenCode.
77
92
 
78
93
  ### Server error on startup
79
94
 
80
- Fixed in v0.5.3. Ensure you're on the latest:
95
+ Clear stale plugin cache and reinstall:
81
96
 
82
97
  ```bash
98
+ rm -rf ~/.cache/opencode/packages/@ionivetech
83
99
  npm update @ionivetech/mugiwara
84
100
  ```
85
101
 
@@ -124,6 +140,4 @@ Mugiwara skills reference general actions. On OpenCode these map to:
124
140
  ## Configuration
125
141
 
126
142
  After install, configure mugiwara in `.mugiwara/config` (project) or
127
- `~/.mugiwara/config` (global). See [docs/config.md](config.md).
128
-
129
- Switch autonomy mode at runtime: `/mugiwara-mode guided|semi|auto`
143
+ `~/.mugiwara/config` (global). See [config.md](../concepts/config.md).
@@ -43,4 +43,4 @@ pi remove mugiwara
43
43
  ## Configuration
44
44
 
45
45
  After install, configure mugiwara in `.mugiwara/config` (project) or
46
- `~/.mugiwara/config` (global). See [docs/config.md](config.md).
46
+ `~/.mugiwara/config` (global). See [config.md](../concepts/config.md).
@@ -56,7 +56,7 @@ mid-wave.
56
56
 
57
57
  The crew runs **inline in your main conversation**; subagents are used only for
58
58
  independent `[PARALLEL]` task batches and background checks. You see every wave
59
- as it happens. See [execution-model.md](execution-model.md).
59
+ as it happens. See [execution-model.md](../concepts/execution-model.md).
60
60
 
61
61
  ## What the crew will never do
62
62
 
@@ -69,4 +69,4 @@ as it happens. See [execution-model.md](execution-model.md).
69
69
  ## Going further
70
70
 
71
71
  - [Developer onboarding](developer-onboarding.md) if you want to contribute.
72
- - [Comparison](comparison.md) if you're weighing mugiwara against alternatives.
72
+ - [Comparison](../concepts/comparison.md) if you're weighing mugiwara against alternatives.
@@ -35,7 +35,7 @@ What actually differs per harness tier. Every skill and agent file ships to ever
35
35
 
36
36
  ## What's the same everywhere
37
37
 
38
- - All 32 (→ 26) skill directories ship to every harness.
39
- - All 15 agent markdown files ship to every harness.
38
+ - All 26 skill directories ship to every harness.
39
+ - All 14 agent markdown files ship to every harness.
40
40
  - `references/` files are always copied.
41
41
  - The workflow, lane sizing, and evidence discipline are identical — the difference is in how the model loads them.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mugiwara",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ionivetech/mugiwara",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, self-healing. Installs into Claude Code, opencode, Copilot, Gemini, Codex, Cursor, Kimi, pi, Windsurf, Cline, Kilo, Antigravity.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,7 +53,7 @@
53
53
  "retrieval-eval": "bun scripts/retrieval-eval.ts",
54
54
  "sync-version": "bun scripts/sync-version.ts",
55
55
  "gate": "bun run typecheck && bun run test && bun run build && bun scripts/validate-content.ts --check-manifest --check-docs && bun run verify-pack && bun scripts/run-evals.ts && bun scripts/retrieval-eval.ts",
56
- "verify-pack": "npm pack --dry-run 2>&1 | grep -q 'ionivetech-mugiwara' && echo 'npm package clean' || (echo 'npm pack failed' && exit 1)",
56
+ "verify-pack": "npm pack --dry-run 2>&1 | node -e \"let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{if(!s.includes('ionivetech-mugiwara')){console.error('npm pack failed');process.exit(1)};console.log('npm package clean')})\"",
57
57
  "prepack": "bun run build && bun run sync-version"
58
58
  },
59
59
  "keywords": [
package/plugin.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "mugiwara",
3
3
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
4
- "version": "0.5.3",
4
+ "version": "0.5.5",
5
5
  "author": {
6
- "name": "ionive"
6
+ "name": "ionivetech"
7
7
  },
8
8
  "license": "MIT",
9
9
  "repository": "https://github.com/ionivetech/mugiwara",
@@ -15,7 +15,7 @@ RESULTS_DIR="$MUGIWARA_DIR/results"
15
15
  mkdir -p "$RESULTS_DIR"
16
16
 
17
17
  TIMESTAMP=$(date +%Y%m%d-%H%M%S)
18
- HASH=$(echo "${LABEL}-${TIMESTAMP}-$$-${RANDOM}" | shasum -a 256 | cut -c1-12 2>/dev/null || echo "${TIMESTAMP}")
18
+ HASH=$(echo "${LABEL}-${TIMESTAMP}-$$-${RANDOM}" | (sha256sum 2>/dev/null || shasum -a 256 2>/dev/null || openssl sha256) | cut -c1-12 2>/dev/null || echo "${TIMESTAMP}")
19
19
  EVIDENCE_FILE="$RESULTS_DIR/${LABEL}-${HASH}.log"
20
20
 
21
21
  {
@@ -13,76 +13,63 @@ REPORT_FILE="$REPORT_DIR/${MISSION}.md"
13
13
 
14
14
  [ -f "$STATE_FILE" ] || { echo "mission-report: $STATE_FILE not found" >&2; exit 1; }
15
15
 
16
- python3 <<PY
17
- import json, os, sys
18
- from datetime import datetime
19
-
20
- state_file = "$STATE_FILE"
21
- report_file = "$REPORT_FILE"
22
- os.makedirs(os.path.dirname(report_file), exist_ok=True)
23
-
24
- with open(state_file) as f:
25
- s = json.load(f)
26
-
27
- mission = s.get("mission", "$MISSION")
28
- actor = s.get("actor", "unknown")
29
- branch = s.get("branch", "unknown")
30
- lane = s.get("lane", "unknown")
31
- lane_reason = s.get("lane_reason", "")
32
- mode = s.get("mode", "guided")
33
- wave = s.get("wave", 0)
34
- files_touched = s.get("files_touched", 0)
35
- loc_delta = s.get("loc_delta", 0)
36
- sensitive = s.get("sensitive_paths", [])
37
- tasks = s.get("tasks", {})
38
- blockers = s.get("blockers_open", 0)
39
- heal_cycle = s.get("heal_cycle", 1)
40
- tokens = s.get("tokens_est", 0)
41
- budget = s.get("budget", 0)
42
- evidence = s.get("evidence", [])
43
- updated = s.get("updated_at", datetime.utcnow().isoformat() + "Z")
44
-
45
- now = datetime.utcnow().strftime("%Y-%m-%d")
46
- report = f"""# Mission: {mission} · {now}
47
-
48
- **Lane** {lane} · **Mode** {mode} · **Actor** {actor} · **Branch** {branch}
49
-
50
- ## What changed
51
-
52
- {files_touched} files, +{loc_delta if loc_delta >= 0 else loc_delta} LOC"""
53
-
54
- if sensitive:
55
- report += "\nSensitive paths: " + ", ".join(sensitive)
56
-
57
- report += f"""
58
-
59
- ## Gates
60
-
61
- | Gate | Verdict | Evidence |
62
- |------|---------|----------|"""
63
-
64
- for e in evidence:
65
- report += f"\n| | PASS | {e} |"
66
-
67
- report += f"""
68
-
69
- ## State
70
-
71
- | Field | Value |
72
- |-------|-------|
73
- | Wave | {wave} |
74
- | Tasks | {tasks.get('done', 0)}/{tasks.get('total', 0)} done |
75
- | Blockers open | {blockers} |
76
- | Heal cycles | {heal_cycle} |
77
- | Tokens used | {tokens:,} / {budget:,} |
78
-
79
- ## Updated
80
-
81
- {updated}
82
- """
83
-
84
- with open(report_file, 'w') as f:
85
- f.write(report)
86
-
87
- print(f"✓ mission report: {report_file}")
88
- PY
16
+ export STATE_FILE REPORT_DIR MISSION
17
+ node << 'NODE'
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const stateFile = process.env.STATE_FILE || ".mugiwara/state.json";
22
+ const reportDir = process.env.REPORT_DIR || ".mugiwara/reports";
23
+ const mission = process.env.MISSION || "unknown";
24
+
25
+ if (!fs.existsSync(stateFile)) {
26
+ console.error("mission-report: " + stateFile + " not found");
27
+ process.exit(1);
28
+ }
29
+
30
+ fs.mkdirSync(reportDir, { recursive: true });
31
+ const s = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
32
+
33
+ const reportFile = path.join(reportDir, mission + ".md");
34
+ const now = new Date().toISOString().slice(0, 10);
35
+ const lane = s.lane || "unknown";
36
+ const mode = s.mode || "guided";
37
+ const actor = s.actor || "unknown";
38
+ const branch = s.branch || "unknown";
39
+ const wave = s.wave || 0;
40
+ const filesTouched = s.files_touched || 0;
41
+ const locDelta = s.loc_delta || 0;
42
+ const sensitive = s.sensitive_paths || [];
43
+ const tasks = s.tasks || {};
44
+ const blockers = s.blockers_open || 0;
45
+ const healCycle = s.heal_cycle || 1;
46
+ const tokens = s.tokens_est || 0;
47
+ const budget = s.budget || 0;
48
+ const evidence = s.evidence || [];
49
+ const updated = s.updated_at || new Date().toISOString();
50
+
51
+ let report = "# Mission: " + mission + " . " + now + "\n\n";
52
+ report += "**Lane** " + lane + " . **Mode** " + mode + " . **Actor** " + actor + " . **Branch** " + branch + "\n\n";
53
+ report += "## What changed\n\n";
54
+ report += filesTouched + " files, +" + locDelta + " LOC";
55
+
56
+ if (sensitive.length) {
57
+ report += "\nSensitive paths: " + sensitive.join(", ");
58
+ }
59
+
60
+ report += "\n\n## Gates\n\n| Gate | Verdict | Evidence |\n|------|---------|----------|";
61
+ for (const e of evidence) {
62
+ report += "\n| \u2014 | PASS | " + e + " |";
63
+ }
64
+
65
+ report += "\n\n## State\n\n| Field | Value |\n|-------|-------|\n";
66
+ report += "| Wave | " + wave + " |\n";
67
+ report += "| Tasks | " + (tasks.done || 0) + "/" + (tasks.total || 0) + " done |\n";
68
+ report += "| Blockers open | " + blockers + " |\n";
69
+ report += "| Heal cycles | " + healCycle + " |\n";
70
+ report += "| Tokens used | " + tokens.toLocaleString() + " / " + budget.toLocaleString() + " |\n\n";
71
+ report += "## Updated\n\n" + updated + "\n";
72
+
73
+ fs.writeFileSync(reportFile, report);
74
+ console.log("\u2713 mission report: " + reportFile);
75
+ NODE
@@ -3,7 +3,7 @@
3
3
  // Builds TF-IDF index over skill descriptions, scores prompts, reports rank-1 + top_k.
4
4
  // No model needed — pure string matching. Outputs JSON.
5
5
 
6
- import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
6
+ import { readdirSync, readFileSync, existsSync, statSync, writeFileSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { parseFrontmatter } from '../src/frontmatter.ts';
9
9
 
@@ -256,6 +256,5 @@ if (ciArg !== -1) {
256
256
  }
257
257
 
258
258
  function writeJson(path: string, obj: object) {
259
- const { writeFileSync } = require('node:fs');
260
259
  writeFileSync(path, JSON.stringify(obj, null, 2) + '\n');
261
260
  }
@@ -9,6 +9,7 @@
9
9
  // keyword match, then reports pass/fail per case and a rank-1 style score.
10
10
  // The command defaults to "claude -p" — override with MUGIWARA_EVAL_CMD.
11
11
  import { readdirSync, readFileSync, existsSync } from 'node:fs';
12
+ import { execFileSync } from 'node:child_process';
12
13
  import { join } from 'node:path';
13
14
 
14
15
  const root = join(import.meta.dirname, '..');
@@ -136,7 +137,6 @@ async function runCases(env: { execFileSync: typeof import('node:child_process')
136
137
 
137
138
  const runArg = process.argv.indexOf('--run');
138
139
  if (runArg !== -1) {
139
- const { execFileSync } = require('node:child_process');
140
140
  const cmd = process.env.MUGIWARA_EVAL_CMD ?? 'claude -p';
141
141
  const [bin, ...pre] = cmd.split(' ');
142
142
  runCases({ execFileSync, bin, pre });
@@ -100,7 +100,7 @@ EVIDENCE=$(ls "$MUGIWARA_DIR/results/" 2>/dev/null | grep "$MISSION" | sed 's|^|
100
100
  SKILL_VERSION="1"
101
101
  if [ -f package.json ]; then
102
102
  PKG_JSON="$MUGIWARA_DIR/../package.json"
103
- [ -f "$PKG_JSON" ] && SKILL_VERSION=$(python3 -c "import json; v=json.load(open('$PKG_JSON')).get('version','1'); print(v.split('.')[0])" 2>/dev/null || echo "1")
103
+ [ -f "$PKG_JSON" ] && SKILL_VERSION=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$PKG_JSON','utf8')).version.split('.')[0])}catch(e){console.log('1')}" 2>/dev/null || echo "1")
104
104
  fi
105
105
 
106
106
  # tokens from env var (harness exports estimated tokens consumed)
@@ -14,7 +14,7 @@ function checkFile(file: string, wantName: string, kind: 'skill' | 'agent'): Rec
14
14
  const { data, body } = parsed;
15
15
  if (data.name !== wantName) errors.push(`${kind} ${file}: name "${data.name}" != "${wantName}"`);
16
16
  const d = data.description ?? '';
17
- if (kind === 'skill' && (d.length < 20 || d.length > 500)) errors.push(`skill ${file}: description must be 20-500 chars (got ${d.length})`);
17
+ if (kind === 'skill' && (d.length < 20 || d.length > 220)) errors.push(`skill ${file}: description must be 20-220 chars (got ${d.length})`);
18
18
  if (kind === 'agent' && d.length < 20) errors.push(`agent ${file}: description too short`);
19
19
  if (kind === 'skill' && body.replace(/\r?\n$/, '').split(/\r?\n/).length > 120) errors.push(`skill ${file}: body exceeds 120 lines`);
20
20
  if (kind === 'skill' && !body.includes('## Skip when')) errors.push(`skill ${file}: missing required "## Skip when" block (≤4 lines, numeric threshold)`);
@@ -188,20 +188,20 @@ if (totalDescChars > INDEX_BUDGET) {
188
188
  const docsArg = process.argv.indexOf('--check-docs');
189
189
  if (docsArg !== -1) {
190
190
  const docsDir = join(import.meta.dirname, '..', 'docs');
191
- const skillsDoc = join(docsDir, 'skills.md');
192
- const agentsDoc = join(docsDir, 'agents.md');
191
+ const skillsDoc = join(docsDir, 'concepts', 'skills.md');
192
+ const agentsDoc = join(docsDir, 'concepts', 'agents.md');
193
193
  let docErrors = 0;
194
194
 
195
195
  if (existsSync(skillsDoc)) {
196
196
  const content = readFileSync(skillsDoc, 'utf8');
197
197
  for (const dir of skillDirs) {
198
198
  if (!content.includes(dir)) {
199
- errors.push(`docs/skills.md: missing skill "${dir}"`);
199
+ errors.push(`docs/concepts/skills.md: missing skill "${dir}"`);
200
200
  docErrors++;
201
201
  }
202
202
  }
203
203
  } else {
204
- errors.push('docs/skills.md: file not found');
204
+ errors.push('docs/concepts/skills.md: file not found');
205
205
  docErrors++;
206
206
  }
207
207
 
@@ -210,12 +210,12 @@ if (docsArg !== -1) {
210
210
  for (const f of agentFiles) {
211
211
  const name = f.replace(/\.md$/, '');
212
212
  if (!content.includes(name)) {
213
- errors.push(`docs/agents.md: missing agent "${name}"`);
213
+ errors.push(`docs/concepts/agents.md: missing agent "${name}"`);
214
214
  docErrors++;
215
215
  }
216
216
  }
217
217
  } else {
218
- errors.push('docs/agents.md: file not found');
218
+ errors.push('docs/concepts/agents.md: file not found');
219
219
  docErrors++;
220
220
  }
221
221
 
package/src/args.ts CHANGED
@@ -9,7 +9,7 @@ export type Args = {
9
9
  const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target' };
10
10
  const BOOL_FLAGS: Record<string, string> = {
11
11
  '--global': 'global', '--yes': 'yes', '-y': 'yes', '--force': 'force',
12
- '--dry-run': 'dryRun', '--keep-logs': 'keepLogs',
12
+ '--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check',
13
13
  '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
14
14
  };
15
15
 
package/src/cli.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // src/cli.ts
3
- import { existsSync, readFileSync, realpathSync, rmSync } from 'node:fs';
3
+ import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
- import { resolve } from 'node:path';
6
- import { pathToFileURL } from 'node:url';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
7
  import { parseArgs, type FlagValue, type Args } from './args.ts';
8
8
  import { createRl, choose, multiChoose, confirm } from './prompt.ts';
9
9
  import { targets, TARGET_IDS } from './targets/index.ts';
@@ -43,12 +43,15 @@ function resetCmd(flags: Args['flags']): void {
43
43
 
44
44
  async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
45
45
  const interactive = !flag(flags.yes);
46
+ if (interactive && !process.stdin.isTTY) {
47
+ throw new Error('Not a terminal. Run with --yes and --global or --project <dir> --target <ids|all>');
48
+ }
46
49
  const rl = interactive ? createRl() : null;
47
50
  try {
48
51
  let scope: Scope | null = flag(flags.global) ? 'global' : str(flags.project) ? 'project' : null;
49
52
  if (!scope) {
50
- if (!interactive) throw new Error('Specify --global or --project <dir> with --yes');
51
- scope = (await choose(rl!, 'Install scope?', ['global (user-wide)', 'project (this repo)'])) === 0 ? 'global' : 'project';
53
+ if (!interactive) { scope = 'project'; }
54
+ else scope = (await choose(rl!, 'Install scope?', ['global (user-wide)', 'project (this repo)'])) === 0 ? 'global' : 'project';
52
55
  }
53
56
  const projectDir = resolve(str(flags.project) ?? process.cwd());
54
57
  if (scope === 'project' && !existsSync(projectDir)) throw new Error(`Project dir not found: ${projectDir}`);
@@ -56,9 +59,11 @@ async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; pro
56
59
  let targetIds = str(flags.target)?.split(',').map(s => s.trim()) ?? null;
57
60
  if (targetIds && targetIds.includes('all')) targetIds = [...TARGET_IDS];
58
61
  if (!targetIds) {
59
- if (!interactive) throw new Error('Specify --target <ids|all> with --yes');
60
- const idx = await multiChoose(rl!, 'Target AI agents?', ['all targets', ...TARGET_IDS]);
61
- targetIds = idx.includes(0) ? [...TARGET_IDS] : idx.map(i => TARGET_IDS[i - 1]);
62
+ if (!interactive) { targetIds = [...TARGET_IDS]; }
63
+ else {
64
+ const idx = await multiChoose(rl!, 'Target AI agents?', ['all targets', ...TARGET_IDS]);
65
+ targetIds = idx.includes(0) ? [...TARGET_IDS] : idx.map(i => TARGET_IDS[i - 1]);
66
+ }
62
67
  }
63
68
  for (const id of targetIds) {
64
69
  if (!targets[id]) throw new Error(`Unknown target: ${id} (valid: ${TARGET_IDS.join(', ')}, all)`);
@@ -110,7 +115,17 @@ async function uninstall(flags: Args['flags']): Promise<void> {
110
115
  const home = homedir();
111
116
  const file = manifestPath({ scope, projectDir, home });
112
117
  const manifest = readManifest(file);
113
- if (!manifest) { console.log('Nothing installed (no manifest found).'); return; }
118
+ if (!manifest) {
119
+ if (scope === 'project') {
120
+ const globalFile = manifestPath({ scope: 'global', projectDir, home });
121
+ if (existsSync(globalFile)) {
122
+ console.log('No project manifest found, but a global install exists. Try:\n mugiwara uninstall --global');
123
+ return;
124
+ }
125
+ }
126
+ console.log('Nothing installed (no manifest found).');
127
+ return;
128
+ }
114
129
  console.log(`Will remove ${manifest.files.length} files (targets: ${manifest.targets.join(', ')}).`);
115
130
  if (!flag(flags.yes)) {
116
131
  const rl = createRl();
@@ -119,7 +134,20 @@ async function uninstall(flags: Args['flags']): Promise<void> {
119
134
  if (!ok) { console.log('Aborted.'); return; }
120
135
  }
121
136
  const removed = removeInstalled(manifest, { dryRun: flag(flags.dryRun) });
122
- if (!flag(flags.dryRun)) rmSync(file);
137
+ if (!flag(flags.dryRun)) {
138
+ rmSync(file);
139
+ const mugiDir = join(scope === 'global' ? home : projectDir, '.mugiwara');
140
+ if (existsSync(mugiDir) && readdirSync(mugiDir).length === 0) {
141
+ rmSync(mugiDir, { recursive: true, force: true });
142
+ }
143
+ if (manifest.targets.includes('opencode')) {
144
+ const opencodeCache = join(home, '.cache', 'opencode', 'packages', '@ionivetech');
145
+ if (existsSync(opencodeCache)) {
146
+ rmSync(opencodeCache, { recursive: true, force: true });
147
+ console.log(' cleared opencode npm cache (stale plugin versions)');
148
+ }
149
+ }
150
+ }
123
151
  console.log(`OK removed ${removed.length} files`);
124
152
  }
125
153
 
@@ -134,7 +162,12 @@ function list(flags: Args['flags']): void {
134
162
  const m = readManifest(file);
135
163
  if (!m) continue;
136
164
  found = true;
137
- console.log(`${label}: v${m.version} targets=${m.targets.join(',')} files=${m.files.length} installed=${m.installedAt}`);
165
+ if (flag(flags.check)) {
166
+ const missing = m.files.filter(f => !existsSync(f));
167
+ console.log(`${label}: v${m.version} targets=${m.targets.join(',')} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
168
+ } else {
169
+ console.log(`${label}: v${m.version} targets=${m.targets.join(',')} files=${m.files.length} installed=${m.installedAt}`);
170
+ }
138
171
  }
139
172
  if (!found) console.log('No mugiwara installation found.');
140
173
  }
@@ -143,10 +176,11 @@ function help(): void {
143
176
  console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
144
177
 
145
178
  Usage:
146
- mugiwara [install] install the crew (default; wizard when flags missing)
179
+ mugiwara [install] install the crew (wizard; with --yes uses project + all)
147
180
  mugiwara update replace existing files (backs up differences first)
148
181
  mugiwara uninstall remove installed files via manifest
149
182
  mugiwara list show installations
183
+ mugiwara list --check health check: show installations + missing files
150
184
  mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
151
185
  mugiwara --help this help
152
186
  mugiwara --version print version
@@ -155,21 +189,14 @@ Flags:
155
189
  --global user-wide install
156
190
  --project <dir> project install (default: cwd)
157
191
  --target <ids|all> comma-separated: ${TARGET_IDS.join(', ')}
158
- --yes, -y non-interactive (needs --global/--project, --target)
192
+ --yes, -y non-interactive (defaults: project, all targets)
159
193
  --force overwrite differing files (with backup)
160
194
  --dry-run print actions without writing
195
+ --check with list: report missing files (health check)
161
196
  --keep-logs with reset: keep .mugiwara/logs (lessons ledger survives)`);
162
197
  }
163
198
 
164
- let entry = process.argv[1] !== undefined ? resolve(process.argv[1]) : undefined;
165
- if (entry !== undefined) {
166
- try {
167
- entry = realpathSync(entry);
168
- } catch {
169
- // ponytail: realpath can throw on a missing/odd path — fall back to the raw resolve
170
- }
171
- }
172
- const isMain = entry !== undefined && import.meta.url === pathToFileURL(entry).href;
199
+ const isMain = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
173
200
  if (isMain) {
174
201
  run(process.argv.slice(2)).catch(err => {
175
202
  console.error(`mugiwara: ${err.message}`);
@@ -7,9 +7,9 @@ export function parseFrontmatter(text: string): { data: FrontmatterData; body: s
7
7
  const data: FrontmatterData = {};
8
8
  for (const line of m[1].split(/\r?\n/)) {
9
9
  if (!line.trim()) continue;
10
- const i = line.indexOf(':');
11
- if (i === -1) throw new Error(`Bad frontmatter line: ${line}`);
12
- data[line.slice(0, i).trim()] = line.slice(i + 1).trim();
10
+ const i = line.indexOf(': ');
11
+ if (i === -1) throw new Error(`Bad frontmatter line (missing ': ' separator): ${line}`);
12
+ data[line.slice(0, i).trim()] = line.slice(i + 2).trim();
13
13
  }
14
14
  return { data, body: text.slice(m[0].length) };
15
15
  }
package/src/installer.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync, rmSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { parseFrontmatter, type FrontmatterData } from './frontmatter.ts';
6
7
  import type { Scope } from './manifest.ts';
7
8
 
@@ -43,8 +44,8 @@ export interface Target {
43
44
  postInstall?(opts: { scope: Scope; projectDir: string; home: string; dryRun: boolean; files: string[] }): { written: string[]; notes: string[] };
44
45
  }
45
46
 
46
- export const CONTENT_DIR = join(import.meta.dirname, '..', 'content');
47
- const pkg = JSON.parse(readFileSync(join(import.meta.dirname, '..', 'package.json'), 'utf8')) as { version: string };
47
+ export const CONTENT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'content');
48
+ const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8')) as { version: string };
48
49
  export const VERSION = pkg.version;
49
50
 
50
51
  export function collectContent(): { skills: ContentItem[]; agents: ContentItem[] } {
@@ -87,8 +88,10 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
87
88
  return;
88
89
  }
89
90
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
90
- const backupFile = join(backupRoot, 'backup', ts, absPath.replace(/[^a-zA-Z0-9]+/g, '_'));
91
- if (!dryRun) { mkdirSync(dirname(backupFile), { recursive: true }); copyFileSync(absPath, backupFile); }
91
+ const fileName = absPath.replace(/[^a-zA-Z0-9]+/g, '_');
92
+ const backupDir = join(backupRoot, 'backup', `${ts}-${target.id}`);
93
+ const backupFile = join(backupDir, fileName);
94
+ if (!dryRun) { mkdirSync(backupDir, { recursive: true }); copyFileSync(absPath, backupFile); }
92
95
  result.backedUp.push(absPath);
93
96
  }
94
97
  if (!dryRun) { mkdirSync(dirname(absPath), { recursive: true }); writeFileSync(absPath, text); }
@@ -133,10 +136,9 @@ export function removeInstalled(manifest: { files: string[] }, { dryRun = false
133
136
  for (const f of manifest.files) {
134
137
  let d = dirname(f);
135
138
  while (existsSync(d) && readdirSync(d).length === 0) {
136
- rmSync(d, { recursive: true });
137
- const parent = dirname(d);
138
- if (parent === d) break;
139
- d = parent;
139
+ if (d.endsWith('/.mugiwara') || d === dirname(d)) break;
140
+ rmSync(d, { recursive: true, force: true });
141
+ d = dirname(d);
140
142
  }
141
143
  }
142
144
  }
@@ -1,8 +1,13 @@
1
1
  // src/targets/opencode.ts
2
- import { join } from 'node:path';
2
+ import { existsSync, readdirSync, copyFileSync, mkdirSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
3
5
  import { stringifyFrontmatter, type FrontmatterData } from '../frontmatter.ts';
4
6
  import type { Target } from '../installer.ts';
5
7
 
8
+ const here = dirname(fileURLToPath(import.meta.url));
9
+ const COMMANDS_SRC = join(here, '..', '..', '.opencode', 'commands');
10
+
6
11
  type CrewConfig = {
7
12
  color: string;
8
13
  temperature: number;
@@ -62,4 +67,20 @@ export const target: Target = {
62
67
  const root = scope === 'global' ? join(home, '.config', 'opencode') : join(projectDir, '.opencode');
63
68
  return join(root, 'skills', skillName, 'references');
64
69
  },
70
+ postInstall({ scope, projectDir, home, dryRun }) {
71
+ const root = scope === 'global' ? join(home, '.config', 'opencode') : join(projectDir, '.opencode');
72
+ const dstDir = join(root, 'commands');
73
+ if (dryRun) return { written: [], notes: [] };
74
+ if (!existsSync(COMMANDS_SRC)) return { written: [], notes: [] };
75
+ mkdirSync(dstDir, { recursive: true });
76
+ const written: string[] = [];
77
+ for (const f of readdirSync(COMMANDS_SRC)) {
78
+ if (!f.endsWith('.md')) continue;
79
+ const src = join(COMMANDS_SRC, f);
80
+ const dst = join(dstDir, f);
81
+ copyFileSync(src, dst);
82
+ written.push(dst);
83
+ }
84
+ return { written, notes: [] };
85
+ },
65
86
  };