@applica-software-guru/sdd 1.0.3 → 1.3.3

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, 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,69 +45,47 @@ 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'))) {
60
+ if (existsSync(resolve(projectDir, ".sdd"))) {
61
61
  console.log(chalk.yellow(`\n SDD project already initialized at ${projectName}/\n`));
62
62
  return;
63
63
  }
64
64
 
65
65
  const promptTheme = {
66
- prefix: chalk.cyan('?'),
66
+ prefix: chalk.cyan("?"),
67
67
  style: { message: (text: string) => chalk.cyan.bold(text) },
68
68
  };
69
69
 
70
70
  const description = await input({
71
- message: 'What should your project do?',
71
+ message: "What should your project do?",
72
72
  theme: promptTheme,
73
73
  });
74
74
 
75
75
  if (!description.trim()) {
76
- console.log(chalk.yellow('\n No description provided. Aborting.\n'));
76
+ console.log(chalk.yellow("\n No description provided. Aborting.\n"));
77
77
  return;
78
78
  }
79
79
 
80
- const agentChoice = await select({
81
- message: 'Which agent do you use?',
82
- choices: [
83
- { value: 'claude', name: 'Claude Code' },
84
- { value: 'codex', name: 'Codex' },
85
- { value: 'opencode', name: 'OpenCode' },
86
- { value: 'other', name: 'Other' },
87
- ],
88
- theme: promptTheme,
89
- });
90
-
91
- let agentName = agentChoice;
92
- let customCommand: string | undefined;
93
-
94
- if (agentChoice === 'other') {
95
- agentName = await input({
96
- message: 'Agent name:',
97
- theme: promptTheme,
98
- });
99
- customCommand = await input({
100
- message: 'Agent command (use $PROMPT_FILE for the prompt file path):',
101
- theme: promptTheme,
102
- });
103
- }
104
-
105
80
  const bootstrapMode = await select({
106
- message: 'How do you want to start?',
81
+ message: "How do you want to start?",
107
82
  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' },
83
+ { value: "skip", name: "Write docs manually" },
84
+ {
85
+ value: "prompt",
86
+ name: "Generate bootstrap prompt (copy to clipboard)",
87
+ },
88
+ { value: "auto", name: "Generate and apply bootstrap automatically" },
111
89
  ],
112
90
  theme: promptTheme,
113
91
  });
@@ -117,52 +95,45 @@ export function registerInit(program: Command): void {
117
95
  }
118
96
 
119
97
  const spinner = ora({
120
- text: 'Creating project structure...',
121
- color: 'cyan',
98
+ text: "Creating project structure...",
99
+ color: "cyan",
122
100
  }).start();
123
101
 
124
102
  const sdd = new SDD({ root: projectDir });
125
103
  const files = await sdd.init({ description: description.trim() });
126
104
 
127
- // Save agent config
128
- const config = await sdd.config();
129
- config.agent = agentName;
130
- if (customCommand) {
131
- config.agents = { [agentName]: customCommand };
132
- }
133
- await writeConfig(projectDir, config);
134
-
135
105
  spinner.stop();
136
106
 
137
107
  // Project created
138
108
  console.log(chalk.cyan.bold(`\n ${chalk.white(projectName)} is ready!\n`));
139
109
 
140
110
  // Show what was created
141
- console.log(chalk.dim(' Created:'));
111
+ console.log(chalk.dim(" Created:"));
142
112
  for (const f of files) {
143
113
  console.log(success(f));
144
114
  }
