@ionivetech/mugiwara 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +488 -0
- package/content/agents/brook-healing.md +36 -0
- package/content/agents/chopper-checkpoint.md +41 -0
- package/content/agents/eval-runner.md +44 -0
- package/content/agents/franky-gates.md +35 -0
- package/content/agents/jinbe-security.md +41 -0
- package/content/agents/luffy-orchestrator.md +44 -0
- package/content/agents/memory-keeper.md +37 -0
- package/content/agents/nami-planner.md +42 -0
- package/content/agents/resume-coordinator.md +39 -0
- package/content/agents/robin-reviewer.md +40 -0
- package/content/agents/sanji-quality.md +36 -0
- package/content/agents/skeptic-verifier.md +39 -0
- package/content/agents/using-mugiwara.md +36 -0
- package/content/agents/usopp-brainstorm.md +36 -0
- package/content/agents/zoro-execution.md +39 -0
- package/content/skills/mugiwara-agent-security/SKILL.md +58 -0
- package/content/skills/mugiwara-backend/SKILL.md +90 -0
- package/content/skills/mugiwara-brainstorm/SKILL.md +53 -0
- package/content/skills/mugiwara-checkpoint/SKILL.md +62 -0
- package/content/skills/mugiwara-dynamic-workflow/SKILL.md +85 -0
- package/content/skills/mugiwara-eval/SKILL.md +82 -0
- package/content/skills/mugiwara-execution/SKILL.md +81 -0
- package/content/skills/mugiwara-frontend/SKILL.md +122 -0
- package/content/skills/mugiwara-gates/SKILL.md +50 -0
- package/content/skills/mugiwara-git/SKILL.md +67 -0
- package/content/skills/mugiwara-healing/SKILL.md +62 -0
- package/content/skills/mugiwara-lessons/SKILL.md +57 -0
- package/content/skills/mugiwara-observability/SKILL.md +54 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +55 -0
- package/content/skills/mugiwara-planning/SKILL.md +98 -0
- package/content/skills/mugiwara-quality/SKILL.md +39 -0
- package/content/skills/mugiwara-resume/SKILL.md +49 -0
- package/content/skills/mugiwara-review/SKILL.md +86 -0
- package/content/skills/mugiwara-security/SKILL.md +87 -0
- package/content/skills/mugiwara-ship/SKILL.md +58 -0
- package/content/skills/mugiwara-workflow/SKILL.md +90 -0
- package/dist/mugiwara.js +602 -0
- package/package.json +29 -0
- package/scripts/install.ps1 +14 -0
- package/scripts/install.sh +17 -0
- package/src/args.ts +31 -0
- package/src/cli.ts +187 -0
- package/src/frontmatter.ts +20 -0
- package/src/installer.ts +118 -0
- package/src/manifest.ts +29 -0
- package/src/prompt.ts +37 -0
- package/src/targets/antigravity.ts +10 -0
- package/src/targets/claude.ts +25 -0
- package/src/targets/cline.ts +10 -0
- package/src/targets/codex.ts +10 -0
- package/src/targets/copilot.ts +26 -0
- package/src/targets/gemini.ts +10 -0
- package/src/targets/generic.ts +43 -0
- package/src/targets/index.ts +14 -0
- package/src/targets/kilo.ts +10 -0
- package/src/targets/opencode.ts +25 -0
- package/src/targets/windsurf.ts +10 -0
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ionivetech/mugiwara",
|
|
3
|
+
"version": "0.1.0",
|
|
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, Windsurf, Cline, Kilo, Antigravity.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mugiwara": "dist/mugiwara.js"
|
|
9
|
+
},
|
|
10
|
+
"files": ["dist", "src", "content", "scripts/install.sh", "scripts/install.ps1", "README.md", "LICENSE"],
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20.11"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"build": "bun build src/cli.ts --outfile dist/mugiwara.js --target node --format esm",
|
|
19
|
+
"validate": "bun scripts/validate-content.ts",
|
|
20
|
+
"evals": "bun scripts/run-evals.ts",
|
|
21
|
+
"prepack": "bun run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["ai", "agents", "skills", "claude-code", "opencode", "copilot", "gemini", "codex", "windsurf", "cline", "multi-agent", "workflow"],
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^26.2.0",
|
|
26
|
+
"typescript": "^7.0.2",
|
|
27
|
+
"vitest": "^4.1.10"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
$ErrorActionPreference = 'Stop'
|
|
3
|
+
|
|
4
|
+
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
5
|
+
Write-Error 'mugiwara requires Node.js >= 20. Install it from https://nodejs.org first.'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
$major = [int](node -p 'process.versions.node.split(".")[0]')
|
|
9
|
+
if ($major -lt 20) {
|
|
10
|
+
Write-Error "mugiwara requires Node.js >= 20 (found $(node --version))."
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
npx -y @ionivetech/mugiwara@latest @args
|
|
14
|
+
exit $LASTEXITCODE
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
PKG="@ionivetech/mugiwara"
|
|
5
|
+
|
|
6
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
7
|
+
echo "mugiwara requires Node.js >= 20. Install it first: https://nodejs.org" >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
|
|
12
|
+
if [ "$MAJOR" -lt 20 ]; then
|
|
13
|
+
echo "mugiwara requires Node.js >= 20 (found $(node --version))." >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
exec npx -y "${PKG}@latest" "$@"
|
package/src/args.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/args.ts
|
|
2
|
+
export type FlagValue = string | boolean | undefined;
|
|
3
|
+
export type Args = {
|
|
4
|
+
command: string;
|
|
5
|
+
_: string[];
|
|
6
|
+
flags: Record<string, FlagValue>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--type': 'type' };
|
|
10
|
+
const BOOL_FLAGS: Record<string, string> = {
|
|
11
|
+
'--global': 'global', '--yes': 'yes', '-y': 'yes', '--force': 'force',
|
|
12
|
+
'--dry-run': 'dryRun', '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function parseArgs(argv: string[]): Args {
|
|
16
|
+
const out: Args = { _: [], flags: {}, command: 'install' };
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
if (a in BOOL_FLAGS) { out.flags[BOOL_FLAGS[a]] = true; continue; }
|
|
20
|
+
if (a in VALUE_FLAGS) {
|
|
21
|
+
const v = argv[++i];
|
|
22
|
+
if (v === undefined || v.startsWith('-')) throw new Error(`Flag ${a} missing value`);
|
|
23
|
+
out.flags[VALUE_FLAGS[a]] = v;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (a.startsWith('-')) throw new Error(`Unknown flag: ${a}`);
|
|
27
|
+
out._.push(a);
|
|
28
|
+
}
|
|
29
|
+
out.command = out._[0] ?? 'install';
|
|
30
|
+
return out;
|
|
31
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/cli.ts
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { parseArgs, type FlagValue, type Args } from './args.ts';
|
|
8
|
+
import { createRl, choose, multiChoose, confirm } from './prompt.ts';
|
|
9
|
+
import { targets, TARGET_IDS } from './targets/index.ts';
|
|
10
|
+
import { installTo, removeInstalled, VERSION, CONTENT_DIR } from './installer.ts';
|
|
11
|
+
import { manifestPath, readManifest, writeManifest, type Scope } from './manifest.ts';
|
|
12
|
+
import { parseFrontmatter } from './frontmatter.ts';
|
|
13
|
+
|
|
14
|
+
const TYPES = ['frontend', 'backend', 'fullstack', 'general'];
|
|
15
|
+
|
|
16
|
+
const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
|
|
17
|
+
const flag = (v: FlagValue): boolean => v === true;
|
|
18
|
+
|
|
19
|
+
export async function run(argv: string[]): Promise<void> {
|
|
20
|
+
const { command, flags } = parseArgs(argv);
|
|
21
|
+
if (flag(flags.help) || command === 'help') return help();
|
|
22
|
+
if (flag(flags.version)) { console.log(`mugiwara ${VERSION}`); return; }
|
|
23
|
+
switch (command) {
|
|
24
|
+
case 'install': return install(flags);
|
|
25
|
+
case 'update': return install({ ...flags, force: true });
|
|
26
|
+
case 'uninstall': return uninstall(flags);
|
|
27
|
+
case 'list': return list(flags);
|
|
28
|
+
case 'skills': return skills();
|
|
29
|
+
default: throw new Error(`Unknown command: ${command}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[]; type: string }> {
|
|
34
|
+
const interactive = !flag(flags.yes);
|
|
35
|
+
const rl = interactive ? createRl() : null;
|
|
36
|
+
try {
|
|
37
|
+
let scope: Scope | null = flag(flags.global) ? 'global' : str(flags.project) ? 'project' : null;
|
|
38
|
+
if (!scope) {
|
|
39
|
+
if (!interactive) throw new Error('Specify --global or --project <dir> with --yes');
|
|
40
|
+
scope = (await choose(rl!, 'Install scope?', ['global (user-wide)', 'project (this repo)'])) === 0 ? 'global' : 'project';
|
|
41
|
+
}
|
|
42
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
43
|
+
if (scope === 'project' && !existsSync(projectDir)) throw new Error(`Project dir not found: ${projectDir}`);
|
|
44
|
+
|
|
45
|
+
let targetIds = str(flags.target)?.split(',').map(s => s.trim()) ?? null;
|
|
46
|
+
if (targetIds && targetIds.includes('all')) targetIds = [...TARGET_IDS];
|
|
47
|
+
if (!targetIds) {
|
|
48
|
+
if (!interactive) throw new Error('Specify --target <ids|all> with --yes');
|
|
49
|
+
const idx = await multiChoose(rl!, 'Target AI agents?', ['all targets', ...TARGET_IDS]);
|
|
50
|
+
targetIds = idx.includes(0) ? [...TARGET_IDS] : idx.map(i => TARGET_IDS[i - 1]);
|
|
51
|
+
}
|
|
52
|
+
for (const id of targetIds) {
|
|
53
|
+
if (!targets[id]) throw new Error(`Unknown target: ${id} (valid: ${TARGET_IDS.join(', ')}, all)`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let type = str(flags.type) ?? null;
|
|
57
|
+
if (!type) {
|
|
58
|
+
if (!interactive) throw new Error('Specify --type with --yes');
|
|
59
|
+
type = TYPES[await choose(rl!, 'Project type?', TYPES)];
|
|
60
|
+
}
|
|
61
|
+
if (!TYPES.includes(type)) throw new Error(`Unknown type: ${type} (valid: ${TYPES.join(', ')})`);
|
|
62
|
+
return { scope, projectDir, targetIds, type };
|
|
63
|
+
} finally {
|
|
64
|
+
if (rl) rl.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function install(flags: Args['flags']): Promise<void> {
|
|
69
|
+
const { scope, projectDir, targetIds, type } = await resolveOptions(flags);
|
|
70
|
+
const home = homedir();
|
|
71
|
+
const allFiles: string[] = [];
|
|
72
|
+
const allNotes: string[] = [];
|
|
73
|
+
const installed: string[] = [];
|
|
74
|
+
for (const id of targetIds) {
|
|
75
|
+
const t = targets[id];
|
|
76
|
+
if (scope === 'global' && !t.native) {
|
|
77
|
+
console.log(`! ${t.label}: project scope only — skipped for global install`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
installed.push(id);
|
|
81
|
+
console.log(`\n-> ${t.label} (${scope})`);
|
|
82
|
+
const r = installTo(t, { scope, projectDir, type, home, dryRun: flag(flags.dryRun), force: flag(flags.force) });
|
|
83
|
+
console.log(` written ${r.written.length}, skipped ${r.skipped.length}, backed up ${r.backedUp.length}`);
|
|
84
|
+
for (const n of r.notes) console.log(` note: ${n}`);
|
|
85
|
+
allFiles.push(...r.written);
|
|
86
|
+
allNotes.push(...r.notes);
|
|
87
|
+
}
|
|
88
|
+
if (flag(flags.dryRun)) { console.log('\nDry run — nothing written.'); return; }
|
|
89
|
+
const file = manifestPath({ scope, projectDir, home });
|
|
90
|
+
const prev = readManifest(file);
|
|
91
|
+
writeManifest(file, {
|
|
92
|
+
version: VERSION,
|
|
93
|
+
scope,
|
|
94
|
+
type,
|
|
95
|
+
installedAt: new Date().toISOString(),
|
|
96
|
+
targets: [...new Set([...(prev?.targets ?? []), ...installed])],
|
|
97
|
+
files: [...new Set([...(prev?.files ?? []), ...allFiles])],
|
|
98
|
+
});
|
|
99
|
+
console.log(`\nOK mugiwara ${VERSION} installed (manifest: ${file})`);
|
|
100
|
+
if (allNotes.length) console.log(`${allNotes.length} note(s) above may need attention.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function uninstall(flags: Args['flags']): Promise<void> {
|
|
104
|
+
const scope: Scope = flag(flags.global) ? 'global' : 'project';
|
|
105
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
106
|
+
const home = homedir();
|
|
107
|
+
const file = manifestPath({ scope, projectDir, home });
|
|
108
|
+
const manifest = readManifest(file);
|
|
109
|
+
if (!manifest) { console.log('Nothing installed (no manifest found).'); return; }
|
|
110
|
+
console.log(`Will remove ${manifest.files.length} files (targets: ${manifest.targets.join(', ')}).`);
|
|
111
|
+
if (!flag(flags.yes)) {
|
|
112
|
+
const rl = createRl();
|
|
113
|
+
const ok = await confirm(rl, 'Proceed?');
|
|
114
|
+
rl.close();
|
|
115
|
+
if (!ok) { console.log('Aborted.'); return; }
|
|
116
|
+
}
|
|
117
|
+
const removed = removeInstalled(manifest, { dryRun: flag(flags.dryRun) });
|
|
118
|
+
if (!flag(flags.dryRun)) rmSync(file);
|
|
119
|
+
console.log(`OK removed ${removed.length} files`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function list(flags: Args['flags']): void {
|
|
123
|
+
const home = homedir();
|
|
124
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
125
|
+
let found = false;
|
|
126
|
+
for (const [label, file] of [
|
|
127
|
+
['project', manifestPath({ scope: 'project', projectDir, home })],
|
|
128
|
+
['global', manifestPath({ scope: 'global', projectDir, home })],
|
|
129
|
+
]) {
|
|
130
|
+
const m = readManifest(file);
|
|
131
|
+
if (!m) continue;
|
|
132
|
+
found = true;
|
|
133
|
+
console.log(`${label}: v${m.version} targets=${m.targets.join(',')} files=${m.files.length} installed=${m.installedAt}`);
|
|
134
|
+
}
|
|
135
|
+
if (!found) console.log('No mugiwara installation found.');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function skills(): void {
|
|
139
|
+
const dir = join(CONTENT_DIR, 'skills');
|
|
140
|
+
const names = readdirSync(dir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
141
|
+
const rows = names.map(name => {
|
|
142
|
+
const { data } = parseFrontmatter(readFileSync(join(dir, name, 'SKILL.md'), 'utf8'));
|
|
143
|
+
return [name, data.description ?? ''] as const;
|
|
144
|
+
});
|
|
145
|
+
const w = Math.max(...rows.map(r => r[0].length)) + 2;
|
|
146
|
+
console.log(`mugiwara ${VERSION} — ${rows.length} skills (agentskills.io format):\n`);
|
|
147
|
+
for (const [name, description] of rows) console.log(` ${name.padEnd(w)}${description}`);
|
|
148
|
+
console.log(`\nInstall skills into any agent via skills.sh:\n npx skills add ionivetech/mugiwara`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function help(): void {
|
|
152
|
+
console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
|
|
153
|
+
|
|
154
|
+
Usage:
|
|
155
|
+
mugiwara [install] install the crew (default; wizard when flags missing)
|
|
156
|
+
mugiwara update replace existing files (backs up differences first)
|
|
157
|
+
mugiwara uninstall remove installed files via manifest
|
|
158
|
+
mugiwara list show installations
|
|
159
|
+
mugiwara skills list installable skills (agentskills.io)
|
|
160
|
+
mugiwara --help this help
|
|
161
|
+
mugiwara --version print version
|
|
162
|
+
|
|
163
|
+
Flags:
|
|
164
|
+
--global user-wide install
|
|
165
|
+
--project <dir> project install (default: cwd)
|
|
166
|
+
--target <ids|all> comma-separated: ${TARGET_IDS.join(', ')}
|
|
167
|
+
--type <t> frontend | backend | fullstack | general
|
|
168
|
+
--yes, -y non-interactive (needs --global/--project, --target, --type)
|
|
169
|
+
--force overwrite differing files (with backup)
|
|
170
|
+
--dry-run print actions without writing`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let entry = process.argv[1] !== undefined ? resolve(process.argv[1]) : undefined;
|
|
174
|
+
if (entry !== undefined) {
|
|
175
|
+
try {
|
|
176
|
+
entry = realpathSync(entry);
|
|
177
|
+
} catch {
|
|
178
|
+
// ponytail: realpath can throw on a missing/odd path — fall back to the raw resolve
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const isMain = entry !== undefined && import.meta.url === pathToFileURL(entry).href;
|
|
182
|
+
if (isMain) {
|
|
183
|
+
run(process.argv.slice(2)).catch(err => {
|
|
184
|
+
console.error(`mugiwara: ${err.message}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/frontmatter.ts
|
|
2
|
+
export type FrontmatterData = Record<string, string>;
|
|
3
|
+
|
|
4
|
+
export function parseFrontmatter(text: string): { data: FrontmatterData; body: string } {
|
|
5
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
6
|
+
if (!m) throw new Error('Missing frontmatter fence (---)');
|
|
7
|
+
const data: FrontmatterData = {};
|
|
8
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
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();
|
|
13
|
+
}
|
|
14
|
+
return { data, body: text.slice(m[0].length) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function stringifyFrontmatter(data: FrontmatterData, body: string): string {
|
|
18
|
+
const lines = Object.entries(data).map(([k, v]) => `${k}: ${v}`);
|
|
19
|
+
return `---\n${lines.join('\n')}\n---\n${body}`;
|
|
20
|
+
}
|
package/src/installer.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// src/installer.ts
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { parseFrontmatter, type FrontmatterData } from './frontmatter.ts';
|
|
6
|
+
import type { Scope } from './manifest.ts';
|
|
7
|
+
|
|
8
|
+
export type ContentItem = { name: string; data: FrontmatterData; body: string };
|
|
9
|
+
|
|
10
|
+
export type InstallOptions = {
|
|
11
|
+
scope: Scope;
|
|
12
|
+
projectDir: string;
|
|
13
|
+
type: string;
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
force?: boolean;
|
|
16
|
+
home?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type InstallResult = {
|
|
20
|
+
written: string[];
|
|
21
|
+
skipped: string[];
|
|
22
|
+
backedUp: string[];
|
|
23
|
+
notes: string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type TransformOut = { relPath: string; text: string } | null;
|
|
27
|
+
|
|
28
|
+
export interface Target {
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
native: boolean;
|
|
32
|
+
paths(opts: { scope: Scope; projectDir: string; home: string }): { skillsDir: string; agentsDir: string };
|
|
33
|
+
transformSkill(data: FrontmatterData, body: string): TransformOut;
|
|
34
|
+
transformAgent(data: FrontmatterData, body: string): TransformOut;
|
|
35
|
+
postInstall?(opts: { scope: Scope; projectDir: string; home: string; dryRun: boolean; files: string[] }): { written: string[]; notes: string[] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const CONTENT_DIR = join(import.meta.dirname, '..', 'content');
|
|
39
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dirname, '..', 'package.json'), 'utf8')) as { version: string };
|
|
40
|
+
export const VERSION = pkg.version;
|
|
41
|
+
|
|
42
|
+
export function collectContent({ includeFrontend }: { includeFrontend: boolean }): { skills: ContentItem[]; agents: ContentItem[] } {
|
|
43
|
+
const skillNames = readdirSync(join(CONTENT_DIR, 'skills'), { withFileTypes: true })
|
|
44
|
+
.filter(e => e.isDirectory()).map(e => e.name)
|
|
45
|
+
.filter(name => includeFrontend || name !== 'mugiwara-frontend');
|
|
46
|
+
const skills = skillNames.map(name => {
|
|
47
|
+
const { data, body } = parseFrontmatter(readFileSync(join(CONTENT_DIR, 'skills', name, 'SKILL.md'), 'utf8'));
|
|
48
|
+
return { name, data, body };
|
|
49
|
+
});
|
|
50
|
+
const agents = readdirSync(join(CONTENT_DIR, 'agents'))
|
|
51
|
+
.filter(f => f.endsWith('.md'))
|
|
52
|
+
.map(f => {
|
|
53
|
+
const { data, body } = parseFrontmatter(readFileSync(join(CONTENT_DIR, 'agents', f), 'utf8'));
|
|
54
|
+
return { name: f.replace(/\.md$/, ''), data, body };
|
|
55
|
+
});
|
|
56
|
+
return { skills, agents };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function installTo(target: Target, opts: InstallOptions): InstallResult {
|
|
60
|
+
const { scope, projectDir, type, dryRun = false, force = false } = opts;
|
|
61
|
+
const home = opts.home ?? homedir();
|
|
62
|
+
const { skills, agents } = collectContent({ includeFrontend: type === 'frontend' || type === 'fullstack' });
|
|
63
|
+
const dirs = target.paths({ scope, projectDir, home });
|
|
64
|
+
const backupRoot = join(scope === 'global' ? home : projectDir, '.mugiwara');
|
|
65
|
+
const result: InstallResult = { written: [], skipped: [], backedUp: [], notes: [] };
|
|
66
|
+
|
|
67
|
+
const writeOne = (absPath: string, text: string) => {
|
|
68
|
+
if (existsSync(absPath)) {
|
|
69
|
+
if (readFileSync(absPath, 'utf8') === text) { result.skipped.push(absPath); return; }
|
|
70
|
+
if (!force) {
|
|
71
|
+
result.skipped.push(absPath);
|
|
72
|
+
result.notes.push(`conflict (not overwritten; run update to replace with backup): ${absPath}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
76
|
+
const backupFile = join(backupRoot, 'backup', ts, absPath.replace(/[^a-zA-Z0-9]+/g, '_'));
|
|
77
|
+
if (!dryRun) { mkdirSync(dirname(backupFile), { recursive: true }); copyFileSync(absPath, backupFile); }
|
|
78
|
+
result.backedUp.push(absPath);
|
|
79
|
+
}
|
|
80
|
+
if (!dryRun) { mkdirSync(dirname(absPath), { recursive: true }); writeFileSync(absPath, text); }
|
|
81
|
+
result.written.push(absPath);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const s of skills) {
|
|
85
|
+
const out = target.transformSkill(s.data, s.body);
|
|
86
|
+
if (out) writeOne(join(dirs.skillsDir, out.relPath), out.text);
|
|
87
|
+
}
|
|
88
|
+
for (const a of agents) {
|
|
89
|
+
const out = target.transformAgent(a.data, a.body);
|
|
90
|
+
if (out) writeOne(join(dirs.agentsDir, out.relPath), out.text);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (target.postInstall) {
|
|
94
|
+
const post = target.postInstall({ scope, projectDir, home, dryRun, files: result.written });
|
|
95
|
+
result.written.push(...post.written);
|
|
96
|
+
result.notes.push(...post.notes);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function removeInstalled(manifest: { files: string[] }, { dryRun = false }: { dryRun?: boolean } = {}): string[] {
|
|
102
|
+
const removed: string[] = [];
|
|
103
|
+
for (const f of manifest.files) {
|
|
104
|
+
if (existsSync(f)) { if (!dryRun) rmSync(f); removed.push(f); }
|
|
105
|
+
}
|
|
106
|
+
if (!dryRun) {
|
|
107
|
+
for (const f of manifest.files) {
|
|
108
|
+
let d = dirname(f);
|
|
109
|
+
while (existsSync(d) && readdirSync(d).length === 0) {
|
|
110
|
+
rmSync(d, { recursive: true });
|
|
111
|
+
const parent = dirname(d);
|
|
112
|
+
if (parent === d) break;
|
|
113
|
+
d = parent;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return removed;
|
|
118
|
+
}
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/manifest.ts
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export type Scope = 'global' | 'project';
|
|
6
|
+
|
|
7
|
+
export type Manifest = {
|
|
8
|
+
version: string;
|
|
9
|
+
scope: Scope;
|
|
10
|
+
type: string;
|
|
11
|
+
installedAt: string;
|
|
12
|
+
targets: string[];
|
|
13
|
+
files: string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function manifestPath({ scope, projectDir, home }: { scope: Scope; projectDir: string; home: string }): string {
|
|
17
|
+
return scope === 'global'
|
|
18
|
+
? join(home, '.mugiwara', 'manifest.json')
|
|
19
|
+
: join(projectDir, '.mugiwara', 'manifest.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readManifest(file: string): Manifest | null {
|
|
23
|
+
return existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) as Manifest : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function writeManifest(file: string, data: Manifest): void {
|
|
27
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
28
|
+
writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
|
|
29
|
+
}
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/prompt.ts
|
|
2
|
+
import readline from 'node:readline/promises';
|
|
3
|
+
|
|
4
|
+
export type Rl = { question: (prompt: string) => Promise<string> };
|
|
5
|
+
|
|
6
|
+
export function createRl(): readline.Interface {
|
|
7
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function choose(rl: Rl, question: string, options: string[]): Promise<number> {
|
|
11
|
+
for (;;) {
|
|
12
|
+
console.log(`\n${question}`);
|
|
13
|
+
options.forEach((o, i) => console.log(` ${i + 1}) ${o}`));
|
|
14
|
+
const n = Number((await rl.question('choice> ')).trim());
|
|
15
|
+
if (Number.isInteger(n) && n >= 1 && n <= options.length) return n - 1;
|
|
16
|
+
console.log(`Enter a number between 1 and ${options.length}.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function multiChoose(rl: Rl, question: string, options: string[]): Promise<number[]> {
|
|
21
|
+
for (;;) {
|
|
22
|
+
console.log(`\n${question} (comma-separated numbers, or "all")`);
|
|
23
|
+
options.forEach((o, i) => console.log(` ${i + 1}) ${o}`));
|
|
24
|
+
const raw = (await rl.question('choices> ')).trim().toLowerCase();
|
|
25
|
+
if (raw === 'all') return options.map((_, i) => i);
|
|
26
|
+
const nums = raw.split(',').map(s => Number(s.trim()));
|
|
27
|
+
if (nums.length > 0 && nums.every(n => Number.isInteger(n) && n >= 1 && n <= options.length)) {
|
|
28
|
+
return [...new Set(nums.map(n => n - 1))];
|
|
29
|
+
}
|
|
30
|
+
console.log('Invalid selection.');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function confirm(rl: Rl, question: string): Promise<boolean> {
|
|
35
|
+
const raw = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
36
|
+
return raw === 'y' || raw === 'yes';
|
|
37
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/targets/claude.ts
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { stringifyFrontmatter, type FrontmatterData } from '../frontmatter.ts';
|
|
4
|
+
import type { Target } from '../installer.ts';
|
|
5
|
+
|
|
6
|
+
export const target: Target = {
|
|
7
|
+
id: 'claude',
|
|
8
|
+
label: 'Claude Code',
|
|
9
|
+
native: true,
|
|
10
|
+
paths({ scope, projectDir, home }) {
|
|
11
|
+
const root = scope === 'global' ? join(home, '.claude') : join(projectDir, '.claude');
|
|
12
|
+
return { skillsDir: join(root, 'skills'), agentsDir: join(root, 'agents') };
|
|
13
|
+
},
|
|
14
|
+
transformSkill(data: FrontmatterData, body: string) {
|
|
15
|
+
return {
|
|
16
|
+
relPath: join(data.name, 'SKILL.md'),
|
|
17
|
+
text: stringifyFrontmatter({ name: data.name, description: data.description }, body),
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
transformAgent(data: FrontmatterData, body: string) {
|
|
21
|
+
const fm: FrontmatterData = { name: data.name, description: data.description };
|
|
22
|
+
if (data.tools) fm.tools = data.tools;
|
|
23
|
+
return { relPath: `${data.name}.md`, text: stringifyFrontmatter(fm, body) };
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// src/targets/codex.ts
|
|
2
|
+
import { makeGeneric } from './generic.ts';
|
|
3
|
+
|
|
4
|
+
export const target = makeGeneric({
|
|
5
|
+
id: 'codex',
|
|
6
|
+
label: 'Codex',
|
|
7
|
+
rulesDir: '.codex/mugiwara',
|
|
8
|
+
bootstrapFile: 'AGENTS.md',
|
|
9
|
+
bootstrapPointer: 'Mugiwara crew installed in .codex/mugiwara/ — read .codex/mugiwara/mugiwara-workflow.md to run the pipeline.',
|
|
10
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/targets/copilot.ts
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { stringifyFrontmatter, type FrontmatterData } from '../frontmatter.ts';
|
|
4
|
+
import type { Target } from '../installer.ts';
|
|
5
|
+
|
|
6
|
+
export const target: Target = {
|
|
7
|
+
id: 'copilot',
|
|
8
|
+
label: 'GitHub Copilot',
|
|
9
|
+
native: true,
|
|
10
|
+
paths({ scope, projectDir, home }) {
|
|
11
|
+
const root = scope === 'global' ? join(home, '.copilot') : join(projectDir, '.github');
|
|
12
|
+
return { skillsDir: join(root, 'instructions'), agentsDir: join(root, 'agents') };
|
|
13
|
+
},
|
|
14
|
+
transformSkill(data: FrontmatterData, body: string) {
|
|
15
|
+
return {
|
|
16
|
+
relPath: `${data.name}.instructions.md`,
|
|
17
|
+
text: stringifyFrontmatter({ description: data.description, applyTo: '**/*' }, body),
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
transformAgent(data: FrontmatterData, body: string) {
|
|
21
|
+
return {
|
|
22
|
+
relPath: `${data.name}.md`,
|
|
23
|
+
text: stringifyFrontmatter({ name: data.name, description: data.description }, body),
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// src/targets/gemini.ts
|
|
2
|
+
import { makeGeneric } from './generic.ts';
|
|
3
|
+
|
|
4
|
+
export const target = makeGeneric({
|
|
5
|
+
id: 'gemini',
|
|
6
|
+
label: 'Gemini',
|
|
7
|
+
rulesDir: '.gemini/mugiwara',
|
|
8
|
+
bootstrapFile: 'GEMINI.md',
|
|
9
|
+
bootstrapPointer: 'Mugiwara crew installed in .gemini/mugiwara/ — read .gemini/mugiwara/mugiwara-workflow.md to run the pipeline.',
|
|
10
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/targets/generic.ts
|
|
2
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import type { Target } from '../installer.ts';
|
|
5
|
+
|
|
6
|
+
export function makeGeneric(opts: {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
rulesDir: string;
|
|
10
|
+
bootstrapFile: string | null;
|
|
11
|
+
bootstrapPointer: string | null;
|
|
12
|
+
}): Target {
|
|
13
|
+
const { id, label, rulesDir, bootstrapFile, bootstrapPointer } = opts;
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
label,
|
|
17
|
+
native: false,
|
|
18
|
+
paths({ scope, projectDir }: { scope: 'global' | 'project'; projectDir: string; home: string }) {
|
|
19
|
+
if (scope === 'global') throw new Error(`${label} supports project scope only`);
|
|
20
|
+
const dir = join(projectDir, rulesDir);
|
|
21
|
+
return { skillsDir: dir, agentsDir: dir };
|
|
22
|
+
},
|
|
23
|
+
transformSkill(data, body) {
|
|
24
|
+
return { relPath: `${data.name}.md`, text: `# ${data.name}\n\n> ${data.description}\n\n${body}` };
|
|
25
|
+
},
|
|
26
|
+
transformAgent(data, body) {
|
|
27
|
+
return { relPath: `agent-${data.name}.md`, text: `# Agent: ${data.name}\n\n> ${data.description}\n\nSkills used: ${data.skills ?? ''}\n\n${body}` };
|
|
28
|
+
},
|
|
29
|
+
postInstall({ projectDir, dryRun }) {
|
|
30
|
+
if (!bootstrapFile) return { written: [], notes: [] };
|
|
31
|
+
const notes: string[] = [];
|
|
32
|
+
const written: string[] = [];
|
|
33
|
+
const file = join(projectDir, bootstrapFile);
|
|
34
|
+
if (!existsSync(file)) {
|
|
35
|
+
if (!dryRun) writeFileSync(file, `${bootstrapPointer}\n`);
|
|
36
|
+
written.push(file);
|
|
37
|
+
} else {
|
|
38
|
+
notes.push(`add this line to ${bootstrapFile} so the agent finds the crew: "${bootstrapPointer}"`);
|
|
39
|
+
}
|
|
40
|
+
return { written, notes };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/targets/index.ts
|
|
2
|
+
import type { Target } from '../installer.ts';
|
|
3
|
+
import { target as claude } from './claude.ts';
|
|
4
|
+
import { target as opencode } from './opencode.ts';
|
|
5
|
+
import { target as copilot } from './copilot.ts';
|
|
6
|
+
import { target as gemini } from './gemini.ts';
|
|
7
|
+
import { target as codex } from './codex.ts';
|
|
8
|
+
import { target as windsurf } from './windsurf.ts';
|
|
9
|
+
import { target as cline } from './cline.ts';
|
|
10
|
+
import { target as kilo } from './kilo.ts';
|
|
11
|
+
import { target as antigravity } from './antigravity.ts';
|
|
12
|
+
|
|
13
|
+
export const targets: Record<string, Target> = { claude, opencode, copilot, gemini, codex, windsurf, cline, kilo, antigravity };
|
|
14
|
+
export const TARGET_IDS = Object.keys(targets);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// src/targets/kilo.ts
|
|
2
|
+
import { makeGeneric } from './generic.ts';
|
|
3
|
+
|
|
4
|
+
export const target = makeGeneric({
|
|
5
|
+
id: 'kilo',
|
|
6
|
+
label: 'Kilo Code',
|
|
7
|
+
rulesDir: '.kilo/rules',
|
|
8
|
+
bootstrapFile: 'kilo.jsonc',
|
|
9
|
+
bootstrapPointer: '{\n "instructions": [\n ".kilo/rules/*.md"\n ]\n}',
|
|
10
|
+
});
|