@mandujs/skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +85 -0
- package/README.md +77 -0
- package/package.json +52 -0
- package/skills/mandu-create-api/SKILL.md +145 -0
- package/skills/mandu-create-feature/SKILL.md +99 -0
- package/skills/mandu-debug/SKILL.md +109 -0
- package/skills/mandu-deploy/SKILL.md +174 -0
- package/skills/mandu-explain/SKILL.md +121 -0
- package/skills/mandu-fs-routes/SKILL.md +131 -0
- package/skills/mandu-guard-guide/SKILL.md +150 -0
- package/skills/mandu-hydration/SKILL.md +134 -0
- package/skills/mandu-slot/SKILL.md +132 -0
- package/src/cli.ts +125 -0
- package/src/index.ts +239 -0
- package/src/init-integration.ts +106 -0
- package/templates/.claude/settings.json +37 -0
- package/templates/.mcp.json +9 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mandu-slot
|
|
3
|
+
description: |
|
|
4
|
+
Filling API 레퍼런스. ctx 메서드, 라이프사이클 훅, 미들웨어 체인.
|
|
5
|
+
Mandu.filling(), ctx.ok(), .guard(), slot 파일 작업 시 자동 호출.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Mandu Slot (Filling API)
|
|
9
|
+
|
|
10
|
+
Slot은 비즈니스 로직을 작성하는 파일입니다. `Mandu.filling()` API를 사용하여
|
|
11
|
+
API 핸들러, 인증 가드, 라이프사이클 훅을 구현합니다.
|
|
12
|
+
|
|
13
|
+
## Filling Chain API
|
|
14
|
+
|
|
15
|
+
### Basic Route Handler
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// app/api/users/route.ts
|
|
19
|
+
import { Mandu } from "@mandujs/core";
|
|
20
|
+
|
|
21
|
+
export default Mandu.filling()
|
|
22
|
+
.get((ctx) => ctx.ok({ users: [] }))
|
|
23
|
+
.post(async (ctx) => {
|
|
24
|
+
const body = await ctx.body<{ name: string }>();
|
|
25
|
+
return ctx.created({ user: { id: "1", ...body } });
|
|
26
|
+
})
|
|
27
|
+
.patch(async (ctx) => {
|
|
28
|
+
const body = await ctx.body<{ name: string }>();
|
|
29
|
+
return ctx.ok({ user: { id: ctx.params.id, ...body } });
|
|
30
|
+
})
|
|
31
|
+
.delete((ctx) => ctx.noContent());
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
CRITICAL: Always use `Mandu.filling()` chain as default export. Never export raw functions.
|
|
35
|
+
|
|
36
|
+
## Context Methods (ctx)
|
|
37
|
+
|
|
38
|
+
### Response Methods
|
|
39
|
+
|
|
40
|
+
| Method | Status | Usage |
|
|
41
|
+
|--------|--------|-------|
|
|
42
|
+
| `ctx.ok(data)` | 200 | Success response |
|
|
43
|
+
| `ctx.created(data)` | 201 | Resource created |
|
|
44
|
+
| `ctx.noContent()` | 204 | Deleted/no body |
|
|
45
|
+
| `ctx.error(status, msg)` | 4xx/5xx | Error response |
|
|
46
|
+
|
|
47
|
+
### Request Methods
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
const body = await ctx.body<T>(); // Parse JSON body (MUST await)
|
|
51
|
+
const { id } = ctx.params; // URL params (/users/[id])
|
|
52
|
+
const { page, limit } = ctx.query; // Query string (?page=1&limit=10)
|
|
53
|
+
const auth = ctx.headers.get("authorization"); // Request header
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Store (cross-middleware state)
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
ctx.set("user", authenticatedUser); // Set value
|
|
60
|
+
const user = ctx.get("user"); // Get value
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Guard (Authentication/Authorization)
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
export default Mandu.filling()
|
|
67
|
+
.guard(async (ctx) => {
|
|
68
|
+
const token = ctx.headers.get("authorization");
|
|
69
|
+
if (!token) return ctx.error(401, "Unauthorized");
|
|
70
|
+
const user = await verifyToken(token);
|
|
71
|
+
ctx.set("user", user);
|
|
72
|
+
// Return void to continue, return Response to block
|
|
73
|
+
})
|
|
74
|
+
.get((ctx) => ctx.ok({ user: ctx.get("user") }));
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Multiple guards run in order. First one to return a Response blocks the chain.
|
|
78
|
+
|
|
79
|
+
## Lifecycle Hooks
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
export default Mandu.filling()
|
|
83
|
+
.onRequest((ctx) => {
|
|
84
|
+
ctx.set("startTime", Date.now()); // Before handler
|
|
85
|
+
})
|
|
86
|
+
.get((ctx) => ctx.ok({ data: "hello" }))
|
|
87
|
+
.afterHandle((ctx, response) => {
|
|
88
|
+
const ms = Date.now() - ctx.get("startTime");
|
|
89
|
+
console.log(`${ctx.method} ${ctx.path} ${ms}ms`);
|
|
90
|
+
return response; // After handler
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Slot File Locations
|
|
95
|
+
|
|
96
|
+
| File | Location | Purpose |
|
|
97
|
+
|------|----------|---------|
|
|
98
|
+
| API route | `app/api/{name}/route.ts` | HTTP endpoint handler |
|
|
99
|
+
| Data loader | `spec/slots/{name}.slot.ts` | Server-side data fetch (before render) |
|
|
100
|
+
| Client logic | `spec/slots/{name}.client.ts` | Client-side island logic |
|
|
101
|
+
|
|
102
|
+
Slot files (`.slot.ts`) run on the server before page rendering.
|
|
103
|
+
Data they return is available to Islands via `useServerData()`.
|
|
104
|
+
|
|
105
|
+
## Middleware Pattern
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Reusable middleware
|
|
109
|
+
const withAuth = (ctx) => {
|
|
110
|
+
const token = ctx.headers.get("authorization");
|
|
111
|
+
if (!token) return ctx.error(401, "Unauthorized");
|
|
112
|
+
ctx.set("user", decodeToken(token));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const withRateLimit = (ctx) => {
|
|
116
|
+
// rate limit logic
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Compose
|
|
120
|
+
export default Mandu.filling()
|
|
121
|
+
.guard(withRateLimit)
|
|
122
|
+
.guard(withAuth)
|
|
123
|
+
.get((ctx) => ctx.ok({ user: ctx.get("user") }));
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Common Mistakes
|
|
127
|
+
|
|
128
|
+
- Exporting raw handler functions instead of `Mandu.filling()` chain
|
|
129
|
+
- Placing slot files outside `spec/slots/` directory
|
|
130
|
+
- Forgetting to `await ctx.body()`
|
|
131
|
+
- Using inline auth checks instead of `.guard()`
|
|
132
|
+
- Not returning `void` from guard when request should continue
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* @mandujs/skills CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bunx mandu-skills install [options]
|
|
7
|
+
* bunx mandu-skills install --force
|
|
8
|
+
* bunx mandu-skills install --dry-run
|
|
9
|
+
* bunx mandu-skills list
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { installSkills, listSkillIds, type SkillId } from "./index.js";
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const command = args[0];
|
|
16
|
+
|
|
17
|
+
function printUsage(): void {
|
|
18
|
+
console.log(`
|
|
19
|
+
@mandujs/skills - Claude Code Plugin for Mandu Framework
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
mandu-skills install [options] Install skills into current project
|
|
23
|
+
mandu-skills list List available skills
|
|
24
|
+
mandu-skills help Show this help
|
|
25
|
+
|
|
26
|
+
Install Options:
|
|
27
|
+
--force Overwrite existing files
|
|
28
|
+
--dry-run Report what would be done without writing
|
|
29
|
+
--target <dir> Target directory (default: cwd)
|
|
30
|
+
--skills <ids> Comma-separated skill IDs to install
|
|
31
|
+
--skip-mcp Skip .mcp.json setup
|
|
32
|
+
--skip-settings Skip .claude/settings.json setup
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseFlag(flag: string): boolean {
|
|
37
|
+
return args.includes(flag);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseFlagValue(flag: string): string | undefined {
|
|
41
|
+
const idx = args.indexOf(flag);
|
|
42
|
+
if (idx === -1 || idx + 1 >= args.length) return undefined;
|
|
43
|
+
return args[idx + 1];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main(): Promise<void> {
|
|
47
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
48
|
+
printUsage();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (command === "list") {
|
|
53
|
+
console.log("\nAvailable Mandu Skills:\n");
|
|
54
|
+
const skills = listSkillIds();
|
|
55
|
+
for (const id of skills) {
|
|
56
|
+
console.log(` - ${id}`);
|
|
57
|
+
}
|
|
58
|
+
console.log(`\nTotal: ${skills.length} skills\n`);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (command === "install") {
|
|
63
|
+
const force = parseFlag("--force");
|
|
64
|
+
const dryRun = parseFlag("--dry-run");
|
|
65
|
+
const targetDir = parseFlagValue("--target") || process.cwd();
|
|
66
|
+
const skipMcp = parseFlag("--skip-mcp");
|
|
67
|
+
const skipSettings = parseFlag("--skip-settings");
|
|
68
|
+
const skillsRaw = parseFlagValue("--skills");
|
|
69
|
+
const skills = skillsRaw
|
|
70
|
+
? (skillsRaw.split(",").map((s) => s.trim()) as SkillId[])
|
|
71
|
+
: undefined;
|
|
72
|
+
|
|
73
|
+
console.log(`\n Mandu Skills Installer${dryRun ? " (dry-run)" : ""}\n`);
|
|
74
|
+
console.log(` Target: ${targetDir}`);
|
|
75
|
+
if (force) console.log(" Mode: force (overwrite existing)");
|
|
76
|
+
console.log();
|
|
77
|
+
|
|
78
|
+
const result = await installSkills({
|
|
79
|
+
targetDir,
|
|
80
|
+
force,
|
|
81
|
+
dryRun,
|
|
82
|
+
skills,
|
|
83
|
+
skipMcp,
|
|
84
|
+
skipSettings,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (result.installed.length > 0) {
|
|
88
|
+
console.log(" Installed:");
|
|
89
|
+
for (const file of result.installed) {
|
|
90
|
+
console.log(` + ${file}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (result.skipped.length > 0) {
|
|
95
|
+
console.log(" Skipped:");
|
|
96
|
+
for (const file of result.skipped) {
|
|
97
|
+
console.log(` - ${file}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (result.errors.length > 0) {
|
|
102
|
+
console.log(" Errors:");
|
|
103
|
+
for (const err of result.errors) {
|
|
104
|
+
console.log(` ! ${err}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const total = result.installed.length + result.skipped.length;
|
|
109
|
+
console.log(`\n Done. ${result.installed.length}/${total} files written.\n`);
|
|
110
|
+
|
|
111
|
+
if (result.errors.length > 0) {
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.error(`Unknown command: ${command}`);
|
|
118
|
+
printUsage();
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
main().catch((err) => {
|
|
123
|
+
console.error("Fatal error:", err);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mandujs/skills - Claude Code Plugin for Mandu Framework
|
|
3
|
+
*
|
|
4
|
+
* Programmatic API for installing and managing Mandu skills
|
|
5
|
+
* in Claude Code projects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdir, readFile, writeFile, mkdir, access, copyFile } from "fs/promises";
|
|
9
|
+
import { join, dirname, resolve } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
/** Root of the @mandujs/skills package */
|
|
16
|
+
const PACKAGE_ROOT = resolve(__dirname, "..");
|
|
17
|
+
|
|
18
|
+
/** Available skill IDs (9 skills) */
|
|
19
|
+
export const SKILL_IDS = [
|
|
20
|
+
"mandu-create-feature",
|
|
21
|
+
"mandu-create-api",
|
|
22
|
+
"mandu-debug",
|
|
23
|
+
"mandu-explain",
|
|
24
|
+
"mandu-guard-guide",
|
|
25
|
+
"mandu-deploy",
|
|
26
|
+
"mandu-slot",
|
|
27
|
+
"mandu-fs-routes",
|
|
28
|
+
"mandu-hydration",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export type SkillId = (typeof SKILL_IDS)[number];
|
|
32
|
+
|
|
33
|
+
export interface InstallOptions {
|
|
34
|
+
/** Target project directory (defaults to cwd) */
|
|
35
|
+
targetDir?: string;
|
|
36
|
+
/** Overwrite existing files */
|
|
37
|
+
force?: boolean;
|
|
38
|
+
/** Only install specific skills */
|
|
39
|
+
skills?: SkillId[];
|
|
40
|
+
/** Skip MCP config setup */
|
|
41
|
+
skipMcp?: boolean;
|
|
42
|
+
/** Skip Claude settings setup */
|
|
43
|
+
skipSettings?: boolean;
|
|
44
|
+
/** Dry run - report what would be done without writing files */
|
|
45
|
+
dryRun?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface InstallResult {
|
|
49
|
+
installed: string[];
|
|
50
|
+
skipped: string[];
|
|
51
|
+
errors: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
55
|
+
try {
|
|
56
|
+
await access(filePath);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Deep-merge two JSON objects. Arrays are replaced, not merged.
|
|
65
|
+
*/
|
|
66
|
+
function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
|
|
67
|
+
const result = { ...target };
|
|
68
|
+
|
|
69
|
+
for (const key of Object.keys(source)) {
|
|
70
|
+
const srcVal = source[key];
|
|
71
|
+
const tgtVal = target[key];
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
srcVal !== null &&
|
|
75
|
+
typeof srcVal === "object" &&
|
|
76
|
+
!Array.isArray(srcVal) &&
|
|
77
|
+
tgtVal !== null &&
|
|
78
|
+
typeof tgtVal === "object" &&
|
|
79
|
+
!Array.isArray(tgtVal)
|
|
80
|
+
) {
|
|
81
|
+
result[key] = deepMerge(
|
|
82
|
+
tgtVal as Record<string, unknown>,
|
|
83
|
+
srcVal as Record<string, unknown>
|
|
84
|
+
);
|
|
85
|
+
} else {
|
|
86
|
+
result[key] = srcVal;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Install Mandu skills into a project.
|
|
95
|
+
*
|
|
96
|
+
* Copies skill SKILL.md files from `skills/<id>/SKILL.md` to
|
|
97
|
+
* `<targetDir>/.claude/skills/<id>.md`.
|
|
98
|
+
*/
|
|
99
|
+
export async function installSkills(options: InstallOptions = {}): Promise<InstallResult> {
|
|
100
|
+
const targetDir = options.targetDir || process.cwd();
|
|
101
|
+
const force = options.force ?? false;
|
|
102
|
+
const skillIds = options.skills ?? [...SKILL_IDS];
|
|
103
|
+
const dryRun = options.dryRun ?? false;
|
|
104
|
+
|
|
105
|
+
const result: InstallResult = {
|
|
106
|
+
installed: [],
|
|
107
|
+
skipped: [],
|
|
108
|
+
errors: [],
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// 1. Install skill files to .claude/skills/
|
|
112
|
+
const skillsDir = join(targetDir, ".claude", "skills");
|
|
113
|
+
if (!dryRun) {
|
|
114
|
+
await mkdir(skillsDir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const skillId of skillIds) {
|
|
118
|
+
const srcPath = join(PACKAGE_ROOT, "skills", skillId, "SKILL.md");
|
|
119
|
+
const destPath = join(skillsDir, `${skillId}.md`);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (!force && (await fileExists(destPath))) {
|
|
123
|
+
result.skipped.push(`skills/${skillId}.md (exists)`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (dryRun) {
|
|
128
|
+
result.installed.push(`skills/${skillId}.md (dry-run)`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await copyFile(srcPath, destPath);
|
|
133
|
+
result.installed.push(`skills/${skillId}.md`);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
result.errors.push(`skills/${skillId}.md: ${err instanceof Error ? err.message : String(err)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. Setup .mcp.json (merge, don't overwrite)
|
|
140
|
+
if (!options.skipMcp) {
|
|
141
|
+
const mcpPath = join(targetDir, ".mcp.json");
|
|
142
|
+
try {
|
|
143
|
+
const templateContent = await readFile(
|
|
144
|
+
join(PACKAGE_ROOT, "templates", ".mcp.json"),
|
|
145
|
+
"utf-8"
|
|
146
|
+
);
|
|
147
|
+
const templateConfig = JSON.parse(templateContent);
|
|
148
|
+
|
|
149
|
+
if (await fileExists(mcpPath)) {
|
|
150
|
+
if (force) {
|
|
151
|
+
if (!dryRun) {
|
|
152
|
+
const existing = JSON.parse(await readFile(mcpPath, "utf-8"));
|
|
153
|
+
const merged = deepMerge(existing, templateConfig);
|
|
154
|
+
await writeFile(mcpPath, JSON.stringify(merged, null, 2) + "\n");
|
|
155
|
+
}
|
|
156
|
+
result.installed.push(".mcp.json (merged)");
|
|
157
|
+
} else {
|
|
158
|
+
// Check if mandu server already configured
|
|
159
|
+
const existing = JSON.parse(await readFile(mcpPath, "utf-8"));
|
|
160
|
+
if (existing.mcpServers?.mandu) {
|
|
161
|
+
result.skipped.push(".mcp.json (mandu server exists)");
|
|
162
|
+
} else {
|
|
163
|
+
if (!dryRun) {
|
|
164
|
+
const merged = deepMerge(existing, templateConfig);
|
|
165
|
+
await writeFile(mcpPath, JSON.stringify(merged, null, 2) + "\n");
|
|
166
|
+
}
|
|
167
|
+
result.installed.push(".mcp.json (mandu server added)");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
if (!dryRun) {
|
|
172
|
+
await writeFile(mcpPath, JSON.stringify(templateConfig, null, 2) + "\n");
|
|
173
|
+
}
|
|
174
|
+
result.installed.push(".mcp.json (created)");
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
result.errors.push(`.mcp.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 3. Setup .claude/settings.json (merge hooks and permissions)
|
|
182
|
+
if (!options.skipSettings) {
|
|
183
|
+
const settingsPath = join(targetDir, ".claude", "settings.json");
|
|
184
|
+
try {
|
|
185
|
+
const templateContent = await readFile(
|
|
186
|
+
join(PACKAGE_ROOT, "templates", ".claude", "settings.json"),
|
|
187
|
+
"utf-8"
|
|
188
|
+
);
|
|
189
|
+
const templateSettings = JSON.parse(templateContent);
|
|
190
|
+
|
|
191
|
+
if (!dryRun) {
|
|
192
|
+
await mkdir(join(targetDir, ".claude"), { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (await fileExists(settingsPath)) {
|
|
196
|
+
if (force) {
|
|
197
|
+
if (!dryRun) {
|
|
198
|
+
const existing = JSON.parse(await readFile(settingsPath, "utf-8"));
|
|
199
|
+
const merged = deepMerge(existing, templateSettings);
|
|
200
|
+
await writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n");
|
|
201
|
+
}
|
|
202
|
+
result.installed.push(".claude/settings.json (merged)");
|
|
203
|
+
} else {
|
|
204
|
+
result.skipped.push(".claude/settings.json (exists, use --force to merge)");
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
if (!dryRun) {
|
|
208
|
+
await writeFile(settingsPath, JSON.stringify(templateSettings, null, 2) + "\n");
|
|
209
|
+
}
|
|
210
|
+
result.installed.push(".claude/settings.json (created)");
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
result.errors.push(`.claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get the path to a skill SKILL.md file in the package
|
|
222
|
+
*/
|
|
223
|
+
export function getSkillPath(skillId: SkillId): string {
|
|
224
|
+
return join(PACKAGE_ROOT, "skills", skillId, "SKILL.md");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get the path to a template file in the package
|
|
229
|
+
*/
|
|
230
|
+
export function getTemplatePath(relativePath: string): string {
|
|
231
|
+
return join(PACKAGE_ROOT, "templates", relativePath);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* List all available skill IDs
|
|
236
|
+
*/
|
|
237
|
+
export function listSkillIds(): readonly SkillId[] {
|
|
238
|
+
return SKILL_IDS;
|
|
239
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration module for `mandu init`
|
|
3
|
+
*
|
|
4
|
+
* Called by packages/cli/src/commands/init.ts during project scaffolding.
|
|
5
|
+
* Copies skills, settings, and configures the Claude Code environment.
|
|
6
|
+
*
|
|
7
|
+
* Usage from init.ts:
|
|
8
|
+
* import { setupClaudeSkills } from "@mandujs/skills/init-integration";
|
|
9
|
+
* await setupClaudeSkills(targetDir);
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFile, writeFile, mkdir, copyFile } from "fs/promises";
|
|
13
|
+
import { join, dirname, resolve } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
const PACKAGE_ROOT = resolve(__dirname, "..");
|
|
19
|
+
|
|
20
|
+
/** Skill directories containing SKILL.md files */
|
|
21
|
+
const SKILL_DIRS = [
|
|
22
|
+
"mandu-create-feature",
|
|
23
|
+
"mandu-create-api",
|
|
24
|
+
"mandu-debug",
|
|
25
|
+
"mandu-explain",
|
|
26
|
+
"mandu-guard-guide",
|
|
27
|
+
"mandu-deploy",
|
|
28
|
+
"mandu-slot",
|
|
29
|
+
"mandu-fs-routes",
|
|
30
|
+
"mandu-hydration",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export interface SetupResult {
|
|
34
|
+
skillsInstalled: number;
|
|
35
|
+
settingsCreated: boolean;
|
|
36
|
+
errors: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Install Claude Code skills and settings into a new project.
|
|
41
|
+
* Called by `mandu init` during project creation.
|
|
42
|
+
*
|
|
43
|
+
* This function:
|
|
44
|
+
* 1. Creates .claude/skills/ directory
|
|
45
|
+
* 2. Copies all 9 skill markdown files (from skills/<id>/SKILL.md -> .claude/skills/<id>.md)
|
|
46
|
+
* 3. Creates .claude/settings.json with hooks and permissions
|
|
47
|
+
*
|
|
48
|
+
* NOTE: .mcp.json is handled separately by init.ts's setupMcpConfig()
|
|
49
|
+
* to support merge logic for Claude, Gemini, and other agent configs.
|
|
50
|
+
*/
|
|
51
|
+
export async function setupClaudeSkills(targetDir: string): Promise<SetupResult> {
|
|
52
|
+
const result: SetupResult = {
|
|
53
|
+
skillsInstalled: 0,
|
|
54
|
+
settingsCreated: false,
|
|
55
|
+
errors: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// 1. Create .claude/skills/ directory
|
|
59
|
+
const skillsDir = join(targetDir, ".claude", "skills");
|
|
60
|
+
try {
|
|
61
|
+
await mkdir(skillsDir, { recursive: true });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
result.errors.push(`mkdir .claude/skills: ${err instanceof Error ? err.message : String(err)}`);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Copy skill files (skills/<id>/SKILL.md -> .claude/skills/<id>.md)
|
|
68
|
+
for (const skillDir of SKILL_DIRS) {
|
|
69
|
+
const srcPath = join(PACKAGE_ROOT, "skills", skillDir, "SKILL.md");
|
|
70
|
+
const destPath = join(skillsDir, `${skillDir}.md`);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await copyFile(srcPath, destPath);
|
|
74
|
+
result.skillsInstalled++;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
result.errors.push(`copy ${skillDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Create .claude/settings.json
|
|
81
|
+
try {
|
|
82
|
+
const templatePath = join(PACKAGE_ROOT, "templates", ".claude", "settings.json");
|
|
83
|
+
const templateContent = await readFile(templatePath, "utf-8");
|
|
84
|
+
const settingsPath = join(targetDir, ".claude", "settings.json");
|
|
85
|
+
await writeFile(settingsPath, templateContent);
|
|
86
|
+
result.settingsCreated = true;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
result.errors.push(`settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the count of available skills (for display in init output)
|
|
96
|
+
*/
|
|
97
|
+
export function getSkillCount(): number {
|
|
98
|
+
return SKILL_DIRS.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get skill names (for display in init output)
|
|
103
|
+
*/
|
|
104
|
+
export function getSkillNames(): string[] {
|
|
105
|
+
return [...SKILL_DIRS];
|
|
106
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(bun *)",
|
|
5
|
+
"Bash(bunx mandu *)",
|
|
6
|
+
"Bash(bunx @mandujs/*)",
|
|
7
|
+
"mcp__mandu__mandu_*"
|
|
8
|
+
],
|
|
9
|
+
"deny": []
|
|
10
|
+
},
|
|
11
|
+
"hooks": {
|
|
12
|
+
"PreToolUse": [
|
|
13
|
+
{
|
|
14
|
+
"matcher": "Edit|Write",
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "bunx mandu guard check-file \"$FILE_PATH\" --json --severity error 2>/dev/null || true"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"description": "Run architecture guard validation before modifying source files"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"PostToolUse": [
|
|
25
|
+
{
|
|
26
|
+
"matcher": "Edit|Write",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "node -e \"const p=process.env.FILE_PATH||'';if(p.includes('spec/contracts/')){process.stdout.write('Contract modified. Run: bunx mandu guard contract --validate')}\" 2>/dev/null || true"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"description": "Notify when contract files are modified"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|