@applica-software-guru/sdd 1.0.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,20 @@
1
- import { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import { input, select } from '@inquirer/prompts';
4
- import clipboardy from 'clipboardy';
5
- import { existsSync, mkdirSync } from 'node:fs';
6
- import { resolve } from 'node:path';
7
- import ora from 'ora';
8
- import { SDD, writeConfig, runAgent } from '@applica-software-guru/sdd-core';
9
- import { printBanner } from '../ui/banner.js';
10
- import { success, info, heading } from '../ui/format.js';
11
- import { renderMarkdown } from '../ui/markdown.js';
12
-
13
- const START_PROMPT = `Read INSTRUCTIONS.md and the documentation in product/ and system/, then run \`sdd sync\` to start working.`;
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { input, select } from "@inquirer/prompts";
4
+ import clipboardy from "clipboardy";
5
+ import { existsSync, mkdirSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import ora from "ora";
8
+ import { SDD, writeConfig, runAgent } from "@applica-software-guru/sdd-core";
9
+ import { printBanner } from "../ui/banner.js";
10
+ import { success, info, heading } from "../ui/format.js";
11
+ import { renderMarkdown } from "../ui/markdown.js";
12
+
13
+ const START_PROMPT = `Read .sdd/skill/sdd/SKILL.md (fallback: .claude/skills/sdd/SKILL.md) and the documentation in product/ and system/, then run \`sdd sync\` to start working.`;
14
14
 
15
15
  function buildBootstrapPrompt(description: string, auto: boolean): string {
16
16
  if (auto) {
17
- return `Read INSTRUCTIONS.md first. This is a new SDD project.
17
+ return `Read .sdd/skill/sdd/SKILL.md first (fallback: .claude/skills/sdd/SKILL.md). This is a new SDD project.
18
18
 
19
19
  Project goal: "${description}"
20
20
 
@@ -28,10 +28,10 @@ Your task: generate the initial documentation for this project based on the desc
28
28
  - system/tech-stack.md — Technologies and frameworks
29
29
  - system/interfaces.md — API contracts
30
30
 
31
- Follow the file format described in INSTRUCTIONS.md for the YAML frontmatter. Do NOT write any code, only documentation. Commit all created files when done.`;
31
+ Follow the file format described in .sdd/skill/sdd/references/file-format.md for the YAML frontmatter. Do NOT write any code, only documentation. Commit all created files when done.`;
32
32
  }
33
33
 
34
- return `Read INSTRUCTIONS.md first. This is a new SDD project.
34
+ return `Read .sdd/skill/sdd/SKILL.md first (fallback: .claude/skills/sdd/SKILL.md). This is a new SDD project.
35
35
 
36
36
  Project goal: "${description}"
37
37
 
@@ -45,45 +45,49 @@ Your task: generate the initial documentation for this project. Ask me a few que
45
45
  - system/tech-stack.md — Technologies and frameworks
46
46
  - system/interfaces.md — API contracts
47
47
 
48
- Follow the file format described in INSTRUCTIONS.md for the YAML frontmatter. Do NOT write any code, only documentation.`;
48
+ Follow the file format described in .sdd/skill/sdd/references/file-format.md for the YAML frontmatter. Do NOT write any code, only documentation.`;
49
49
  }
50
50
 
51
51
  export function registerInit(program: Command): void {
52
52
  program
53
- .command('init <project-name>')
54
- .description('Initialize a new SDD project')
53
+ .command("init <project-name>")
54
+ .description("Initialize a new SDD project")
55
55
  .action(async (projectName: string) => {
56
56
  printBanner();
57
57
 
58
58
  const projectDir = resolve(process.cwd(), projectName);
59
59
 
60
- if (existsSync(resolve(projectDir, '.sdd'))) {
61
- console.log(chalk.yellow(`\n SDD project already initialized at ${projectName}/\n`));
60
+ if (existsSync(resolve(projectDir, ".sdd"))) {
61
+ console.log(
62
+ chalk.yellow(
63
+ `\n SDD project already initialized at ${projectName}/\n`,
64
+ ),
65
+ );
62
66
  return;
63
67
  }
64
68
 
65
69
  const promptTheme = {
66
- prefix: chalk.cyan('?'),
70
+ prefix: chalk.cyan("?"),
67
71
  style: { message: (text: string) => chalk.cyan.bold(text) },
68
72
  };
69
73
 
70
74
  const description = await input({
71
- message: 'What should your project do?',
75
+ message: "What should your project do?",
72
76
  theme: promptTheme,
73
77
  });
74
78
 
75
79
  if (!description.trim()) {
76
- console.log(chalk.yellow('\n No description provided. Aborting.\n'));
80
+ console.log(chalk.yellow("\n No description provided. Aborting.\n"));
77
81
  return;
78
82
  }
79
83
 
80
84
  const agentChoice = await select({
81
- message: 'Which agent do you use?',
85
+ message: "Which agent do you use?",
82
86
  choices: [
83
- { value: 'claude', name: 'Claude Code' },
84
- { value: 'codex', name: 'Codex' },
85
- { value: 'opencode', name: 'OpenCode' },
86
- { value: 'other', name: 'Other' },
87
+ { value: "claude", name: "Claude Code" },
88
+ { value: "codex", name: "Codex" },
89
+ { value: "opencode", name: "OpenCode" },
90
+ { value: "other", name: "Other" },
87
91
  ],
88
92
  theme: promptTheme,
89
93
  });
@@ -91,23 +95,26 @@ export function registerInit(program: Command): void {
91
95
  let agentName = agentChoice;
92
96
  let customCommand: string | undefined;
93
97
 
94
- if (agentChoice === 'other') {
98
+ if (agentChoice === "other") {
95
99
  agentName = await input({
96
- message: 'Agent name:',
100
+ message: "Agent name:",
97
101
  theme: promptTheme,
98
102
  });
99
103
  customCommand = await input({
100
- message: 'Agent command (use $PROMPT_FILE for the prompt file path):',
104
+ message: "Agent command (use $PROMPT_FILE for the prompt file path):",
101
105
  theme: promptTheme,
102
106
  });
103
107
  }
104
108
 
105
109
  const bootstrapMode = await select({
106
- message: 'How do you want to start?',
110
+ message: "How do you want to start?",
107
111
  choices: [
108
- { value: 'skip', name: 'Write docs manually' },
109
- { value: 'prompt', name: 'Generate bootstrap prompt (copy to clipboard)' },
110
- { value: 'auto', name: 'Generate and apply bootstrap automatically' },
112
+ { value: "skip", name: "Write docs manually" },
113
+ {
114
+ value: "prompt",
115
+ name: "Generate bootstrap prompt (copy to clipboard)",
116
+ },
117
+ { value: "auto", name: "Generate and apply bootstrap automatically" },
111
118
  ],
112
119
  theme: promptTheme,
113
120
  });
@@ -117,8 +124,8 @@ export function registerInit(program: Command): void {
117
124
  }
118
125
 
119
126
  const spinner = ora({
120
- text: 'Creating project structure...',
121
- color: 'cyan',
127
+ text: "Creating project structure...",
128
+ color: "cyan",
122
129
  }).start();
123
130
 
124
131
  const sdd = new SDD({ root: projectDir });
@@ -135,28 +142,31 @@ export function registerInit(program: Command): void {
135
142
  spinner.stop();
136
143
 
137
144
  // Project created
138
- console.log(chalk.cyan.bold(`\n ${chalk.white(projectName)} is ready!\n`));
145
+ console.log(
146
+ chalk.cyan.bold(`\n ${chalk.white(projectName)} is ready!\n`),
147
+ );
139
148
 
140
149
  // Show what was created
141
- console.log(chalk.dim(' Created:'));
150
+ console.log(chalk.dim(" Created:"));
142
151
  for (const f of files) {
143
152
  console.log(success(f));
144
153
  }
145
- console.log(success('product/'));
146
- console.log(success('product/features/'));
147
- console.log(success('system/'));
148
- console.log(success('code/'));
154
+ console.log(success("product/"));
155
+ console.log(success("product/features/"));
156
+ console.log(success("system/"));
157
+ console.log(success("code/"));
158
+ console.log(success(".claude/skills/"));
149
159
 
150
- if (bootstrapMode === 'auto') {
160
+ if (bootstrapMode === "auto") {
151
161
  const prompt = buildBootstrapPrompt(description.trim(), true);
152
162
 
153
- console.log(chalk.dim(''.repeat(30)));
154
- console.log(heading('Agent Prompt'));
163
+ console.log(chalk.dim("".repeat(30)));
164
+ console.log(heading("Agent Prompt"));
155
165
  console.log(renderMarkdown(prompt));
156
- console.log(chalk.dim(''.repeat(30)));
166
+ console.log(chalk.dim("".repeat(30)));
157
167
 
158
168
  console.log(info(`Using agent: ${chalk.cyan(agentName)}`));
159
- console.log(info('Starting agent...\n'));
169
+ console.log(info("Starting agent...\n"));
160
170
 
161
171
  const exitCode = await runAgent({
162
172
  root: projectDir,
@@ -170,48 +180,62 @@ export function registerInit(program: Command): void {
170
180
  process.exit(exitCode);
171
181
  }
172
182
 
173
- console.log(chalk.green('\n Agent completed successfully.'));
183
+ console.log(chalk.green("\n Agent completed successfully."));
174
184
  return;
175
185
  }
176
186
 
177
- if (bootstrapMode === 'prompt') {
187
+ if (bootstrapMode === "prompt") {
178
188
  const prompt = buildBootstrapPrompt(description.trim(), false);
179
189
 
180
- console.log(chalk.cyan.bold('\n Next steps:\n'));
181
- console.log(` ${chalk.white('1.')} Enter the project folder:\n`);
190
+ console.log(chalk.cyan.bold("\n Next steps:\n"));
191
+ console.log(` ${chalk.white("1.")} Enter the project folder:\n`);
182
192
  console.log(` ${chalk.green(`cd ${projectName}`)}\n`);
183
- console.log(` ${chalk.white('2.')} Open your AI agent and paste the prompt below.`);
184
- console.log(` It will ask you a few questions and generate the initial docs.\n`);
185
-
186
- console.log(chalk.dim(' ─'.repeat(30)));
187
- console.log(heading('Agent Prompt'));
193
+ console.log(
194
+ ` ${chalk.white("2.")} Open your AI agent and paste the prompt below.`,
195
+ );
196
+ console.log(
197
+ ` It will ask you a few questions and generate the initial docs.\n`,
198
+ );
199
+
200
+ console.log(chalk.dim(" ─".repeat(30)));
201
+ console.log(heading("Agent Prompt"));
188
202
  console.log(renderMarkdown(prompt));
189
203
 
190
204
  try {
191
205
  await clipboardy.write(prompt);
192
- console.log(success('Copied to clipboard — paste it into your agent.\n'));
206
+ console.log(
207
+ success("Copied to clipboard — paste it into your agent.\n"),
208
+ );
193
209
  } catch {
194
- console.log(info('Copy the prompt above into your agent.\n'));
210
+ console.log(info("Copy the prompt above into your agent.\n"));
195
211
  }
196
212
  return;
197
213
  }
198
214
 
199
215
  // skip — manual mode
200
- console.log(chalk.cyan.bold('\n Next steps:\n'));
201
- console.log(` ${chalk.white('1.')} Enter the project folder:\n`);
216
+ console.log(chalk.cyan.bold("\n Next steps:\n"));
217
+ console.log(` ${chalk.white("1.")} Enter the project folder:\n`);
202
218
  console.log(` ${chalk.green(`cd ${projectName}`)}\n`);
203
- console.log(` ${chalk.white('2.')} Start writing your documentation in ${chalk.cyan('product/')} and ${chalk.cyan('system/')}.`);
204
- console.log(` Check ${chalk.cyan('INSTRUCTIONS.md')} for the file format.\n`);
205
- console.log(` ${chalk.white('3.')} When ready, let your AI agent run:\n`);
206
- console.log(` ${chalk.green('sdd sync')}\n`);
219
+ console.log(
220
+ ` ${chalk.white("2.")} Start writing your documentation in ${chalk.cyan("product/")} and ${chalk.cyan("system/")}.`,
221
+ );
222
+ console.log(
223
+ ` Check ${chalk.cyan(".sdd/skill/sdd/SKILL.md")} for the workflow.\n`,
224
+ );
225
+ console.log(
226
+ ` ${chalk.white("3.")} When ready, let your AI agent run:\n`,
227
+ );
228
+ console.log(` ${chalk.green("sdd sync")}\n`);
207
229
 
208
230
  const prompt = START_PROMPT;
209
231
 
210
232
  try {
211
233
  await clipboardy.write(prompt);
212
- console.log(success('Copied to clipboard — paste it into your agent.\n'));
234
+ console.log(
235
+ success("Copied to clipboard — paste it into your agent.\n"),
236
+ );
213
237
  } catch {
214
- console.log(info('Copy the prompt above into your agent.\n'));
238
+ console.log(info("Copy the prompt above into your agent.\n"));
215
239
  }
216
240
  });
217
241
  }
@@ -0,0 +1,161 @@
1
+ import { Command } from 'commander';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve, join, basename } from 'node:path';
4
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from 'node:fs';
5
+ import { writeFile } from 'node:fs/promises';
6
+ import { spawn } from 'node:child_process';
7
+ import chalk from 'chalk';
8
+ import { success, info, error as fmtError } from '../ui/format.js';
9
+
10
+ const SCAFFOLD_TEMPLATE = (name: string) => `import React from 'react';
11
+
12
+ export default function ${name}() {
13
+ return (
14
+ <div style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
15
+ <h1>${name}</h1>
16
+ <p>Start editing this component to see changes live.</p>
17
+ </div>
18
+ );
19
+ }
20
+ `;
21
+
22
+ const PID_FILE = '.sdd/ui.pid';
23
+
24
+ function collect(value: string, prev: string[]): string[] {
25
+ return [...prev, value];
26
+ }
27
+
28
+ function resolveSddUiDir(): string {
29
+ const pkgRequire = createRequire(__filename);
30
+ try {
31
+ return dirname(pkgRequire.resolve('@applica-software-guru/sdd-ui/package.json'));
32
+ } catch {
33
+ console.error(fmtError(
34
+ 'Package @applica-software-guru/sdd-ui not found.\n' +
35
+ ' Run: npm install @applica-software-guru/sdd-ui',
36
+ ));
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ export function registerUI(program: Command): void {
42
+ const ui = program
43
+ .command('ui')
44
+ .description('UI Component Editor — visual development with live preview');
45
+
46
+ // launch-editor
47
+ ui
48
+ .command('launch-editor <component-name>')
49
+ .description('Launch the split-panel UI editor for a React component')
50
+ .option('--screenshot <path>', 'Screenshot to show in the spec panel (repeatable)', collect, [])
51
+ .option('--port <n>', 'Port for the UI editor', '5174')
52
+ .option('--detach', 'Run in background and return immediately')
53
+ .action(async (componentName: string, options) => {
54
+ const projectRoot = process.cwd();
55
+ const port = options.port as string;
56
+ const detach = Boolean(options.detach);
57
+ const screenshotArgs = options.screenshot as string[];
58
+
59
+ // Resolve and validate screenshot paths
60
+ const screenshotPaths = screenshotArgs.map((p) => resolve(projectRoot, p));
61
+ for (const p of screenshotPaths) {
62
+ if (!existsSync(p)) {
63
+ console.error(fmtError(`Screenshot not found: ${p}`));
64
+ process.exit(1);
65
+ }
66
+ }
67
+
68
+ // Resolve component path
69
+ const componentsDir = join(projectRoot, 'code', 'components');
70
+ const componentPath = join(componentsDir, `${componentName}.tsx`);
71
+
72
+ // Scaffold component if it doesn't exist
73
+ if (!existsSync(componentPath)) {
74
+ if (!existsSync(componentsDir)) {
75
+ mkdirSync(componentsDir, { recursive: true });
76
+ }
77
+ writeFileSync(componentPath, SCAFFOLD_TEMPLATE(componentName), 'utf-8');
78
+ console.log(info(`Scaffolded component at ${chalk.cyan(componentPath)}`));
79
+ }
80
+
81
+ const sddUiDir = resolveSddUiDir();
82
+
83
+ // Write .env.local — screenshot paths are pipe-separated (| is not valid in fs paths)
84
+ const envLines = [
85
+ `VITE_COMPONENT_PATH=${componentPath}`,
86
+ `VITE_COMPONENT_NAME=${componentName}`,
87
+ `VITE_SCREENSHOT_PATHS=${screenshotPaths.join('|')}`,
88
+ ];
89
+ await writeFile(join(sddUiDir, '.env.local'), envLines.join('\n'), 'utf-8');
90
+
91
+ // Print launch info
92
+ console.log('');
93
+ console.log(success(`sdd-ui starting on ${chalk.cyan(`http://localhost:${port}`)}`));
94
+ console.log(info(`Component: ${chalk.cyan(componentPath)}`));
95
+ for (const p of screenshotPaths) {
96
+ console.log(info(`Screenshot: ${chalk.cyan(basename(p))} ${chalk.dim(p)}`));
97
+ }
98
+
99
+ if (detach) {
100
+ // Launch detached — process survives after sdd exits
101
+ const vite = spawn('npx', ['vite', '--port', port], {
102
+ cwd: sddUiDir,
103
+ stdio: 'ignore',
104
+ detached: true,
105
+ shell: false,
106
+ });
107
+ vite.unref();
108
+
109
+ // Save PID so `sdd ui stop` can kill it
110
+ const pidFile = join(projectRoot, PID_FILE);
111
+ writeFileSync(pidFile, String(vite.pid), 'utf-8');
112
+
113
+ console.log(info(`Running in background ${chalk.dim(`(PID ${vite.pid})`)}`));
114
+ console.log(chalk.dim(` Stop with: ${chalk.white('sdd ui stop')}\n`));
115
+ } else {
116
+ console.log(chalk.dim(' Press Ctrl+C to stop.\n'));
117
+
118
+ const vite = spawn('npx', ['vite', '--port', port], {
119
+ cwd: sddUiDir,
120
+ stdio: 'inherit',
121
+ shell: false,
122
+ });
123
+
124
+ vite.on('error', (err) => {
125
+ console.error(fmtError(`Failed to start vite: ${err.message}`));
126
+ process.exit(1);
127
+ });
128
+
129
+ vite.on('close', (code) => process.exit(code ?? 0));
130
+
131
+ process.on('SIGINT', () => vite.kill('SIGINT'));
132
+ process.on('SIGTERM', () => vite.kill('SIGTERM'));
133
+ }
134
+ });
135
+
136
+ // stop
137
+ ui
138
+ .command('stop')
139
+ .description('Stop a detached UI editor started with --detach')
140
+ .action(() => {
141
+ const projectRoot = process.cwd();
142
+ const pidFile = join(projectRoot, PID_FILE);
143
+
144
+ if (!existsSync(pidFile)) {
145
+ console.log(info('No running sdd-ui process found.'));
146
+ return;
147
+ }
148
+
149
+ const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
150
+
151
+ try {
152
+ process.kill(pid, 'SIGTERM');
153
+ unlinkSync(pidFile);
154
+ console.log(success(`Stopped sdd-ui ${chalk.dim(`(PID ${pid})`)}`));
155
+ } catch {
156
+ // Process already dead — clean up the stale pid file
157
+ unlinkSync(pidFile);
158
+ console.log(info('Process was already stopped. Cleaned up pid file.'));
159
+ }
160
+ });
161
+ }
@@ -0,0 +1,51 @@
1
+ import { Command } from 'commander';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import chalk from 'chalk';
6
+ import { heading, success, info } from '../ui/format.js';
7
+ import { installSkills } from '../skills.js';
8
+
9
+ export function registerUpgrade(program: Command): void {
10
+ program
11
+ .command('upgrade')
12
+ .description('Upgrade sdd to the latest version and refresh skills in the current project')
13
+ .action(async () => {
14
+ console.log(heading('SDD Upgrade'));
15
+ console.log(info('Running: npm install -g @applica-software-guru/sdd@latest\n'));
16
+
17
+ const exitCode = await new Promise<number>((resolve) => {
18
+ const child = spawn('npm', ['install', '-g', '@applica-software-guru/sdd@latest'], {
19
+ stdio: 'inherit',
20
+ shell: false,
21
+ });
22
+ child.on('close', (code) => resolve(code ?? 1));
23
+ child.on('error', () => resolve(1));
24
+ });
25
+
26
+ if (exitCode !== 0) {
27
+ console.error(chalk.red('\n Upgrade failed. Check the output above.'));
28
+ process.exit(exitCode);
29
+ }
30
+
31
+ console.log('');
32
+ console.log(success('CLI upgraded.'));
33
+
34
+ // If run from inside an SDD project, refresh the skills too
35
+ const projectRoot = process.cwd();
36
+ const isSddProject = existsSync(join(projectRoot, '.sdd', 'config.yaml'));
37
+
38
+ if (isSddProject) {
39
+ const updated = installSkills(projectRoot);
40
+ if (updated) {
41
+ console.log(success(`Skills updated in ${chalk.cyan('.claude/skills/')}`));
42
+ }
43
+ } else {
44
+ console.log(
45
+ chalk.dim(
46
+ '\n Run this command from inside an SDD project to also refresh skill files.',
47
+ ),
48
+ );
49
+ }
50
+ });
51
+ }
package/src/index.ts CHANGED
@@ -1,24 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { createRequire } from 'node:module';
4
- import { registerInit } from './commands/init.js';
5
- import { registerStatus } from './commands/status.js';
6
- import { registerDiff } from './commands/diff.js';
7
- import { registerSync } from './commands/sync.js';
8
- import { registerValidate } from './commands/validate.js';
9
- import { registerMarkSynced } from './commands/mark-synced.js';
10
- import { registerCR } from './commands/cr.js';
11
- import { registerBug } from './commands/bug.js';
12
- import { registerApply } from './commands/apply.js';
2
+ import { Command } from "commander";
3
+ import { createRequire } from "node:module";
4
+ import { registerInit } from "./commands/init.js";
5
+ import { registerStatus } from "./commands/status.js";
6
+ import { registerDiff } from "./commands/diff.js";
7
+ import { registerSync } from "./commands/sync.js";
8
+ import { registerValidate } from "./commands/validate.js";
9
+ import { registerMarkSynced } from "./commands/mark-synced.js";
10
+ import { registerCR } from "./commands/cr.js";
11
+ import { registerBug } from "./commands/bug.js";
12
+ import { registerApply } from "./commands/apply.js";
13
+ import { registerAdapters } from "./commands/adapters.js";
14
+ import { registerUI } from "./commands/ui.js";
15
+ import { registerUpgrade } from "./commands/upgrade.js";
13
16
 
14
17
  const packageRequire = createRequire(__filename);
15
- const packageJson = packageRequire('../package.json') as { version: string };
18
+ const packageJson = packageRequire("../package.json") as { version: string };
16
19
 
17
20
  const program = new Command();
18
21
 
19
22
  program
20
- .name('sdd')
21
- .description('Story Driven Development — manage apps through structured documentation')
23
+ .name("sdd")
24
+ .description("Story Driven Development — manage apps through structured documentation")
22
25
  .version(packageJson.version);
23
26
 
24
27
  registerInit(program);
@@ -30,6 +33,9 @@ registerMarkSynced(program);
30
33
  registerCR(program);
31
34
  registerBug(program);
32
35
  registerApply(program);
36
+ registerAdapters(program);
37
+ registerUI(program);
38
+ registerUpgrade(program);
33
39
 
34
40
  program.parseAsync().catch((err) => {
35
41
  console.error(err.message);
package/src/skills.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { cpSync, existsSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+
4
+ /**
5
+ * Returns the path to the bundled skills directory inside the CLI package.
6
+ * Works both in development (monorepo) and when installed globally.
7
+ */
8
+ export function getSkillsSourceDir(): string {
9
+ // __filename → <cli-pkg>/dist/skills.js → go up two levels to <cli-pkg>/
10
+ const cliPkgDir = resolve(dirname(__filename), '..');
11
+ return join(cliPkgDir, 'skills');
12
+ }
13
+
14
+ /**
15
+ * Copies all bundled skill files into <projectDir>/.claude/skills/.
16
+ * Safe to call on existing projects — overwrites with the latest version.
17
+ */
18
+ export function installSkills(projectDir: string): boolean {
19
+ const src = getSkillsSourceDir();
20
+ if (!existsSync(src)) return false;
21
+
22
+ const dest = join(projectDir, '.claude', 'skills');
23
+ cpSync(src, dest, { recursive: true, force: true });
24
+ return true;
25
+ }