@dedesfr/prompter 0.7.9 → 0.8.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.
Files changed (68) hide show
  1. package/AGENTS.md +18 -0
  2. package/CHANGELOG.md +30 -0
  3. package/CLAUDE.md +17 -0
  4. package/dist/cli/index.js +2 -1
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/commands/init.d.ts +4 -0
  7. package/dist/commands/init.d.ts.map +1 -1
  8. package/dist/commands/init.js +164 -1
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/update.d.ts.map +1 -1
  11. package/dist/commands/update.js +21 -0
  12. package/dist/commands/update.js.map +1 -1
  13. package/dist/core/configurators/slash/antigravity.d.ts +1 -0
  14. package/dist/core/configurators/slash/antigravity.d.ts.map +1 -1
  15. package/dist/core/configurators/slash/antigravity.js +3 -0
  16. package/dist/core/configurators/slash/antigravity.js.map +1 -1
  17. package/dist/core/configurators/slash/base.d.ts +18 -0
  18. package/dist/core/configurators/slash/base.d.ts.map +1 -1
  19. package/dist/core/configurators/slash/base.js +65 -0
  20. package/dist/core/configurators/slash/base.js.map +1 -1
  21. package/dist/core/configurators/slash/claude.d.ts +1 -0
  22. package/dist/core/configurators/slash/claude.d.ts.map +1 -1
  23. package/dist/core/configurators/slash/claude.js +3 -0
  24. package/dist/core/configurators/slash/claude.js.map +1 -1
  25. package/dist/core/configurators/slash/codex.d.ts +1 -0
  26. package/dist/core/configurators/slash/codex.d.ts.map +1 -1
  27. package/dist/core/configurators/slash/codex.js +3 -0
  28. package/dist/core/configurators/slash/codex.js.map +1 -1
  29. package/dist/core/configurators/slash/droid.d.ts +1 -0
  30. package/dist/core/configurators/slash/droid.d.ts.map +1 -1
  31. package/dist/core/configurators/slash/droid.js +3 -0
  32. package/dist/core/configurators/slash/droid.js.map +1 -1
  33. package/dist/core/configurators/slash/forge.d.ts +1 -0
  34. package/dist/core/configurators/slash/forge.d.ts.map +1 -1
  35. package/dist/core/configurators/slash/forge.js +3 -0
  36. package/dist/core/configurators/slash/forge.js.map +1 -1
  37. package/dist/core/configurators/slash/github-copilot.d.ts +1 -0
  38. package/dist/core/configurators/slash/github-copilot.d.ts.map +1 -1
  39. package/dist/core/configurators/slash/github-copilot.js +3 -0
  40. package/dist/core/configurators/slash/github-copilot.js.map +1 -1
  41. package/dist/core/configurators/slash/kilocode.d.ts +1 -0
  42. package/dist/core/configurators/slash/kilocode.d.ts.map +1 -1
  43. package/dist/core/configurators/slash/kilocode.js +3 -0
  44. package/dist/core/configurators/slash/kilocode.js.map +1 -1
  45. package/dist/core/configurators/slash/opencode.d.ts +1 -0
  46. package/dist/core/configurators/slash/opencode.d.ts.map +1 -1
  47. package/dist/core/configurators/slash/opencode.js +3 -0
  48. package/dist/core/configurators/slash/opencode.js.map +1 -1
  49. package/dist/core/skill-discovery.d.ts +12 -0
  50. package/dist/core/skill-discovery.d.ts.map +1 -0
  51. package/dist/core/skill-discovery.js +58 -0
  52. package/dist/core/skill-discovery.js.map +1 -0
  53. package/package.json +1 -1
  54. package/src/cli/index.ts +2 -1
  55. package/src/commands/init.ts +182 -2
  56. package/src/commands/update.ts +22 -0
  57. package/src/core/configurators/slash/antigravity.ts +4 -0
  58. package/src/core/configurators/slash/base.ts +95 -0
  59. package/src/core/configurators/slash/claude.ts +4 -0
  60. package/src/core/configurators/slash/codex.ts +4 -0
  61. package/src/core/configurators/slash/droid.ts +4 -0
  62. package/src/core/configurators/slash/forge.ts +4 -0
  63. package/src/core/configurators/slash/github-copilot.ts +4 -0
  64. package/src/core/configurators/slash/kilocode.ts +4 -0
  65. package/src/core/configurators/slash/opencode.ts +4 -0
  66. package/src/core/skill-discovery.ts +68 -0
  67. package/.claude/settings.local.json +0 -7
  68. package/.github/prompts/prd-agent-generator.prompt.md +0 -133