145
- console.log(success('product/'));
146
- console.log(success('product/features/'));
147
- console.log(success('system/'));
148
- console.log(success('code/'));
149
-
150
- if (bootstrapMode === 'auto') {
115
+ console.log(success("product/"));
116
+ console.log(success("product/features/"));
117
+ console.log(success("system/"));
118
+ console.log(success("code/"));
119
+ console.log(success(".claude/skills/"));
120
+
121
+ if (bootstrapMode === "auto") {
122
+ const agentName = "claude";
151
123
  const prompt = buildBootstrapPrompt(description.trim(), true);
152
124
 
153
- console.log(chalk.dim(''.repeat(30)));
154
- console.log(heading('Agent Prompt'));
125
+ console.log(chalk.dim("".repeat(30)));
126
+ console.log(heading("Agent Prompt"));
155
127
  console.log(renderMarkdown(prompt));
156
- console.log(chalk.dim(''.repeat(30)));
128
+ console.log(chalk.dim("".repeat(30)));
157
129
 
158
- console.log(info(`Using agent: ${chalk.cyan(agentName)}`));
159
- console.log(info('Starting agent...\n'));
130
+ console.log(info(`Using agent: ${chalk.cyan(agentName)} (default)`));
131
+ console.log(info("Starting agent...\n"));
160
132
 
161
133
  const exitCode = await runAgent({
162
134
  root: projectDir,
163
135
  prompt,
164
136
  agent: agentName,
165
- agents: customCommand ? { [agentName]: customCommand } : undefined,
166
137
  });
167
138
 
168
139
  if (exitCode !== 0) {
@@ -170,48 +141,50 @@ export function registerInit(program: Command): void {
170
141
  process.exit(exitCode);
171
142
  }
172
143
 
173
- console.log(chalk.green('\n Agent completed successfully.'));
144
+ console.log(chalk.green("\n Agent completed successfully."));
174
145
  return;
175
146
  }
176
147
 
177
- if (bootstrapMode === 'prompt') {
148
+ if (bootstrapMode === "prompt") {
178
149
  const prompt = buildBootstrapPrompt(description.trim(), false);
179
150
 
180
- console.log(chalk.cyan.bold('\n Next steps:\n'));
181
- console.log(` ${chalk.white('1.')} Enter the project folder:\n`);
151
+ console.log(chalk.cyan.bold("\n Next steps:\n"));
152
+ console.log(` ${chalk.white("1.")} Enter the project folder:\n`);
182
153
  console.log(` ${chalk.green(`cd ${projectName}`)}\n`);
183
- console.log(` ${chalk.white('2.')} Open your AI agent and paste the prompt below.`);
154
+ console.log(` ${chalk.white("2.")} Open your AI agent and paste the prompt below.`);
184
155
  console.log(` It will ask you a few questions and generate the initial docs.\n`);
185
156
 
186
- console.log(chalk.dim(''.repeat(30)));
187
- console.log(heading('Agent Prompt'));
157
+ console.log(chalk.dim("".repeat(30)));
158
+ console.log(heading("Agent Prompt"));
188
159
  console.log(renderMarkdown(prompt));
189
160
 
190
161
  try {
191
162
  await clipboardy.write(prompt);
192
- console.log(success('Copied to clipboard — paste it into your agent.\n'));
163
+ console.log(success("Copied to clipboard — paste it into your agent.\n"));
193
164
  } catch {
194
- console.log(info('Copy the prompt above into your agent.\n'));
165
+ console.log(info("Copy the prompt above into your agent.\n"));
195
166
  }
196
167
  return;
197
168
  }
198
169
 
199
170
  // skip — manual mode
200
- console.log(chalk.cyan.bold('\n Next steps:\n'));
201
- console.log(` ${chalk.white('1.')} Enter the project folder:\n`);
171
+ console.log(chalk.cyan.bold("\n Next steps:\n"));
172
+ console.log(` ${chalk.white("1.")} Enter the project folder:\n`);
202
173
  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`);
174
+ console.log(
175
+ ` ${chalk.white("2.")} Start writing your documentation in ${chalk.cyan("product/")} and ${chalk.cyan("system/")}.`,
176
+ );
177
+ console.log(` Check ${chalk.cyan(".sdd/skill/sdd/SKILL.md")} for the workflow.\n`);
178
+ console.log(` ${chalk.white("3.")} When ready, let your AI agent run:\n`);
179
+ console.log(` ${chalk.green("sdd sync")}\n`);
207
180
 
208
181
  const prompt = START_PROMPT;
209
182
 
210
183
  try {
211
184
  await clipboardy.write(prompt);
212
- console.log(success('Copied to clipboard — paste it into your agent.\n'));
185
+ console.log(success("Copied to clipboard — paste it into your agent.\n"));
213
186
  } catch {
214
- console.log(info('Copy the prompt above into your agent.\n'));
187
+ console.log(info("Copy the prompt above into your agent.\n"));
215
188
  }
216
189
  });
217
190
  }
@@ -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
+ }