bmad-module-skill-forge 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nvmrc +1 -0
- package/LICENSE +26 -0
- package/README.md +213 -0
- package/docs/404.md +20 -0
- package/docs/agents.md +73 -0
- package/docs/architecture.md +301 -0
- package/docs/examples.md +144 -0
- package/docs/getting-started.md +198 -0
- package/docs/index.md +75 -0
- package/docs/workflows.md +190 -0
- package/package.json +109 -0
- package/src/agents/forger.agent.yaml +93 -0
- package/src/forger/README.md +17 -0
- package/src/forger/forge-tier.yaml +13 -0
- package/src/forger/preferences.yaml +10 -0
- package/src/knowledge/agentskills-spec.md +158 -0
- package/src/knowledge/confidence-tiers.md +153 -0
- package/src/knowledge/manual-section-integrity.md +156 -0
- package/src/knowledge/overview.md +56 -0
- package/src/knowledge/progressive-capability.md +120 -0
- package/src/knowledge/provenance-tracking.md +163 -0
- package/src/knowledge/skf-knowledge-index.csv +9 -0
- package/src/knowledge/skill-lifecycle.md +150 -0
- package/src/knowledge/zero-hallucination.md +101 -0
- package/src/module-help.csv +12 -0
- package/src/module.yaml +22 -0
- package/src/workflows/README.md +148 -0
- package/src/workflows/analyze-source/data/skill-brief-schema.md +75 -0
- package/src/workflows/analyze-source/data/unit-detection-heuristics.md +88 -0
- package/src/workflows/analyze-source/steps-c/step-01-init.md +191 -0
- package/src/workflows/analyze-source/steps-c/step-01b-continue.md +144 -0
- package/src/workflows/analyze-source/steps-c/step-02-scan-project.md +208 -0
- package/src/workflows/analyze-source/steps-c/step-03-identify-units.md +199 -0
- package/src/workflows/analyze-source/steps-c/step-04-map-and-detect.md +222 -0
- package/src/workflows/analyze-source/steps-c/step-05-recommend.md +236 -0
- package/src/workflows/analyze-source/steps-c/step-06-generate-briefs.md +239 -0
- package/src/workflows/analyze-source/templates/analysis-report-template.md +41 -0
- package/src/workflows/analyze-source/validation-report.md +657 -0
- package/src/workflows/analyze-source/workflow-plan-analyze-source.md +385 -0
- package/src/workflows/analyze-source/workflow.md +61 -0
- package/src/workflows/audit-skill/data/drift-report-template.md +62 -0
- package/src/workflows/audit-skill/data/severity-rules.md +52 -0
- package/src/workflows/audit-skill/steps-c/step-01-init.md +195 -0
- package/src/workflows/audit-skill/steps-c/step-02-re-index.md +191 -0
- package/src/workflows/audit-skill/steps-c/step-03-structural-diff.md +194 -0
- package/src/workflows/audit-skill/steps-c/step-04-semantic-diff.md +199 -0
- package/src/workflows/audit-skill/steps-c/step-05-severity-classify.md +210 -0
- package/src/workflows/audit-skill/steps-c/step-06-report.md +217 -0
- package/src/workflows/audit-skill/validation-report.md +545 -0
- package/src/workflows/audit-skill/workflow-plan-audit-skill.md +318 -0
- package/src/workflows/audit-skill/workflow.md +63 -0
- package/src/workflows/brief-skill/data/skill-brief-schema.md +82 -0
- package/src/workflows/brief-skill/steps-c/step-01-gather-intent.md +184 -0
- package/src/workflows/brief-skill/steps-c/step-02-analyze-target.md +203 -0
- package/src/workflows/brief-skill/steps-c/step-03-scope-definition.md +225 -0
- package/src/workflows/brief-skill/steps-c/step-04-confirm-brief.md +185 -0
- package/src/workflows/brief-skill/steps-c/step-05-write-brief.md +156 -0
- package/src/workflows/brief-skill/validation-report.md +630 -0
- package/src/workflows/brief-skill/workflow-plan-brief-skill.md +360 -0
- package/src/workflows/brief-skill/workflow.md +57 -0
- package/src/workflows/create-skill/data/extraction-patterns.md +63 -0
- package/src/workflows/create-skill/data/skill-sections.md +166 -0
- package/src/workflows/create-skill/steps-c/step-01-load-brief.md +176 -0
- package/src/workflows/create-skill/steps-c/step-02-ecosystem-check.md +143 -0
- package/src/workflows/create-skill/steps-c/step-03-extract.md +184 -0
- package/src/workflows/create-skill/steps-c/step-04-enrich.md +152 -0
- package/src/workflows/create-skill/steps-c/step-05-compile.md +198 -0
- package/src/workflows/create-skill/steps-c/step-06-validate.md +162 -0
- package/src/workflows/create-skill/steps-c/step-07-generate-artifacts.md +165 -0
- package/src/workflows/create-skill/steps-c/step-08-report.md +160 -0
- package/src/workflows/create-skill/validation-report.md +189 -0
- package/src/workflows/create-skill/workflow-plan-create-skill.md +491 -0
- package/src/workflows/create-skill/workflow.md +52 -0
- package/src/workflows/create-stack-skill/data/integration-patterns.md +61 -0
- package/src/workflows/create-stack-skill/data/manifest-patterns.md +35 -0
- package/src/workflows/create-stack-skill/data/stack-skill-template.md +110 -0
- package/src/workflows/create-stack-skill/steps-c/step-01-init.md +142 -0
- package/src/workflows/create-stack-skill/steps-c/step-02-detect-manifests.md +145 -0
- package/src/workflows/create-stack-skill/steps-c/step-03-rank-and-confirm.md +161 -0
- package/src/workflows/create-stack-skill/steps-c/step-04-parallel-extract.md +155 -0
- package/src/workflows/create-stack-skill/steps-c/step-05-detect-integrations.md +154 -0
- package/src/workflows/create-stack-skill/steps-c/step-06-compile-stack.md +160 -0
- package/src/workflows/create-stack-skill/steps-c/step-07-generate-output.md +202 -0
- package/src/workflows/create-stack-skill/steps-c/step-08-validate.md +193 -0
- package/src/workflows/create-stack-skill/steps-c/step-09-report.md +137 -0
- package/src/workflows/create-stack-skill/validation-report.md +401 -0
- package/src/workflows/create-stack-skill/workflow-plan-create-stack-skill.md +481 -0
- package/src/workflows/create-stack-skill/workflow.md +59 -0
- package/src/workflows/export-skill/data/managed-section-format.md +70 -0
- package/src/workflows/export-skill/data/snippet-format.md +32 -0
- package/src/workflows/export-skill/steps-c/step-01-load-skill.md +184 -0
- package/src/workflows/export-skill/steps-c/step-02-package.md +153 -0
- package/src/workflows/export-skill/steps-c/step-03-generate-snippet.md +166 -0
- package/src/workflows/export-skill/steps-c/step-04-update-context.md +222 -0
- package/src/workflows/export-skill/steps-c/step-05-token-report.md +126 -0
- package/src/workflows/export-skill/steps-c/step-06-summary.md +167 -0
- package/src/workflows/export-skill/validation-report.md +263 -0
- package/src/workflows/export-skill/workflow-plan-export-skill.md +369 -0
- package/src/workflows/export-skill/workflow.md +56 -0
- package/src/workflows/quick-skill/data/registry-resolution.md +91 -0
- package/src/workflows/quick-skill/data/skill-template.md +66 -0
- package/src/workflows/quick-skill/steps-c/step-01-resolve-target.md +165 -0
- package/src/workflows/quick-skill/steps-c/step-02-ecosystem-check.md +143 -0
- package/src/workflows/quick-skill/steps-c/step-03-quick-extract.md +177 -0
- package/src/workflows/quick-skill/steps-c/step-04-compile.md +180 -0
- package/src/workflows/quick-skill/steps-c/step-05-validate.md +150 -0
- package/src/workflows/quick-skill/steps-c/step-06-write.md +160 -0
- package/src/workflows/quick-skill/validation-report.md +543 -0
- package/src/workflows/quick-skill/workflow-plan-quick-skill.md +405 -0
- package/src/workflows/quick-skill/workflow.md +56 -0
- package/src/workflows/setup-forge/data/tier-rules.md +50 -0
- package/src/workflows/setup-forge/steps-c/step-01-detect-and-tier.md +140 -0
- package/src/workflows/setup-forge/steps-c/step-02-write-config.md +141 -0
- package/src/workflows/setup-forge/steps-c/step-03-auto-index.md +116 -0
- package/src/workflows/setup-forge/steps-c/step-04-report.md +131 -0
- package/src/workflows/setup-forge/validation-report.md +504 -0
- package/src/workflows/setup-forge/workflow-plan-setup-forge.md +428 -0
- package/src/workflows/setup-forge/workflow.md +49 -0
- package/src/workflows/test-skill/data/output-section-formats.md +96 -0
- package/src/workflows/test-skill/data/scoring-rules.md +74 -0
- package/src/workflows/test-skill/steps-c/step-01-init.md +185 -0
- package/src/workflows/test-skill/steps-c/step-02-detect-mode.md +162 -0
- package/src/workflows/test-skill/steps-c/step-03-coverage-check.md +222 -0
- package/src/workflows/test-skill/steps-c/step-04-coherence-check.md +249 -0
- package/src/workflows/test-skill/steps-c/step-05-score.md +222 -0
- package/src/workflows/test-skill/steps-c/step-06-report.md +175 -0
- package/src/workflows/test-skill/templates/test-report-template.md +24 -0
- package/src/workflows/test-skill/validation-report.md +390 -0
- package/src/workflows/test-skill/workflow-plan-test-skill.md +288 -0
- package/src/workflows/test-skill/workflow.md +57 -0
- package/src/workflows/update-skill/data/manual-section-rules.md +44 -0
- package/src/workflows/update-skill/data/merge-conflict-rules.md +62 -0
- package/src/workflows/update-skill/steps-c/step-01-init.md +208 -0
- package/src/workflows/update-skill/steps-c/step-02-detect-changes.md +184 -0
- package/src/workflows/update-skill/steps-c/step-03-re-extract.md +187 -0
- package/src/workflows/update-skill/steps-c/step-04-merge.md +230 -0
- package/src/workflows/update-skill/steps-c/step-05-validate.md +187 -0
- package/src/workflows/update-skill/steps-c/step-06-write.md +204 -0
- package/src/workflows/update-skill/steps-c/step-07-report.md +201 -0
- package/src/workflows/update-skill/validation-report.md +559 -0
- package/src/workflows/update-skill/workflow-plan-update-skill.md +441 -0
- package/src/workflows/update-skill/workflow.md +56 -0
- package/tools/cli/commands/install.js +33 -0
- package/tools/cli/commands/status.js +195 -0
- package/tools/cli/commands/uninstall.js +226 -0
- package/tools/cli/commands/update.js +63 -0
- package/tools/cli/lib/compiler.js +306 -0
- package/tools/cli/lib/ide-commands.js +244 -0
- package/tools/cli/lib/installer.js +307 -0
- package/tools/cli/lib/manifest.js +109 -0
- package/tools/cli/lib/ui.js +277 -0
- package/tools/cli/lib/version-check.js +86 -0
- package/tools/cli/skf-cli.js +45 -0
- package/tools/skf-npx-wrapper.js +36 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKF Installer - Core orchestrator
|
|
3
|
+
* Copies SKF source files, compiles agents, creates folder structure.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const { spinner } = require('@clack/prompts');
|
|
9
|
+
const yaml = require('js-yaml');
|
|
10
|
+
const { compileAgentFile } = require('./compiler');
|
|
11
|
+
const { generateIdeCommands } = require('./ide-commands');
|
|
12
|
+
const { writeManifest } = require('./manifest');
|
|
13
|
+
|
|
14
|
+
class Installer {
|
|
15
|
+
constructor() {
|
|
16
|
+
// Resolve directories relative to this file (tools/cli/lib/ -> up 3 levels)
|
|
17
|
+
const repoRoot = path.resolve(__dirname, '..', '..', '..');
|
|
18
|
+
this.srcDir = path.join(repoRoot, 'src');
|
|
19
|
+
this.docsDir = path.join(repoRoot, 'docs');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async install(config) {
|
|
23
|
+
const { projectDir, skfFolder } = config;
|
|
24
|
+
const skfDir = path.join(projectDir, skfFolder);
|
|
25
|
+
const action = config._action || 'fresh';
|
|
26
|
+
const s = spinner();
|
|
27
|
+
|
|
28
|
+
// Handle update vs fresh for existing installation
|
|
29
|
+
if (action === 'update' && (await fs.pathExists(skfDir))) {
|
|
30
|
+
const configPath = path.join(skfDir, 'config.yaml');
|
|
31
|
+
if (!config._savedConfigYaml && (await fs.pathExists(configPath))) {
|
|
32
|
+
config._savedConfigYaml = await fs.readFile(configPath, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// On update, extract settings from saved config
|
|
36
|
+
if (config._savedConfigYaml) {
|
|
37
|
+
try {
|
|
38
|
+
const savedData = yaml.load(config._savedConfigYaml);
|
|
39
|
+
if (!config.ides && savedData.ides) config.ides = savedData.ides;
|
|
40
|
+
if (!config.skills_output_folder && savedData.skills_output_folder) config.skills_output_folder = savedData.skills_output_folder;
|
|
41
|
+
if (!config.forge_data_folder && savedData.forge_data_folder) config.forge_data_folder = savedData.forge_data_folder;
|
|
42
|
+
} catch {
|
|
43
|
+
/* ignore parse errors, defaults will apply */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
s.start('Updating SKF files...');
|
|
48
|
+
await fs.remove(skfDir);
|
|
49
|
+
s.stop('Old files cleared');
|
|
50
|
+
} else if (action === 'fresh' && (await fs.pathExists(skfDir))) {
|
|
51
|
+
s.start('Removing existing SKF installation...');
|
|
52
|
+
await fs.remove(skfDir);
|
|
53
|
+
s.stop('Old installation removed');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Ensure parent directory exists (for _bmad/skf/)
|
|
57
|
+
await fs.ensureDir(path.dirname(skfDir));
|
|
58
|
+
|
|
59
|
+
// Step 1: Copy source files
|
|
60
|
+
s.start('Copying SKF files...');
|
|
61
|
+
try {
|
|
62
|
+
await this.copySrcFiles(skfDir);
|
|
63
|
+
s.stop('SKF files copied');
|
|
64
|
+
} catch (error) {
|
|
65
|
+
s.stop('Failed to copy SKF files');
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Step 2: Setup agent sidecar
|
|
70
|
+
s.start('Setting up agent sidecar...');
|
|
71
|
+
try {
|
|
72
|
+
await this.setupSidecar(projectDir);
|
|
73
|
+
s.stop('Agent sidecar initialized');
|
|
74
|
+
} catch (error) {
|
|
75
|
+
s.stop('Failed to setup sidecar');
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Step 2b: Update .gitignore
|
|
80
|
+
await this.updateGitignore(projectDir);
|
|
81
|
+
|
|
82
|
+
// Step 3: Write config.yaml
|
|
83
|
+
s.start('Writing configuration...');
|
|
84
|
+
try {
|
|
85
|
+
await this.writeConfig(skfDir, config);
|
|
86
|
+
s.stop('Configuration saved');
|
|
87
|
+
} catch (error) {
|
|
88
|
+
s.stop('Failed to write configuration');
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Step 4: Compile agents
|
|
93
|
+
s.start('Compiling agents...');
|
|
94
|
+
try {
|
|
95
|
+
const agents = await this.compileAgents(skfDir, skfFolder);
|
|
96
|
+
s.stop(`Compiled ${agents.length} agent${agents.length === 1 ? '' : 's'}`);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
s.stop('Failed to compile agents');
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Step 5: Create output folders
|
|
103
|
+
s.start('Creating project folders...');
|
|
104
|
+
try {
|
|
105
|
+
await this.createOutputFolders(projectDir, config);
|
|
106
|
+
s.stop('Project folders created');
|
|
107
|
+
} catch (error) {
|
|
108
|
+
s.stop('Failed to create project folders');
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Step 6: Copy learning material (optional)
|
|
113
|
+
if (config.install_learning !== false) {
|
|
114
|
+
s.start('Copying learning & reference material...');
|
|
115
|
+
try {
|
|
116
|
+
await this.copyLearningMaterial(projectDir);
|
|
117
|
+
s.stop('Learning material added to _skf-learn/');
|
|
118
|
+
} catch (error) {
|
|
119
|
+
s.stop('Failed to copy learning material');
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Step 7: Generate IDE command files
|
|
125
|
+
let ideFiles = [];
|
|
126
|
+
const selectedIdes = config.ides || [];
|
|
127
|
+
if (selectedIdes.length > 0 && !selectedIdes.every((ide) => ide === 'other')) {
|
|
128
|
+
s.start('Generating IDE commands...');
|
|
129
|
+
try {
|
|
130
|
+
const ideResult = await generateIdeCommands(projectDir, skfFolder, selectedIdes);
|
|
131
|
+
ideFiles = ideResult.files || [];
|
|
132
|
+
if (ideResult.generated > 0) {
|
|
133
|
+
s.stop(`IDE commands generated for ${ideResult.ides.join(', ')}`);
|
|
134
|
+
} else {
|
|
135
|
+
s.stop('No IDE commands to generate');
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
s.stop('Failed to generate IDE commands');
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Step 8: Write installation manifest
|
|
144
|
+
s.start('Writing manifest...');
|
|
145
|
+
try {
|
|
146
|
+
const packageJson = require('../../../package.json');
|
|
147
|
+
await writeManifest(projectDir, config, {
|
|
148
|
+
version: packageJson.version,
|
|
149
|
+
ideFiles,
|
|
150
|
+
});
|
|
151
|
+
s.stop('Installation manifest saved');
|
|
152
|
+
} catch (error) {
|
|
153
|
+
s.stop('Failed to write manifest');
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { success: true, skfDir, projectDir };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Copy src/ content into the target SKF directory.
|
|
162
|
+
* Workflows are placed under a skillforge/ subdirectory to match agent path references.
|
|
163
|
+
*/
|
|
164
|
+
async copySrcFiles(skfDir) {
|
|
165
|
+
// Copy agents and knowledge directly
|
|
166
|
+
for (const dir of ['agents', 'knowledge']) {
|
|
167
|
+
const src = path.join(this.srcDir, dir);
|
|
168
|
+
const dest = path.join(skfDir, dir);
|
|
169
|
+
if (await fs.pathExists(src)) {
|
|
170
|
+
await fs.copy(src, dest);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Workflows go under workflows/skillforge/ to match agent YAML paths
|
|
175
|
+
const workflowsSrc = path.join(this.srcDir, 'workflows');
|
|
176
|
+
const workflowsDest = path.join(skfDir, 'workflows', 'skillforge');
|
|
177
|
+
if (await fs.pathExists(workflowsSrc)) {
|
|
178
|
+
await fs.copy(workflowsSrc, workflowsDest);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Copy module.yaml and module-help.csv
|
|
182
|
+
for (const file of ['module.yaml', 'module-help.csv']) {
|
|
183
|
+
const src = path.join(this.srcDir, file);
|
|
184
|
+
if (await fs.pathExists(src)) {
|
|
185
|
+
await fs.copy(src, path.join(skfDir, file));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Setup the ferris sidecar directory with template files.
|
|
192
|
+
* Existing sidecar files are preserved (they contain user state).
|
|
193
|
+
*/
|
|
194
|
+
async setupSidecar(projectDir) {
|
|
195
|
+
const sidecarDir = path.join(projectDir, '_bmad', '_memory', 'forger-sidecar');
|
|
196
|
+
await fs.ensureDir(sidecarDir);
|
|
197
|
+
|
|
198
|
+
const forgerSrc = path.join(this.srcDir, 'forger');
|
|
199
|
+
if (await fs.pathExists(forgerSrc)) {
|
|
200
|
+
const files = await fs.readdir(forgerSrc);
|
|
201
|
+
for (const file of files) {
|
|
202
|
+
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
203
|
+
const dest = path.join(sidecarDir, file);
|
|
204
|
+
// Don't overwrite existing sidecar files (preserves user state)
|
|
205
|
+
if (!(await fs.pathExists(dest))) {
|
|
206
|
+
await fs.copy(path.join(forgerSrc, file), dest);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async writeConfig(skfDir, config) {
|
|
214
|
+
// On update, restore the user's existing config
|
|
215
|
+
if (config._savedConfigYaml) {
|
|
216
|
+
await fs.writeFile(path.join(skfDir, 'config.yaml'), config._savedConfigYaml, 'utf8');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Get user name from git or system
|
|
221
|
+
const getUserName = () => {
|
|
222
|
+
try {
|
|
223
|
+
const { execSync } = require('node:child_process');
|
|
224
|
+
return execSync('git config user.name', { encoding: 'utf8' }).trim() || 'Developer';
|
|
225
|
+
} catch {
|
|
226
|
+
return 'Developer';
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const configData = {
|
|
231
|
+
user_name: getUserName(),
|
|
232
|
+
project_name: config.project_name || 'Untitled Project',
|
|
233
|
+
communication_language: 'en',
|
|
234
|
+
document_output_language: 'en',
|
|
235
|
+
skills_output_folder: config.skills_output_folder || 'skills',
|
|
236
|
+
forge_data_folder: config.forge_data_folder || 'forge-data',
|
|
237
|
+
skf_folder: config.skfFolder,
|
|
238
|
+
ides: config.ides || [],
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const yamlStr = yaml.dump(configData, { lineWidth: -1 });
|
|
242
|
+
await fs.writeFile(path.join(skfDir, 'config.yaml'), `# SKF Configuration - Generated by installer\n${yamlStr}`, 'utf8');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async compileAgents(skfDir, skfFolder) {
|
|
246
|
+
const agentsDir = path.join(skfDir, 'agents');
|
|
247
|
+
const files = await fs.readdir(agentsDir);
|
|
248
|
+
const agentFiles = files.filter((f) => f.endsWith('.agent.yaml'));
|
|
249
|
+
const results = [];
|
|
250
|
+
|
|
251
|
+
for (const file of agentFiles) {
|
|
252
|
+
const yamlPath = path.join(agentsDir, file);
|
|
253
|
+
const result = compileAgentFile(yamlPath, { skfFolder });
|
|
254
|
+
results.push(result);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return results;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async createOutputFolders(projectDir, config) {
|
|
261
|
+
const skillsFolder = config.skills_output_folder || 'skills';
|
|
262
|
+
const forgeDataFolder = config.forge_data_folder || 'forge-data';
|
|
263
|
+
|
|
264
|
+
for (const folder of [skillsFolder, forgeDataFolder]) {
|
|
265
|
+
const folderPath = path.join(projectDir, folder);
|
|
266
|
+
if (!(await fs.pathExists(folderPath))) {
|
|
267
|
+
await fs.ensureDir(folderPath);
|
|
268
|
+
await fs.writeFile(path.join(folderPath, '.gitkeep'), '# This file ensures the directory is tracked by git\n');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async updateGitignore(projectDir) {
|
|
274
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
275
|
+
const entry = '_bmad/_memory/';
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
279
|
+
const content = await fs.readFile(gitignorePath, 'utf8');
|
|
280
|
+
// Check if entry already present (exact line match)
|
|
281
|
+
const lines = content.split('\n');
|
|
282
|
+
if (lines.some((line) => line.trim() === entry)) return;
|
|
283
|
+
// Append with preceding newline if file doesn't end with one
|
|
284
|
+
const prefix = content.endsWith('\n') ? '' : '\n';
|
|
285
|
+
await fs.appendFile(gitignorePath, `${prefix}${entry}\n`, 'utf8');
|
|
286
|
+
} else {
|
|
287
|
+
await fs.writeFile(gitignorePath, `${entry}\n`, 'utf8');
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
// Non-critical — don't fail the install over .gitignore
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async copyLearningMaterial(projectDir) {
|
|
295
|
+
const learnDir = path.join(projectDir, '_skf-learn');
|
|
296
|
+
if (await fs.pathExists(this.docsDir)) {
|
|
297
|
+
await fs.copy(this.docsDir, learnDir, {
|
|
298
|
+
filter: (srcPath) => {
|
|
299
|
+
// Skip website-specific files
|
|
300
|
+
return !path.basename(srcPath).startsWith('404');
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = { Installer };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKF Manifest - Tracks installed files for uninstall and smart updates.
|
|
3
|
+
* Writes _bmad/_config/manifest.yaml after each install.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const yaml = require('js-yaml');
|
|
9
|
+
|
|
10
|
+
const MANIFEST_DIR = '_bmad/_config';
|
|
11
|
+
const MANIFEST_FILE = 'skf-manifest.yaml';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Recursively collect all file paths under a directory,
|
|
15
|
+
* returned as paths relative to projectDir.
|
|
16
|
+
*/
|
|
17
|
+
async function collectFiles(dir, projectDir) {
|
|
18
|
+
const files = [];
|
|
19
|
+
if (!(await fs.pathExists(dir))) return files;
|
|
20
|
+
|
|
21
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const fullPath = path.join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
const subFiles = await collectFiles(fullPath, projectDir);
|
|
26
|
+
files.push(...subFiles);
|
|
27
|
+
} else {
|
|
28
|
+
files.push(path.relative(projectDir, fullPath));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build and write the manifest after installation.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} projectDir - Project root
|
|
38
|
+
* @param {object} config - Install config (skfFolder, ides, etc.)
|
|
39
|
+
* @param {object} options - Additional info (version, ideFiles)
|
|
40
|
+
*/
|
|
41
|
+
async function writeManifest(projectDir, config, options = {}) {
|
|
42
|
+
const skfFolder = config.skfFolder || '_bmad/skf';
|
|
43
|
+
const skfDir = path.join(projectDir, skfFolder);
|
|
44
|
+
const sidecarDir = path.join(projectDir, '_bmad', '_memory', 'forger-sidecar');
|
|
45
|
+
|
|
46
|
+
// Collect all installed files
|
|
47
|
+
const skfFiles = await collectFiles(skfDir, projectDir);
|
|
48
|
+
const sidecarFiles = await collectFiles(sidecarDir, projectDir);
|
|
49
|
+
|
|
50
|
+
// Collect IDE command files
|
|
51
|
+
const ideFiles = options.ideFiles || [];
|
|
52
|
+
|
|
53
|
+
// Collect learning material files
|
|
54
|
+
const learnDir = path.join(projectDir, '_skf-learn');
|
|
55
|
+
const learnFiles = await collectFiles(learnDir, projectDir);
|
|
56
|
+
|
|
57
|
+
// Collect output folder gitkeep files
|
|
58
|
+
const outputFiles = [];
|
|
59
|
+
for (const folder of [config.skills_output_folder || 'skills', config.forge_data_folder || 'forge-data']) {
|
|
60
|
+
const gitkeep = path.join(folder, '.gitkeep');
|
|
61
|
+
if (await fs.pathExists(path.join(projectDir, gitkeep))) {
|
|
62
|
+
outputFiles.push(gitkeep);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const manifest = {
|
|
67
|
+
version: options.version || 'unknown',
|
|
68
|
+
installed_at: new Date().toISOString(),
|
|
69
|
+
action: config._action || 'fresh',
|
|
70
|
+
module: 'skf',
|
|
71
|
+
skf_folder: skfFolder,
|
|
72
|
+
ides: config.ides || [],
|
|
73
|
+
skills_output_folder: config.skills_output_folder || 'skills',
|
|
74
|
+
forge_data_folder: config.forge_data_folder || 'forge-data',
|
|
75
|
+
directories: [skfFolder, '_bmad/_memory/forger-sidecar', ...(learnFiles.length > 0 ? ['_skf-learn'] : [])],
|
|
76
|
+
files: {
|
|
77
|
+
skf: skfFiles,
|
|
78
|
+
sidecar: sidecarFiles,
|
|
79
|
+
ide_commands: ideFiles,
|
|
80
|
+
learning: learnFiles,
|
|
81
|
+
output: outputFiles,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const manifestDir = path.join(projectDir, MANIFEST_DIR);
|
|
86
|
+
await fs.ensureDir(manifestDir);
|
|
87
|
+
|
|
88
|
+
const yamlStr = yaml.dump(manifest, { lineWidth: -1, sortKeys: false });
|
|
89
|
+
await fs.writeFile(
|
|
90
|
+
path.join(manifestDir, MANIFEST_FILE),
|
|
91
|
+
`# SKF Installation Manifest - Generated by installer\n# Do not edit manually\n${yamlStr}`,
|
|
92
|
+
'utf8',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read an existing manifest, or return null if none exists.
|
|
98
|
+
*/
|
|
99
|
+
async function readManifest(projectDir) {
|
|
100
|
+
const manifestPath = path.join(projectDir, MANIFEST_DIR, MANIFEST_FILE);
|
|
101
|
+
try {
|
|
102
|
+
const content = await fs.readFile(manifestPath, 'utf8');
|
|
103
|
+
return yaml.load(content) || null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { writeManifest, readManifest, MANIFEST_DIR, MANIFEST_FILE };
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKF Installer UI - Banner, prompts, and success message.
|
|
3
|
+
* Uses @clack/prompts for terminal UI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { intro, outro, text, select, multiselect, confirm, note, isCancel, cancel, log } = require('@clack/prompts');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const figlet = require('figlet');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const fs = require('fs-extra');
|
|
11
|
+
const yaml = require('js-yaml');
|
|
12
|
+
|
|
13
|
+
const SKF_FOLDER = '_bmad/skf';
|
|
14
|
+
|
|
15
|
+
class UI {
|
|
16
|
+
displayBanner() {
|
|
17
|
+
let banner;
|
|
18
|
+
try {
|
|
19
|
+
banner = figlet.textSync('SKF', { font: 'Standard' });
|
|
20
|
+
} catch {
|
|
21
|
+
banner = '\n S K F';
|
|
22
|
+
}
|
|
23
|
+
const packageJson = require('../../../package.json');
|
|
24
|
+
intro(
|
|
25
|
+
`${chalk.cyan(banner)}\n${chalk.white.bold(' Skill Forge')} ${chalk.dim(`v${packageJson.version}`)}\n${chalk.dim(' AST-verified, provenance-backed agent skills from code\n repositories, documentation, and developer discourse')}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async detectInstallation(projectDir) {
|
|
30
|
+
const hasBmadSkf = await fs.pathExists(path.join(projectDir, SKF_FOLDER));
|
|
31
|
+
const hasBmadDir = await fs.pathExists(path.join(projectDir, '_bmad'));
|
|
32
|
+
|
|
33
|
+
if (hasBmadSkf) {
|
|
34
|
+
return { type: 'existing', folder: SKF_FOLDER };
|
|
35
|
+
}
|
|
36
|
+
if (hasBmadDir) {
|
|
37
|
+
return { type: 'bmad-ready', folder: SKF_FOLDER };
|
|
38
|
+
}
|
|
39
|
+
return { type: 'fresh', folder: SKF_FOLDER };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async promptInstall() {
|
|
43
|
+
this.displayBanner();
|
|
44
|
+
|
|
45
|
+
const projectDir = process.cwd();
|
|
46
|
+
const defaultProjectName = path.basename(projectDir);
|
|
47
|
+
const detection = await this.detectInstallation(projectDir);
|
|
48
|
+
const skfFolder = detection.folder;
|
|
49
|
+
|
|
50
|
+
log.info(`Target: ${chalk.cyan(projectDir)}`);
|
|
51
|
+
|
|
52
|
+
let action = 'fresh';
|
|
53
|
+
|
|
54
|
+
if (detection.type === 'existing') {
|
|
55
|
+
log.warn(`Found existing installation at ${chalk.white(SKF_FOLDER + '/')}`);
|
|
56
|
+
|
|
57
|
+
const choice = await select({
|
|
58
|
+
message: 'What would you like to do?',
|
|
59
|
+
options: [
|
|
60
|
+
{ label: 'Update — Replace SKF files, keep config.yaml', value: 'update' },
|
|
61
|
+
{ label: 'Fresh install — Remove everything and start over', value: 'fresh' },
|
|
62
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (isCancel(choice) || choice === 'cancel') {
|
|
67
|
+
cancel('Installation cancelled.');
|
|
68
|
+
return { cancelled: true };
|
|
69
|
+
}
|
|
70
|
+
action = choice;
|
|
71
|
+
} else {
|
|
72
|
+
log.info(`Agents and workflows will be installed in ${chalk.white(skfFolder + '/')}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (action === 'update') {
|
|
76
|
+
log.info('Existing config.yaml will be preserved.');
|
|
77
|
+
return {
|
|
78
|
+
projectDir,
|
|
79
|
+
skfFolder,
|
|
80
|
+
_detection: detection,
|
|
81
|
+
_action: action,
|
|
82
|
+
cancelled: false,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Load saved config to pre-populate defaults on fresh reinstall
|
|
87
|
+
const savedConfig = await this.loadSavedConfig(projectDir, skfFolder);
|
|
88
|
+
if (savedConfig) {
|
|
89
|
+
log.info('Previous configuration detected — defaults pre-populated.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const ideOptions = [
|
|
93
|
+
{ label: 'Claude Code', value: 'claude-code' },
|
|
94
|
+
{ label: 'Cline', value: 'cline' },
|
|
95
|
+
{ label: 'Codex', value: 'codex' },
|
|
96
|
+
{ label: 'Cursor', value: 'cursor' },
|
|
97
|
+
{ label: 'GitHub Copilot', value: 'github-copilot' },
|
|
98
|
+
{ label: 'Roo Code', value: 'roo' },
|
|
99
|
+
{ label: 'Windsurf', value: 'windsurf' },
|
|
100
|
+
{ label: 'Other', value: 'other' },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// Pre-check IDEs: saved config takes priority, then auto-detect from directories
|
|
104
|
+
const savedIdes = savedConfig?.ides || [];
|
|
105
|
+
let initialIdes = [];
|
|
106
|
+
if (savedIdes.length > 0) {
|
|
107
|
+
initialIdes = savedIdes;
|
|
108
|
+
} else {
|
|
109
|
+
const detectedIdes = await this.detectIdes(projectDir);
|
|
110
|
+
if (detectedIdes.length > 0) {
|
|
111
|
+
initialIdes = detectedIdes;
|
|
112
|
+
log.info(`Auto-detected IDEs: ${detectedIdes.join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Mark initially selected IDEs
|
|
117
|
+
for (const opt of ideOptions) {
|
|
118
|
+
opt.initialSelected = initialIdes.includes(opt.value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Project name
|
|
122
|
+
const project_name = await text({
|
|
123
|
+
message: 'Project name:',
|
|
124
|
+
initialValue: savedConfig?.project_name || defaultProjectName,
|
|
125
|
+
});
|
|
126
|
+
if (isCancel(project_name)) {
|
|
127
|
+
cancel('Installation cancelled.');
|
|
128
|
+
return { cancelled: true };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Skills output folder
|
|
132
|
+
const skills_output_folder = await text({
|
|
133
|
+
message: 'Where should generated skills be saved?',
|
|
134
|
+
initialValue: savedConfig?.skills_output_folder || 'skills',
|
|
135
|
+
});
|
|
136
|
+
if (isCancel(skills_output_folder)) {
|
|
137
|
+
cancel('Installation cancelled.');
|
|
138
|
+
return { cancelled: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Forge data folder
|
|
142
|
+
const forge_data_folder = await text({
|
|
143
|
+
message: 'Where should forge workspace artifacts be stored?',
|
|
144
|
+
initialValue: savedConfig?.forge_data_folder || 'forge-data',
|
|
145
|
+
});
|
|
146
|
+
if (isCancel(forge_data_folder)) {
|
|
147
|
+
cancel('Installation cancelled.');
|
|
148
|
+
return { cancelled: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// IDE selection
|
|
152
|
+
const ides = await multiselect({
|
|
153
|
+
message: 'Which tools/IDEs are you using?',
|
|
154
|
+
options: ideOptions,
|
|
155
|
+
initialValues: initialIdes,
|
|
156
|
+
required: true,
|
|
157
|
+
});
|
|
158
|
+
if (isCancel(ides)) {
|
|
159
|
+
cancel('Installation cancelled.');
|
|
160
|
+
return { cancelled: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Learning material
|
|
164
|
+
const install_learning = await confirm({
|
|
165
|
+
message: 'Install learning & reference material?',
|
|
166
|
+
initialValue: true,
|
|
167
|
+
});
|
|
168
|
+
if (isCancel(install_learning)) {
|
|
169
|
+
cancel('Installation cancelled.');
|
|
170
|
+
return { cancelled: true };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
projectDir,
|
|
175
|
+
project_name,
|
|
176
|
+
skills_output_folder,
|
|
177
|
+
forge_data_folder,
|
|
178
|
+
ides,
|
|
179
|
+
install_learning,
|
|
180
|
+
skfFolder,
|
|
181
|
+
_detection: detection,
|
|
182
|
+
_action: action,
|
|
183
|
+
cancelled: false,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async detectIdes(projectDir) {
|
|
188
|
+
const markers = {
|
|
189
|
+
'claude-code': ['.claude'],
|
|
190
|
+
cursor: ['.cursor'],
|
|
191
|
+
cline: ['.clinerules'],
|
|
192
|
+
codex: ['.codex'],
|
|
193
|
+
'github-copilot': ['.github/copilot-instructions.md', '.github/prompts'],
|
|
194
|
+
roo: ['.roo', '.roomodes'],
|
|
195
|
+
windsurf: ['.windsurf'],
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const detected = [];
|
|
199
|
+
for (const [ide, paths] of Object.entries(markers)) {
|
|
200
|
+
for (const p of paths) {
|
|
201
|
+
if (await fs.pathExists(path.join(projectDir, p))) {
|
|
202
|
+
detected.push(ide);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return detected;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async loadSavedConfig(projectDir, skfFolder) {
|
|
211
|
+
const configPath = path.join(projectDir, skfFolder, 'config.yaml');
|
|
212
|
+
try {
|
|
213
|
+
if (await fs.pathExists(configPath)) {
|
|
214
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
215
|
+
return yaml.load(content) || null;
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// ignore parse errors
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
displaySuccess(skfFolder, ides = [], action = 'fresh') {
|
|
224
|
+
const ideNames = {
|
|
225
|
+
'claude-code': 'Claude Code',
|
|
226
|
+
cline: 'Cline',
|
|
227
|
+
codex: 'Codex',
|
|
228
|
+
cursor: 'Cursor',
|
|
229
|
+
'github-copilot': 'GitHub Copilot',
|
|
230
|
+
roo: 'Roo Code',
|
|
231
|
+
windsurf: 'Windsurf',
|
|
232
|
+
other: 'your IDE',
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
let ideDisplay;
|
|
236
|
+
if (!ides || ides.length === 0) {
|
|
237
|
+
ideDisplay = 'your IDE';
|
|
238
|
+
} else if (ides.length === 1) {
|
|
239
|
+
ideDisplay = ideNames[ides[0]] || 'your IDE';
|
|
240
|
+
} else {
|
|
241
|
+
ideDisplay = ides.map((ide) => ideNames[ide] || ide).join(' or ');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let noteTitle;
|
|
245
|
+
let noteBody;
|
|
246
|
+
|
|
247
|
+
if (action === 'update') {
|
|
248
|
+
noteTitle = 'Update complete!';
|
|
249
|
+
noteBody = [
|
|
250
|
+
`${chalk.white.bold('What Changed')}`,
|
|
251
|
+
'SKF files and agents have been refreshed.',
|
|
252
|
+
'Your config.yaml and sidecar state are preserved.',
|
|
253
|
+
'',
|
|
254
|
+
`${chalk.white.bold('Next Steps')}`,
|
|
255
|
+
`1. Reload the agent in ${ideDisplay}:`,
|
|
256
|
+
` ${chalk.cyan(`"Read and activate ${skfFolder}/agents/forger.md"`)}`,
|
|
257
|
+
'2. Run @Ferris SF to re-detect tools if needed',
|
|
258
|
+
].join('\n');
|
|
259
|
+
} else {
|
|
260
|
+
noteTitle = 'Installation complete!';
|
|
261
|
+
noteBody = [
|
|
262
|
+
`${chalk.white.bold('Get Started')}`,
|
|
263
|
+
`1. Open this folder in ${ideDisplay}`,
|
|
264
|
+
'2. Locate the chat window and type:',
|
|
265
|
+
` ${chalk.cyan(`"Read and activate ${skfFolder}/agents/forger.md"`)}`,
|
|
266
|
+
'3. Ferris (your Skill Architect) will guide you through',
|
|
267
|
+
' setting up and forging your first agent skill',
|
|
268
|
+
].join('\n');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
note(noteBody, noteTitle);
|
|
272
|
+
|
|
273
|
+
outro(`Agent: Ferris (Skill Architect & Integrity Guardian)\nDocs: https://github.com/armelhbobdad/bmad-module-skill-forge`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = { UI };
|