@@ -7,10 +7,12 @@ import { projectTemplate, agentsTemplate, claudeTemplate } from '../core/templat
7
7
  import { PROMPT_TEMPLATES } from '../core/prompt-templates.js';
8
8
  import { registry } from '../core/configurators/slash/index.js';
9
9
  import { SlashCommandId } from '../core/templates/index.js';
10
+ import { discoverSkills, SkillMetadata } from '../core/skill-discovery.js';
10
11
 
11
12
  interface InitOptions {
12
13
  tools?: string[];
13
14
  prompts?: string[];
15
+ skills?: string[];
14
16
  noInteractive?: boolean;
15
17
  }
16
18
 
@@ -145,7 +147,7 @@ export class InitCommand {
145
147
  // Detect currently installed prompts (use path.join to get prompter path)
146
148
  const prompterPathForDetection = path.join(projectPath, PROMPTER_DIR);
147
149
  const currentPrompts = await this.detectInstalledPrompts(prompterPathForDetection);
148
-
150
+
149
151
  selectedPrompts = await checkbox({
150
152
  message: 'Select prompt templates to install:',
151
153
  choices: this.getCategorizedPromptChoices(currentPrompts),
@@ -158,6 +160,34 @@ export class InitCommand {
158
160
  }
159
161
  }
160
162
 
163
+ // Select skills
164
+ const availableSkills = await discoverSkills(path.join(projectPath, 'skills'));
165
+ let selectedSkills: SkillMetadata[] = [];
166
+
167
+ if (options.skills && options.skills.length > 0) {
168
+ const requestedNames = options.skills.flatMap(s => s.split(',').map(s => s.trim()));
169
+ selectedSkills = availableSkills.filter(s => requestedNames.includes(s.name));
170
+ } else if (!options.noInteractive && availableSkills.length > 0) {
171
+ try {
172
+ const prompterPathForDetection = path.join(projectPath, PROMPTER_DIR);
173
+ const currentSkillNames = await this.detectInstalledSkills(prompterPathForDetection);
174
+
175
+ const selectedSkillNames = await checkbox({
176
+ message: 'Select skills to install:',
177
+ choices: availableSkills.map(skill => ({
178
+ name: ` ${skill.name}`,
179
+ value: skill.name,
180
+ checked: currentSkillNames.includes(skill.name)
181
+ }))
182
+ });
183
+ selectedSkills = availableSkills.filter(s => selectedSkillNames.includes(s.name));
184
+ } catch (error) {
185
+ // User cancelled
186
+ console.log(chalk.yellow(isReInitialization ? '\nRe-configuration cancelled.' : '\nInitialization cancelled.'));
187
+ return;
188
+ }
189
+ }
190
+
161
191
  // Create or ensure prompter directory
162
192
  const prompterPath = await PrompterConfig.ensurePrompterDir(projectPath);
163
193
  if (!isReInitialization) {
@@ -349,10 +379,13 @@ export class InitCommand {
349
379
  }
350
380
  }
351
381
 
382
+ // --- Skills setup ---
383
+ const skillChanges = await this.setupSkills(projectPath, prompterPath, selectedTools, selectedSkills);
384
+
352
385
  // Success message
353
386
  if (isReInitialization) {
354
387
  console.log(chalk.green('\n✅ Prompter tools updated successfully!\n'));
355
- if (toolsToAdd.length > 0 || toolsToRemove.length > 0 || promptsToAdd.length > 0 || promptsToRemove.length > 0) {
388
+ if (toolsToAdd.length > 0 || toolsToRemove.length > 0 || promptsToAdd.length > 0 || promptsToRemove.length > 0 || skillChanges.added.length > 0 || skillChanges.removed.length > 0) {
356
389
  console.log(chalk.blue('Summary:'));
357
390
  if (toolsToAdd.length > 0) {
358
391
  console.log(chalk.green(' Tools Added: ') + toolsToAdd.map(t => {
@@ -378,6 +411,12 @@ export class InitCommand {
378
411
  return prompt ? prompt.name : p;
379
412
  }).join(', '));
380
413
  }
414
+ if (skillChanges.added.length > 0) {
415
+ console.log(chalk.green(' Skills Added: ') + skillChanges.added.join(', '));
416
+ }
417
+ if (skillChanges.removed.length > 0) {
418
+ console.log(chalk.yellow(' Skills Removed: ') + skillChanges.removed.join(', '));
419
+ }
381
420
  console.log();
382
421
  } else {
383
422
  console.log(chalk.gray(' No changes made.\n'));
@@ -387,6 +426,9 @@ export class InitCommand {
387
426
  if (promptsToAdd.length > 0) {
388
427
  console.log(chalk.gray(`Installed ${promptsToAdd.length} prompt template(s).\n`));
389
428
  }
429
+ if (skillChanges.added.length > 0) {
430
+ console.log(chalk.gray(`Installed ${skillChanges.added.length} skill(s).\n`));
431
+ }
390
432
  console.log(chalk.gray('Run `prompter guide` for next steps.\n'));
391
433
  }
392
434
  }
@@ -604,6 +646,144 @@ Use \`@/prompter/CLAUDE.md\` to learn:
604
646
  }
605
647
  }
606
648
 
649
+ private async setupSkills(
650
+ projectPath: string,
651
+ prompterPath: string,
652
+ selectedTools: string[],
653
+ selectedSkills: SkillMetadata[]
654
+ ): Promise<{ added: string[]; removed: string[] }> {
655
+ const result = { added: [] as string[], removed: [] as string[] };
656
+
657
+ const installedSkillNames = await this.detectInstalledSkills(prompterPath);
658
+ const selectedSkillNames = selectedSkills.map(s => s.name);
659
+
660
+ const skillsToAdd = selectedSkills.filter(s => !installedSkillNames.includes(s.name));
661
+ const skillsToRemove = installedSkillNames.filter(n => !selectedSkillNames.includes(n));
662
+ const skillsToKeep = selectedSkills.filter(s => installedSkillNames.includes(s.name));
663
+
664
+ const skillsTargetDir = path.join(prompterPath, 'skills');
665
+
666
+ // Remove deselected skills
667
+ if (skillsToRemove.length > 0) {
668
+ console.log(chalk.blue('\n🗑️ Removing skills...\n'));
669
+
670
+ for (const skillName of skillsToRemove) {
671
+ const staleDir = path.join(skillsTargetDir, skillName);
672
+ try {
673
+ await fs.rm(staleDir, { recursive: true, force: true });
674
+ console.log(chalk.yellow('✓') + ` Removed skill ${chalk.cyan(skillName)}`);
675
+ result.removed.push(skillName);
676
+ } catch {
677
+ // ignore
678
+ }
679
+
680
+ for (const toolId of selectedTools) {
681
+ const configurator = registry.get(toolId);
682
+ if (!configurator) continue;
683
+ const removed = await configurator.removeSkillFiles(projectPath, [skillName]);
684
+ for (const file of removed) {
685
+ console.log(chalk.yellow('✓') + ` Removed ${chalk.cyan(file)}`);
686
+ }
687
+ }
688
+ }
689
+ }
690
+
691
+ // Install new skills
692
+ if (skillsToAdd.length > 0) {
693
+ console.log(chalk.blue('\n🧩 Installing skills...\n'));
694
+ await fs.mkdir(skillsTargetDir, { recursive: true });
695
+
696
+ for (const skill of skillsToAdd) {
697
+ const targetDir = path.join(skillsTargetDir, skill.name);
698
+ try {
699
+ await this.copyDirectory(skill.sourcePath, targetDir);
700
+ console.log(chalk.green('✓') + ` Installed skill ${chalk.cyan(skill.name)}`);
701
+ result.added.push(skill.name);
702
+ } catch (error) {
703
+ console.log(chalk.red('✗') + ` Failed to install skill ${skill.name}: ${error}`);
704
+ }
705
+ }
706
+
707
+ if (selectedTools.length > 0) {
708
+ console.log(chalk.blue('\n📝 Creating skill workflow files...\n'));
709
+ for (const toolId of selectedTools) {
710
+ const configurator = registry.get(toolId);
711
+ if (!configurator) continue;
712
+ try {
713
+ const files = await configurator.generateSkills(projectPath, skillsToAdd);
714
+ for (const file of files) {
715
+ console.log(chalk.green('✓') + ` Created ${chalk.cyan(file)}`);
716
+ }
717
+ } catch (error) {
718
+ console.log(chalk.red('✗') + ` Failed to create skill files for ${toolId}: ${error}`);
719
+ }
720
+ }
721
+ }
722
+ }
723
+
724
+ // Update kept skills
725
+ if (skillsToKeep.length > 0) {
726
+ await fs.mkdir(skillsTargetDir, { recursive: true });
727
+
728
+ for (const skill of skillsToKeep) {
729
+ const targetDir = path.join(skillsTargetDir, skill.name);
730
+ try {
731
+ await this.copyDirectory(skill.sourcePath, targetDir);
732
+ } catch {
733
+ // ignore update errors
734
+ }
735
+ }
736
+
737
+ for (const toolId of selectedTools) {
738
+ const configurator = registry.get(toolId);
739
+ if (!configurator) continue;
740
+ try {
741
+ await configurator.generateSkills(projectPath, skillsToKeep);
742
+ } catch {
743
+ // ignore
744
+ }
745
+ }
746
+ }
747
+
748
+ return result;
749
+ }
750
+
751
+ private async detectInstalledSkills(prompterPath: string): Promise<string[]> {
752
+ const skillsDir = path.join(prompterPath, 'skills');
753
+ const names: string[] = [];
754
+
755
+ try {
756
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
757
+ for (const entry of entries) {
758
+ if (!entry.isDirectory()) continue;
759
+ const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md');
760
+ if (await this.fileExists(skillMdPath)) {
761
+ names.push(entry.name);
762
+ }
763
+ }
764
+ } catch {
765
+ // skills directory doesn't exist yet
766
+ }
767
+
768
+ return names;
769
+ }
770
+
771
+ private async copyDirectory(src: string, dest: string): Promise<void> {
772
+ await fs.mkdir(dest, { recursive: true });
773
+ const entries = await fs.readdir(src, { withFileTypes: true });
774
+
775
+ for (const entry of entries) {
776
+ const srcPath = path.join(src, entry.name);
777
+ const destPath = path.join(dest, entry.name);
778
+
779
+ if (entry.isDirectory()) {
780
+ await this.copyDirectory(srcPath, destPath);
781
+ } else {
782
+ await fs.copyFile(srcPath, destPath);
783
+ }
784
+ }
785
+ }
786
+
607
787
  private async ensureRootAgentsFile(projectPath: string): Promise<void> {
608
788
  const rootAgentsPath = path.join(projectPath, 'AGENTS.md');
609
789
  const instructionsBlock = `<!-- PROMPTER:START -->
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import { PrompterConfig, AVAILABLE_PROMPTS, PROMPTER_DIR } from '../core/config.js';
5
5
  import { registry } from '../core/configurators/slash/index.js';
6
6
  import { PROMPT_TEMPLATES } from '../core/prompt-templates.js';
7
+ import { discoverSkills } from '../core/skill-discovery.js';
7
8
 
8
9
  export class UpdateCommand {
9
10
  async execute(): Promise<void> {
@@ -56,6 +57,27 @@ export class UpdateCommand {
56
57
  updatedCount++;
57
58
  }
58
59
 
60
+ // Update existing skill workflow files
61
+ const skillsDir = path.join(projectPath, 'skills');
62
+ const availableSkills = await discoverSkills(skillsDir);
63
+
64
+ if (availableSkills.length > 0) {
65
+ for (const toolId of configuredTools) {
66
+ const configurator = registry.get(toolId);
67
+ if (!configurator) continue;
68
+
69
+ try {
70
+ const updatedSkillFiles = await configurator.updateExistingSkills(projectPath, availableSkills);
71
+ for (const file of updatedSkillFiles) {
72
+ console.log(chalk.green('✓') + ` Updated ${chalk.cyan(file)}`);
73
+ updatedCount++;
74
+ }
75
+ } catch (error) {
76
+ console.log(chalk.red('✗') + ` Failed to update skills for ${configurator.toolId}: ${error}`);
77
+ }
78
+ }
79
+ }
80
+
59
81
  if (updatedCount === 0) {
60
82
  console.log(chalk.yellow('⚠️ No workflow files found to update.'));
61
83
  console.log(chalk.gray(' Run `prompter init` to create them.\n'));
@@ -63,4 +63,8 @@ export class AntigravityConfigurator extends SlashCommandConfigurator {
63
63
  const description = DESCRIPTIONS[id];
64
64
  return `---\ndescription: ${description}\n---`;
65
65
  }
66
+
67
+ protected getSkillTargetDir(skillName: string): string {
68
+ return `.agent/skills/${skillName}`;
69
+ }
66
70
  }
@@ -2,6 +2,7 @@ import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
3
  import { SlashCommandId, TemplateManager } from '../../templates/index.js';
4
4
  import { PROMPTER_MARKERS } from '../../config.js';
5
+ import { SkillMetadata } from '../../skill-discovery.js';
5
6
 
6
7
  export interface SlashCommandTarget {
7
8
  id: SlashCommandId;
@@ -9,6 +10,12 @@ export interface SlashCommandTarget {
9
10
  kind: 'slash';
10
11
  }
11
12
 
13
+ export interface SkillTarget {
14
+ name: string;
15
+ path: string;
16
+ kind: 'skill';
17
+ }
18
+
12
19
  const ALL_COMMANDS: SlashCommandId[] = ['enhance', 'prd-generator', 'prd-agent-generator', 'product-brief', 'epic-single', 'epic-generator', 'story-single', 'story-generator', 'qa-test-scenario', 'skill-creator', 'ai-humanizer', 'api-contract-generator', 'apply', 'archive', 'design-system', 'erd-generator', 'fsd-generator', 'proposal', 'tdd-generator', 'tdd-lite-generator', 'wireframe-generator', 'document-explainer'];
13
20
 
14
21
  export abstract class SlashCommandConfigurator {
@@ -77,10 +84,98 @@ export abstract class SlashCommandConfigurator {
77
84
  protected abstract getRelativePath(id: SlashCommandId): string;
78
85
  protected abstract getFrontmatter(id: SlashCommandId): string | undefined;
79
86
 
87
+ /**
88
+ * Returns the relative directory path where the skill's full directory
89
+ * will be placed inside the tool's config dir.
90
+ * e.g. `.claude/skills/laravel-code-review`
91
+ */
92
+ protected abstract getSkillTargetDir(skillName: string): string;
93
+
80
94
  protected getBody(id: SlashCommandId): string {
81
95
  return TemplateManager.getSlashCommandBody(id).trim();
82
96
  }
83
97
 
98
+ // --- Skill directory management ---
99
+
100
+ getSkillTargets(skills: SkillMetadata[]): SkillTarget[] {
101
+ return skills.map(skill => ({
102
+ name: skill.name,
103
+ path: this.getSkillTargetDir(skill.name),
104
+ kind: 'skill'
105
+ }));
106
+ }
107
+
108
+ async generateSkills(projectPath: string, skills: SkillMetadata[]): Promise<string[]> {
109
+ const created: string[] = [];
110
+
111
+ for (const skill of skills) {
112
+ const targetDir = path.join(projectPath, this.getSkillTargetDir(skill.name));
113
+ await this.copyDir(skill.sourcePath, targetDir);
114
+ created.push(this.getSkillTargetDir(skill.name));
115
+ }
116
+
117
+ return created;
118
+ }
119
+
120
+ async updateExistingSkills(projectPath: string, skills: SkillMetadata[]): Promise<string[]> {
121
+ const updated: string[] = [];
122
+
123
+ for (const skill of skills) {
124
+ const relativeDir = this.getSkillTargetDir(skill.name);
125
+ const targetDir = path.join(projectPath, relativeDir);
126
+
127
+ if (await this.dirExists(targetDir)) {
128
+ await this.copyDir(skill.sourcePath, targetDir);
129
+ updated.push(relativeDir);
130
+ }
131
+ }
132
+
133
+ return updated;
134
+ }
135
+
136
+ async removeSkillFiles(projectPath: string, skillNames: string[]): Promise<string[]> {
137
+ const removed: string[] = [];
138
+
139
+ for (const name of skillNames) {
140
+ const relativeDir = this.getSkillTargetDir(name);
141
+ const targetDir = path.join(projectPath, relativeDir);
142
+
143
+ if (await this.dirExists(targetDir)) {
144
+ await fs.rm(targetDir, { recursive: true, force: true });
145
+ removed.push(relativeDir);
146
+ }
147
+ }
148
+
149
+ return removed;
150
+ }
151
+
152
+ private async copyDir(src: string, dest: string): Promise<void> {
153
+ await fs.mkdir(dest, { recursive: true });
154
+ const entries = await fs.readdir(src, { withFileTypes: true });
155
+
156
+ for (const entry of entries) {
157
+ const srcPath = path.join(src, entry.name);
158
+ const destPath = path.join(dest, entry.name);
159
+
160
+ if (entry.isDirectory()) {
161
+ await this.copyDir(srcPath, destPath);
162
+ } else {
163
+ await fs.copyFile(srcPath, destPath);
164
+ }
165
+ }
166
+ }
167
+
168
+ private async dirExists(dirPath: string): Promise<boolean> {
169
+ try {
170
+ const stat = await fs.stat(dirPath);
171
+ return stat.isDirectory();
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
176
+
177
+ // --- Shared helpers ---
178
+
84
179
  protected async updateBody(filePath: string, body: string): Promise<void> {
85
180
  const content = await fs.readFile(filePath, 'utf-8');
86
181
  const startIndex = content.indexOf(PROMPTER_MARKERS.start);
@@ -63,4 +63,8 @@ export class ClaudeConfigurator extends SlashCommandConfigurator {
63
63
  // Claude Code uses the filename as the command name
64
64
  return undefined;
65
65
  }
66
+
67
+ protected getSkillTargetDir(skillName: string): string {
68
+ return `.claude/skills/${skillName}`;
69
+ }
66
70
  }
@@ -63,4 +63,8 @@ export class CodexConfigurator extends SlashCommandConfigurator {
63
63
  const description = DESCRIPTIONS[id];
64
64
  return `---\ndescription: ${description}\n---`;
65
65
  }
66
+
67
+ protected getSkillTargetDir(skillName: string): string {
68
+ return `.codex/skills/${skillName}`;
69
+ }
66
70
  }
@@ -37,4 +37,8 @@ export class DroidConfigurator extends SlashCommandConfigurator {
37
37
  protected getFrontmatter(id: SlashCommandId): string | undefined {
38
38
  return undefined;
39
39
  }
40
+
41
+ protected getSkillTargetDir(skillName: string): string {
42
+ return `.factory/skills/${skillName}`;
43
+ }
40
44
  }
@@ -37,4 +37,8 @@ export class ForgeConfigurator extends SlashCommandConfigurator {
37
37
  protected getFrontmatter(id: SlashCommandId): string | undefined {
38
38
  return undefined;
39
39
  }
40
+
41
+ protected getSkillTargetDir(skillName: string): string {
42
+ return `.forge/skills/${skillName}`;
43
+ }
40
44
  }
@@ -99,6 +99,10 @@ export class GithubCopilotConfigurator extends SlashCommandConfigurator {
99
99
  return createdOrUpdated;
100
100
  }
101
101
 
102
+ protected getSkillTargetDir(skillName: string): string {
103
+ return `.github/skills/${skillName}`;
104
+ }
105
+
102
106
  private async checkFileExists(filePath: string): Promise<boolean> {
103
107
  try {
104
108
  await fs.access(filePath);
@@ -63,4 +63,8 @@ export class KiloCodeConfigurator extends SlashCommandConfigurator {
63
63
  const description = DESCRIPTIONS[id];
64
64
  return `---\ndescription: ${description}\n---`;
65
65
  }
66
+
67
+ protected getSkillTargetDir(skillName: string): string {
68
+ return `.kilocode/skills/${skillName}`;
69
+ }
66
70
  }
@@ -63,4 +63,8 @@ export class OpenCodeConfigurator extends SlashCommandConfigurator {
63
63
  const description = DESCRIPTIONS[id];
64
64
  return `---\nagent: build\ndescription: ${description}\n---`;
65
65
  }
66
+
67
+ protected getSkillTargetDir(skillName: string): string {
68
+ return `.opencode/skills/${skillName}`;
69
+ }
66
70
  }
@@ -0,0 +1,68 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'yaml';
4
+
5
+ export interface SkillMetadata {
6
+ name: string;
7
+ description: string;
8
+ sourcePath: string;
9
+ body: string;
10
+ }
11
+
12
+ /**
13
+ * Discover skills in a directory by scanning for subdirectories containing SKILL.md files.
14
+ * Parses YAML frontmatter (name, description) and extracts the markdown body.
15
+ */
16
+ export async function discoverSkills(skillsDir: string): Promise<SkillMetadata[]> {
17
+ const skills: SkillMetadata[] = [];
18
+
19
+ let entries: import('fs').Dirent[];
20
+ try {
21
+ entries = await fs.readdir(skillsDir, { withFileTypes: true });
22
+ } catch {
23
+ return skills;
24
+ }
25
+
26
+ for (const entry of entries) {
27
+ if (!entry.isDirectory()) continue;
28
+
29
+ const skillDir = path.join(skillsDir, entry.name);
30
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
31
+
32
+ try {
33
+ const content = await fs.readFile(skillMdPath, 'utf-8');
34
+ const parsed = parseSkillMd(content);
35
+
36
+ if (parsed) {
37
+ skills.push({
38
+ name: parsed.name,
39
+ description: parsed.description,
40
+ sourcePath: skillDir,
41
+ body: parsed.body
42
+ });
43
+ }
44
+ } catch {
45
+ // SKILL.md not found or unreadable, skip this directory
46
+ }
47
+ }
48
+
49
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
50
+ }
51
+
52
+ function parseSkillMd(content: string): { name: string; description: string; body: string } | null {
53
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
54
+ if (!match) return null;
55
+
56
+ try {
57
+ const meta = yaml.parse(match[1]);
58
+ if (!meta.name || !meta.description) return null;
59
+
60
+ return {
61
+ name: String(meta.name),
62
+ description: String(meta.description),
63
+ body: match[2].trim()
64
+ };
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
@@ -1,7 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npx tsc:*)"
5
- ]
6
- }
7
- }
@@ -1,133 +0,0 @@
1
- ---
2
- description: Generate a PRD with autonomous assumptions (non-interactive mode)
3
- ---
4
- $ARGUMENTS
5
- <!-- prompter-managed-start -->
6
- # PRD Generator (Non-Interactive Mode)
7
-
8
- Create detailed Product Requirements Documents that are clear, actionable, and suitable for implementation based solely on the user's initial input.
9
-
10
- ---
11
-
12
- ## The Job
13
-
14
- 1. Receive a feature description from the user
15
- 2. Analyze the input and make reasonable assumptions where details are missing
16
- 3. Generate a structured PRD based on the input
17
-
18
- ---
19
-
20
- ## Handling Ambiguity
21
-
22
- When the user's input lacks specific details:
23
-
24
- - **Make reasonable assumptions** based on common patterns and best practices
25
- - **Document assumptions** in the PRD under "Assumptions Made"
26
- - **Flag critical unknowns** in the "Open Questions" section
27
- - **Err on the side of MVP scope** when scope is unclear
28
- - **Default to standard patterns** (e.g., CRUD operations, standard UI components)
29
-
30
- ---
31
-
32
- ## PRD Structure
33
-
34
- Generate the PRD with these sections:
35
-
36
- ### 1. Introduction/Overview
37
- Brief description of the feature and the problem it solves.
38
-
39
- ### 2. Assumptions Made
40
- List key assumptions made due to missing details in the original request:
41
- - "Assumed target users are [X] based on feature context"
42
- - "Assumed MVP scope since no specific scope mentioned"
43
- - "Assumed standard authentication is already in place"
44
-
45
- ### 3. Goals
46
- Specific, measurable objectives (bullet list).
47
-
48
- ### 4. User Stories
49
- Each story needs:
50
- - **Title:** Short descriptive name
51
- - **Description:** "As a [user], I want [feature] so that [benefit]"
52
- - **Acceptance Criteria:** Verifiable checklist of what "done" means
53
-
54
- Each story should be small enough to implement in one focused session.
55
-
56
- **Format:**
57
- ```markdown
58
- ### US-001: [Title]
59
- **Description:** As a [user], I want [feature] so that [benefit].
60
-
61
- **Acceptance Criteria:**
62
- - [ ] Specific verifiable criterion
63
- - [ ] Another criterion
64
- - [ ] Typecheck/lint passes
65
- - [ ] **[UI stories only]** Verify in browser using dev-browser skill
66
- ```
67
-
68
- **Important:**
69
- - Acceptance criteria must be verifiable, not vague. "Works correctly" is bad. "Button shows confirmation dialog before deleting" is good.
70
- - **For any story with UI changes:** Always include "Verify in browser using dev-browser skill" as acceptance criteria. This ensures visual verification of frontend work.
71
-
72
- ### 5. Functional Requirements
73
- Numbered list of specific functionalities:
74
- - "FR-1: The system must allow users to..."
75
- - "FR-2: When a user clicks X, the system must..."
76
-
77
- Be explicit and unambiguous.
78
-
79
- ### 6. Non-Goals (Out of Scope)
80
- What this feature will NOT include. Critical for managing scope.
81
-
82
- ### 7. Design Considerations (Optional)
83
- - UI/UX requirements
84
- - Link to mockups if available
85
- - Relevant existing components to reuse
86
-
87
- ### 8. Technical Considerations (Optional)
88
- - Known constraints or dependencies
89
- - Integration points with existing systems
90
- - Performance requirements
91
-
92
- ### 9. Success Metrics
93
- How will success be measured?
94
- - "Reduce time to complete X by 50%"
95
- - "Increase conversion rate by 10%"
96
-
97
- ### 10. Open Questions
98
- Remaining questions or areas needing clarification. This is where you document:
99
- - Critical unknowns that affect implementation
100
- - Areas where the original request was ambiguous
101
- - Decisions that may need stakeholder input
102
-
103
- ---
104
-
105
- ## Writing for Junior Developers
106
-
107
- The PRD reader may be a junior developer or AI agent. Therefore:
108
-
109
- - Be explicit and unambiguous
110
- - Avoid jargon or explain it
111
- - Provide enough detail to understand purpose and core logic
112
- - Number requirements for easy reference
113
- - Use concrete examples where helpful
114
-
115
- ---
116
-
117
- ## Output
118
-
119
- - **Format:** Markdown (`.md`)
120
-
121
- ---
122
-
123
- ## WORKFLOW STEPS
124
- 1. Read the user's input about the feature
125
- 2. Generate a unique, URL-friendly slug from the feature name (lowercase, hyphen-separated)
126
- 3. Create the directory `prompter/<slug>/` if it doesn't exist
127
- 4. Generate the complete PRD following all requirements above
128
- 5. Save the PRD to `prompter/<slug>/prd-agent.md`
129
- 6. Report the saved file path
130
-
131
- ## REFERENCE
132
- - Read `prompter/project.md` for project context if needed
133
- <!-- prompter-managed-end -->