@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.
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "mandu": {
4
+ "command": "bunx",
5
+ "args": ["@mandujs/mcp"],
6
+ "cwd": "."
7
+ }
8
+ }
9
+ }