bmad-module-ultracode-goal 0.1.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/.claude-plugin/marketplace.json +20 -0
- package/.gitattributes +16 -0
- package/.nvmrc +1 -0
- package/LICENSE +27 -0
- package/README.md +200 -0
- package/docs/_internal/RELEASING.md +101 -0
- package/docs/_internal/STABILITY.md +62 -0
- package/docs/architecture.md +77 -0
- package/docs/assets/ucg-logo.svg +73 -0
- package/docs/gate-model.md +109 -0
- package/docs/getting-started.md +51 -0
- package/docs/health-check.md +61 -0
- package/docs/how-it-works.md +73 -0
- package/docs/index.md +25 -0
- package/docs/parallel-mode.md +33 -0
- package/docs/troubleshooting.md +56 -0
- package/docs/why-ultracode-goal.md +34 -0
- package/package.json +100 -0
- package/skills/.gitkeep +0 -0
- package/skills/module.yaml +12 -0
- package/skills/ultracode-goal/SKILL.md +75 -0
- package/skills/ultracode-goal/assets/execute-epic.workflow.js +208 -0
- package/skills/ultracode-goal/customize.toml +47 -0
- package/skills/ultracode-goal/references/define-done.md +41 -0
- package/skills/ultracode-goal/references/execute.md +90 -0
- package/skills/ultracode-goal/references/finalize.md +64 -0
- package/skills/ultracode-goal/references/gate.md +70 -0
- package/skills/ultracode-goal/references/health-check.md +332 -0
- package/skills/ultracode-goal/references/ingest-and-scope.md +46 -0
- package/skills/ultracode-goal/references/preflight.md +100 -0
- package/skills/ultracode-goal/scripts/gate_eval.py +280 -0
- package/skills/ultracode-goal/scripts/health_check_fp.py +196 -0
- package/skills/ultracode-goal/scripts/hooks/budget_stop.py +162 -0
- package/skills/ultracode-goal/scripts/hooks/guard_pretooluse.py +174 -0
- package/skills/ultracode-goal/scripts/preflight_check.py +399 -0
- package/tools/cli/commands/install.js +36 -0
- package/tools/cli/commands/status.js +161 -0
- package/tools/cli/commands/uninstall.js +175 -0
- package/tools/cli/commands/update.js +65 -0
- package/tools/cli/lib/ide-skills.js +218 -0
- package/tools/cli/lib/installer.js +226 -0
- package/tools/cli/lib/manifest.js +100 -0
- package/tools/cli/lib/platform-codes.yaml +223 -0
- package/tools/cli/lib/ui.js +353 -0
- package/tools/cli/lib/version-check.js +86 -0
- package/tools/cli/ucg-cli.js +45 -0
- package/tools/ucg-npx-wrapper.js +36 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UCG Status Command
|
|
3
|
+
* Shows installation state, version, configured IDEs, and skill integrity.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const yaml = require('js-yaml');
|
|
10
|
+
const { readManifest } = require('../lib/manifest');
|
|
11
|
+
const { getAvailablePlatforms } = require('../lib/ide-skills');
|
|
12
|
+
|
|
13
|
+
const UCG_FOLDER = '_bmad/ucg';
|
|
14
|
+
const SKILL_NAME = 'ultracode-goal';
|
|
15
|
+
|
|
16
|
+
function getIdeNames() {
|
|
17
|
+
const names = { other: 'Other' };
|
|
18
|
+
for (const p of getAvailablePlatforms()) {
|
|
19
|
+
names[p.value] = p.label;
|
|
20
|
+
}
|
|
21
|
+
return names;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readYaml(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
27
|
+
return yaml.load(content) || {};
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function getStatus(projectDir) {
|
|
34
|
+
const ucgDir = path.join(projectDir, UCG_FOLDER);
|
|
35
|
+
const skillDir = path.join(ucgDir, SKILL_NAME);
|
|
36
|
+
|
|
37
|
+
const installed = await fs.pathExists(ucgDir);
|
|
38
|
+
if (!installed) {
|
|
39
|
+
return { installed: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Read config
|
|
43
|
+
const config = await readYaml(path.join(ucgDir, 'config.yaml'));
|
|
44
|
+
|
|
45
|
+
// Read installed version
|
|
46
|
+
let installedVersion = null;
|
|
47
|
+
try {
|
|
48
|
+
installedVersion = (await fs.readFile(path.join(ucgDir, 'VERSION'), 'utf8')).trim();
|
|
49
|
+
} catch {
|
|
50
|
+
/* keep null */
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Skill integrity: SKILL.md, stage references, deterministic scripts, hooks
|
|
54
|
+
const skillInstalled = await fs.pathExists(path.join(skillDir, 'SKILL.md'));
|
|
55
|
+
|
|
56
|
+
let referenceCount = 0;
|
|
57
|
+
const referencesDir = path.join(skillDir, 'references');
|
|
58
|
+
if (await fs.pathExists(referencesDir)) {
|
|
59
|
+
const entries = await fs.readdir(referencesDir);
|
|
60
|
+
referenceCount = entries.filter((entry) => entry.endsWith('.md')).length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const gateScript = await fs.pathExists(path.join(skillDir, 'scripts', 'gate_eval.py'));
|
|
64
|
+
const preflightScript = await fs.pathExists(path.join(skillDir, 'scripts', 'preflight_check.py'));
|
|
65
|
+
const hooksPresent = await fs.pathExists(path.join(skillDir, 'scripts', 'hooks'));
|
|
66
|
+
|
|
67
|
+
// Learning material
|
|
68
|
+
const learnInstalled = await fs.pathExists(path.join(projectDir, '_ucg-learn'));
|
|
69
|
+
|
|
70
|
+
// Read manifest
|
|
71
|
+
const manifest = await readManifest(projectDir);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
installed: true,
|
|
75
|
+
config,
|
|
76
|
+
installedVersion,
|
|
77
|
+
skillInstalled,
|
|
78
|
+
referenceCount,
|
|
79
|
+
gateScript,
|
|
80
|
+
preflightScript,
|
|
81
|
+
hooksPresent,
|
|
82
|
+
learnInstalled,
|
|
83
|
+
manifest,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function displayStatus(status, version) {
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(chalk.hex('#6366F1').bold(' UltraCode Goal — Status'));
|
|
90
|
+
console.log(chalk.dim(` v${version}`));
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
if (!status.installed) {
|
|
94
|
+
console.log(chalk.yellow(' Not installed.'));
|
|
95
|
+
console.log(chalk.dim(' Run: npx bmad-module-ultracode-goal install'));
|
|
96
|
+
console.log('');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const config = status.config || {};
|
|
101
|
+
|
|
102
|
+
// Installation
|
|
103
|
+
const manifest = status.manifest;
|
|
104
|
+
console.log(chalk.white.bold(' Installation'));
|
|
105
|
+
console.log(` Project: ${chalk.hex('#818CF8')(config.project_name || '(unknown)')}`);
|
|
106
|
+
console.log(` UCG folder: ${chalk.dim(UCG_FOLDER + '/')}`);
|
|
107
|
+
console.log(` Version: ${status.installedVersion ? chalk.white(status.installedVersion) : chalk.yellow('(unknown)')}`);
|
|
108
|
+
console.log(` Skill: ${status.skillInstalled ? chalk.green('installed') : chalk.yellow('not installed')}`);
|
|
109
|
+
if (manifest) {
|
|
110
|
+
console.log(
|
|
111
|
+
` Installed: ${chalk.dim(manifest.installed_at ? new Date(manifest.installed_at).toLocaleDateString() : '(unknown)')}`,
|
|
112
|
+
);
|
|
113
|
+
console.log(` Manifest: ${chalk.green('present')}`);
|
|
114
|
+
} else {
|
|
115
|
+
console.log(` Manifest: ${chalk.yellow('missing')} ${chalk.dim('(reinstall to generate)')}`);
|
|
116
|
+
}
|
|
117
|
+
console.log('');
|
|
118
|
+
|
|
119
|
+
// Skill integrity
|
|
120
|
+
console.log(chalk.white.bold(' Skill Integrity'));
|
|
121
|
+
console.log(` Stages: ${chalk.white(status.referenceCount)} ${chalk.dim('reference files')}`);
|
|
122
|
+
console.log(` gate_eval: ${status.gateScript ? chalk.green('present') : chalk.red('missing')}`);
|
|
123
|
+
console.log(` preflight: ${status.preflightScript ? chalk.green('present') : chalk.red('missing')}`);
|
|
124
|
+
console.log(` hooks: ${status.hooksPresent ? chalk.green('present') : chalk.red('missing')}`);
|
|
125
|
+
console.log('');
|
|
126
|
+
|
|
127
|
+
// IDEs
|
|
128
|
+
const ides = config.ides || [];
|
|
129
|
+
const ideNames = getIdeNames();
|
|
130
|
+
console.log(chalk.white.bold(' IDEs'));
|
|
131
|
+
if (ides.length > 0) {
|
|
132
|
+
for (const ide of ides) {
|
|
133
|
+
console.log(` ${chalk.green('●')} ${ideNames[ide] || ide}`);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
console.log(chalk.dim(' None configured'));
|
|
137
|
+
}
|
|
138
|
+
console.log('');
|
|
139
|
+
|
|
140
|
+
// Learning material
|
|
141
|
+
console.log(chalk.white.bold(' Learning Material'));
|
|
142
|
+
console.log(` _ucg-learn/: ${status.learnInstalled ? chalk.green('installed') : chalk.dim('not installed')}`);
|
|
143
|
+
console.log('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
command: 'status',
|
|
148
|
+
description: 'Show UCG installation state, version, and configuration',
|
|
149
|
+
options: [],
|
|
150
|
+
action: async () => {
|
|
151
|
+
try {
|
|
152
|
+
const projectDir = process.cwd();
|
|
153
|
+
const packageJson = require('../../../package.json');
|
|
154
|
+
const status = await getStatus(projectDir);
|
|
155
|
+
displayStatus(status, packageJson.version);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error(chalk.red('\nFailed to read status:'), error.message);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UCG Uninstall Command
|
|
3
|
+
* Clean removal of UCG files, IDE skill directories, and learning material.
|
|
4
|
+
* Uses the manifest to know exactly what to remove.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
const { confirm, isCancel, spinner, log, outro } = require('@clack/prompts');
|
|
11
|
+
const { readManifest, MANIFEST_DIR, MANIFEST_FILE } = require('../lib/manifest');
|
|
12
|
+
const { removeAllUcgSkills } = require('../lib/ide-skills');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Count files that still exist on disk from manifest lists.
|
|
16
|
+
*/
|
|
17
|
+
async function countExistingFiles(projectDir, manifest) {
|
|
18
|
+
const allFiles = [...(manifest.files.ucg || []), ...(manifest.files.ide_skills || []), ...(manifest.files.learning || [])];
|
|
19
|
+
|
|
20
|
+
let existing = 0;
|
|
21
|
+
for (const file of allFiles) {
|
|
22
|
+
if (await fs.pathExists(path.join(projectDir, file))) {
|
|
23
|
+
existing++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { total: allFiles.length, existing };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Display what will be removed, grouped by category.
|
|
31
|
+
*/
|
|
32
|
+
async function displayRemovalPlan(projectDir, manifest) {
|
|
33
|
+
const categories = [
|
|
34
|
+
{ key: 'ucg', label: 'UCG module files', dir: manifest.ucg_folder },
|
|
35
|
+
{ key: 'ide_skills', label: 'IDE skill directories' },
|
|
36
|
+
{ key: 'learning', label: 'Learning material', dir: '_ucg-learn' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const lines = [];
|
|
40
|
+
for (const cat of categories) {
|
|
41
|
+
const files = manifest.files[cat.key] || [];
|
|
42
|
+
if (files.length === 0) continue;
|
|
43
|
+
|
|
44
|
+
// Count how many still exist
|
|
45
|
+
let existCount = 0;
|
|
46
|
+
for (const f of files) {
|
|
47
|
+
if (await fs.pathExists(path.join(projectDir, f))) existCount++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (existCount === 0) continue;
|
|
51
|
+
|
|
52
|
+
if (cat.dir) {
|
|
53
|
+
lines.push(`${chalk.red('×')} ${cat.label} ${chalk.dim(`(${cat.dir}/ — ${existCount} files)`)}`);
|
|
54
|
+
} else {
|
|
55
|
+
lines.push(`${chalk.red('×')} ${cat.label} ${chalk.dim(`(${existCount} files)`)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Manifest itself
|
|
60
|
+
lines.push(`${chalk.red('×')} Installation manifest ${chalk.dim(`(${MANIFEST_DIR}/${MANIFEST_FILE})`)}`);
|
|
61
|
+
|
|
62
|
+
log.warn(`The following will be removed:\n${lines.join('\n')}`);
|
|
63
|
+
log.info(
|
|
64
|
+
'Note: hooks merged into .claude/settings.local.json are not removed —\ndelete the ultracode-goal entries there manually if you added them.',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Remove a directory if it exists and is empty.
|
|
70
|
+
*/
|
|
71
|
+
async function removeEmptyDir(dirPath) {
|
|
72
|
+
if (!(await fs.pathExists(dirPath))) return;
|
|
73
|
+
try {
|
|
74
|
+
const entries = await fs.readdir(dirPath);
|
|
75
|
+
if (entries.length === 0) {
|
|
76
|
+
await fs.remove(dirPath);
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
command: 'uninstall',
|
|
85
|
+
description: 'Remove the UCG installation from the current project',
|
|
86
|
+
options: [],
|
|
87
|
+
action: async () => {
|
|
88
|
+
try {
|
|
89
|
+
const projectDir = process.cwd();
|
|
90
|
+
const manifest = await readManifest(projectDir);
|
|
91
|
+
|
|
92
|
+
if (!manifest) {
|
|
93
|
+
// Check if UCG exists without manifest
|
|
94
|
+
const ucgExists = await fs.pathExists(path.join(projectDir, '_bmad/ucg'));
|
|
95
|
+
if (ucgExists) {
|
|
96
|
+
log.warn(
|
|
97
|
+
'No manifest found. Reinstall first to generate one,\nthen run uninstall again for clean removal.\nRun: npx bmad-module-ultracode-goal install',
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
log.warn('UltraCode Goal is not installed in this directory.');
|
|
101
|
+
}
|
|
102
|
+
process.exit(0);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { existing } = await countExistingFiles(projectDir, manifest);
|
|
107
|
+
if (existing === 0) {
|
|
108
|
+
log.warn('No UCG files found to remove.');
|
|
109
|
+
// Clean up stale manifest
|
|
110
|
+
await fs.remove(path.join(projectDir, MANIFEST_DIR, MANIFEST_FILE));
|
|
111
|
+
process.exit(0);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await displayRemovalPlan(projectDir, manifest);
|
|
116
|
+
|
|
117
|
+
const shouldProceed = await confirm({
|
|
118
|
+
message: 'Proceed with uninstall?',
|
|
119
|
+
initialValue: false,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (isCancel(shouldProceed) || !shouldProceed) {
|
|
123
|
+
log.warn('Uninstall cancelled.');
|
|
124
|
+
process.exit(0);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const s = spinner();
|
|
129
|
+
|
|
130
|
+
// Remove IDE skill directories from all known platforms (skills + legacy command files)
|
|
131
|
+
s.start('Removing IDE skill directories...');
|
|
132
|
+
const removedIdeDirs = await removeAllUcgSkills(projectDir);
|
|
133
|
+
if (removedIdeDirs.length > 0) {
|
|
134
|
+
s.stop(`Cleaned the skill from ${removedIdeDirs.length} IDE target(s)`);
|
|
135
|
+
} else {
|
|
136
|
+
s.stop('No IDE skill directories found');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Remove learning material directory
|
|
140
|
+
const learnFiles = manifest.files.learning || [];
|
|
141
|
+
if (learnFiles.length > 0) {
|
|
142
|
+
s.start('Removing learning material...');
|
|
143
|
+
const learnDir = path.join(projectDir, '_ucg-learn');
|
|
144
|
+
if (await fs.pathExists(learnDir)) {
|
|
145
|
+
await fs.remove(learnDir);
|
|
146
|
+
}
|
|
147
|
+
s.stop('Learning material removed');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Remove UCG module directory
|
|
151
|
+
s.start('Removing UCG module...');
|
|
152
|
+
const ucgDir = path.join(projectDir, manifest.ucg_folder);
|
|
153
|
+
if (await fs.pathExists(ucgDir)) {
|
|
154
|
+
await fs.remove(ucgDir);
|
|
155
|
+
}
|
|
156
|
+
s.stop('UCG module removed');
|
|
157
|
+
|
|
158
|
+
// Remove manifest
|
|
159
|
+
const manifestPath = path.join(projectDir, MANIFEST_DIR, MANIFEST_FILE);
|
|
160
|
+
if (await fs.pathExists(manifestPath)) {
|
|
161
|
+
await fs.remove(manifestPath);
|
|
162
|
+
}
|
|
163
|
+
// Clean empty _bmad/_config/ and _bmad/ if we were the only occupant
|
|
164
|
+
await removeEmptyDir(path.join(projectDir, MANIFEST_DIR));
|
|
165
|
+
await removeEmptyDir(path.join(projectDir, '_bmad'));
|
|
166
|
+
|
|
167
|
+
outro('UltraCode Goal uninstalled successfully.');
|
|
168
|
+
|
|
169
|
+
process.exit(0);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(chalk.red('\nUninstall failed:'), error.message);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UCG Quick Update Command
|
|
3
|
+
* Replaces UCG files and reinstalls the skill without re-prompting.
|
|
4
|
+
* Preserves config.yaml.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
const yaml = require('js-yaml');
|
|
11
|
+
const { Installer } = require('../lib/installer');
|
|
12
|
+
const { UI } = require('../lib/ui');
|
|
13
|
+
|
|
14
|
+
const UCG_FOLDER = '_bmad/ucg';
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
command: 'update',
|
|
18
|
+
description: 'Update UCG files and reinstall the skill (preserves config)',
|
|
19
|
+
options: [],
|
|
20
|
+
action: async () => {
|
|
21
|
+
try {
|
|
22
|
+
const projectDir = process.cwd();
|
|
23
|
+
const ucgDir = path.join(projectDir, UCG_FOLDER);
|
|
24
|
+
|
|
25
|
+
if (!(await fs.pathExists(ucgDir))) {
|
|
26
|
+
console.log(chalk.yellow('\n UltraCode Goal is not installed in this directory.'));
|
|
27
|
+
console.log(chalk.dim(' Run: npx bmad-module-ultracode-goal install\n'));
|
|
28
|
+
process.exit(0);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log(chalk.hex('#6366F1').bold(' UltraCode Goal — Quick Update'));
|
|
34
|
+
console.log(chalk.dim(' Replacing UCG files, preserving config.\n'));
|
|
35
|
+
|
|
36
|
+
const installer = new Installer();
|
|
37
|
+
const result = await installer.install({
|
|
38
|
+
projectDir,
|
|
39
|
+
ucgFolder: UCG_FOLDER,
|
|
40
|
+
_action: 'update',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (result && result.success) {
|
|
44
|
+
// Read config to get IDEs for post-update notes
|
|
45
|
+
let ides = [];
|
|
46
|
+
try {
|
|
47
|
+
const configContent = await fs.readFile(path.join(ucgDir, 'config.yaml'), 'utf8');
|
|
48
|
+
const config = yaml.load(configContent);
|
|
49
|
+
ides = config?.ides || [];
|
|
50
|
+
} catch {
|
|
51
|
+
/* use empty */
|
|
52
|
+
}
|
|
53
|
+
const ui = new UI();
|
|
54
|
+
ui.displaySuccess(UCG_FOLDER, ides, 'update');
|
|
55
|
+
process.exit(0);
|
|
56
|
+
} else {
|
|
57
|
+
console.error(chalk.red('\nUpdate failed.'));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(chalk.red('\nUpdate failed:'), error.message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE Skill Installer — Verbatim skill directory installation
|
|
3
|
+
*
|
|
4
|
+
* Follows the BMAD standard: skill directories (containing SKILL.md +
|
|
5
|
+
* supporting files) are copied directly to each IDE's skills/ directory.
|
|
6
|
+
* IDEs read SKILL.md natively.
|
|
7
|
+
*
|
|
8
|
+
* Supports 23+ IDEs via platform-codes.yaml (config-driven, no IDE-specific code).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
const yaml = require('js-yaml');
|
|
14
|
+
|
|
15
|
+
const PLATFORM_CODES_PATH = path.join(__dirname, 'platform-codes.yaml');
|
|
16
|
+
|
|
17
|
+
// UCG-owned skill directories inside IDE skills/ targets
|
|
18
|
+
const UCG_SKILLS = new Set(['ultracode-goal']);
|
|
19
|
+
|
|
20
|
+
// OS/editor artifacts to filter during copy
|
|
21
|
+
const ARTIFACT_FILTER = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini', '._.DS_Store']);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load platform configuration from platform-codes.yaml.
|
|
25
|
+
* Returns { platforms: { 'claude-code': { name, preferred, installer: { target_dir, legacy_targets } }, ... } }
|
|
26
|
+
*/
|
|
27
|
+
function loadPlatforms() {
|
|
28
|
+
const content = fs.readFileSync(PLATFORM_CODES_PATH, 'utf8');
|
|
29
|
+
return yaml.load(content);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get available platforms for UI display.
|
|
34
|
+
* Returns array of { value, label, preferred, skillInvocationPrefix }.
|
|
35
|
+
* skillInvocationPrefix is null when the IDE only auto-invokes skills.
|
|
36
|
+
*/
|
|
37
|
+
function getAvailablePlatforms() {
|
|
38
|
+
const config = loadPlatforms();
|
|
39
|
+
return Object.entries(config.platforms)
|
|
40
|
+
.filter(([, p]) => !p.suspended)
|
|
41
|
+
.map(([code, p]) => ({
|
|
42
|
+
value: code,
|
|
43
|
+
label: p.name,
|
|
44
|
+
preferred: p.preferred || false,
|
|
45
|
+
skillInvocationPrefix: p.skill_invocation_prefix ?? null,
|
|
46
|
+
}))
|
|
47
|
+
.sort((a, b) => {
|
|
48
|
+
// Preferred first, then alphabetical
|
|
49
|
+
if (a.preferred !== b.preferred) return b.preferred ? 1 : -1;
|
|
50
|
+
return a.label.localeCompare(b.label);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get IDE auto-detection markers.
|
|
56
|
+
* Returns { 'claude-code': ['.claude'], 'cursor': ['.cursor'], ... }
|
|
57
|
+
*
|
|
58
|
+
* Uses explicit detection_marker from platform config when available,
|
|
59
|
+
* otherwise derives from the top-level directory of target_dir.
|
|
60
|
+
* The derived marker only works when the IDE's config directory exists
|
|
61
|
+
* independently of the installer (e.g. .claude/ exists before UCG install).
|
|
62
|
+
*/
|
|
63
|
+
function getDetectionMarkers() {
|
|
64
|
+
const config = loadPlatforms();
|
|
65
|
+
const markers = {};
|
|
66
|
+
for (const [code, p] of Object.entries(config.platforms)) {
|
|
67
|
+
if (p.suspended) continue;
|
|
68
|
+
const targetDir = p.installer?.target_dir;
|
|
69
|
+
if (!targetDir) continue;
|
|
70
|
+
if (p.installer.detection_marker) {
|
|
71
|
+
markers[code] = [p.installer.detection_marker];
|
|
72
|
+
} else {
|
|
73
|
+
// Derive from target_dir — works when top-level dir is the IDE's own config dir
|
|
74
|
+
const topDir = '.' + targetDir.split('/')[0].replace(/^\./, '');
|
|
75
|
+
markers[code] = [topDir];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return markers;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Install skill directories to all selected IDEs.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} projectDir - Project root directory
|
|
85
|
+
* @param {string} ucgDir - Path to installed UCG module (e.g., {projectDir}/_bmad/ucg)
|
|
86
|
+
* @param {string[]} ideCodes - Array of IDE codes (e.g., ['claude-code', 'cursor'])
|
|
87
|
+
* @returns {{ installed: number, ides: string[], directories: string[] }}
|
|
88
|
+
*/
|
|
89
|
+
async function installSkillsToIdes(projectDir, ucgDir, ideCodes) {
|
|
90
|
+
if (!ideCodes || ideCodes.length === 0) return { installed: 0, ides: [], directories: [] };
|
|
91
|
+
|
|
92
|
+
const config = loadPlatforms();
|
|
93
|
+
let totalInstalled = 0;
|
|
94
|
+
const processedIdes = [];
|
|
95
|
+
const allDirectories = [];
|
|
96
|
+
|
|
97
|
+
// Copy filter: skip OS artifacts
|
|
98
|
+
const copyFilter = (src) => !ARTIFACT_FILTER.has(path.basename(src));
|
|
99
|
+
|
|
100
|
+
for (const ideCode of ideCodes) {
|
|
101
|
+
const platform = config.platforms[ideCode];
|
|
102
|
+
if (!platform || !platform.installer?.target_dir) continue;
|
|
103
|
+
|
|
104
|
+
const targetDir = path.join(projectDir, platform.installer.target_dir);
|
|
105
|
+
|
|
106
|
+
// Clean legacy targets first (old command files from previous UCG installs)
|
|
107
|
+
await cleanLegacyTargets(projectDir, platform);
|
|
108
|
+
|
|
109
|
+
// Clean existing UCG skills from this IDE (for update/reinstall)
|
|
110
|
+
await cleanUcgSkills(targetDir);
|
|
111
|
+
|
|
112
|
+
// Ensure target directory exists
|
|
113
|
+
await fs.ensureDir(targetDir);
|
|
114
|
+
|
|
115
|
+
// Copy UCG-owned skill directories from the installed module
|
|
116
|
+
if (await fs.pathExists(ucgDir)) {
|
|
117
|
+
const entries = await fs.readdir(ucgDir, { withFileTypes: true });
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (!entry.isDirectory()) continue;
|
|
120
|
+
|
|
121
|
+
if (UCG_SKILLS.has(entry.name)) {
|
|
122
|
+
const src = path.join(ucgDir, entry.name);
|
|
123
|
+
const dest = path.join(targetDir, entry.name);
|
|
124
|
+
await fs.copy(src, dest, { filter: copyFilter });
|
|
125
|
+
totalInstalled++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
allDirectories.push(platform.installer.target_dir);
|
|
131
|
+
processedIdes.push(ideCode);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { installed: totalInstalled, ides: processedIdes, directories: allDirectories };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Remove legacy command files from old IDE target directories.
|
|
139
|
+
* Handles migration from command-file approach to skill-directory approach.
|
|
140
|
+
*/
|
|
141
|
+
async function cleanLegacyTargets(projectDir, platform) {
|
|
142
|
+
const legacyTargets = platform.installer?.legacy_targets || [];
|
|
143
|
+
|
|
144
|
+
for (const legacyDir of legacyTargets) {
|
|
145
|
+
const fullPath = path.join(projectDir, legacyDir);
|
|
146
|
+
if (!(await fs.pathExists(fullPath))) continue;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const files = await fs.readdir(fullPath);
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
// Remove UCG-specific command files from legacy directories
|
|
152
|
+
if (file.startsWith('bmad-ultracode-goal-') || file.startsWith('bmad-ucg-')) {
|
|
153
|
+
await fs.remove(path.join(fullPath, file));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Remove empty directories
|
|
158
|
+
const remaining = await fs.readdir(fullPath);
|
|
159
|
+
if (remaining.length === 0) {
|
|
160
|
+
await fs.remove(fullPath);
|
|
161
|
+
// Also try to remove empty parent
|
|
162
|
+
const parentDir = path.dirname(fullPath);
|
|
163
|
+
try {
|
|
164
|
+
const parentRemaining = await fs.readdir(parentDir);
|
|
165
|
+
if (parentRemaining.length === 0) await fs.remove(parentDir);
|
|
166
|
+
} catch {
|
|
167
|
+
/* ignore */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
/* non-critical, continue */
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Remove existing UCG skill directories from an IDE target.
|
|
178
|
+
* Called before reinstalling to ensure clean state.
|
|
179
|
+
*/
|
|
180
|
+
async function cleanUcgSkills(targetDir) {
|
|
181
|
+
if (!(await fs.pathExists(targetDir))) return;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const entries = await fs.readdir(targetDir);
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
// Only remove UCG-owned directories
|
|
187
|
+
if (UCG_SKILLS.has(entry)) {
|
|
188
|
+
await fs.remove(path.join(targetDir, entry));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
/* non-critical */
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Remove all UCG skills from all known IDE directories.
|
|
198
|
+
* Used during uninstall.
|
|
199
|
+
*/
|
|
200
|
+
async function removeAllUcgSkills(projectDir) {
|
|
201
|
+
const config = loadPlatforms();
|
|
202
|
+
const removed = [];
|
|
203
|
+
|
|
204
|
+
for (const [, platform] of Object.entries(config.platforms)) {
|
|
205
|
+
if (!platform.installer?.target_dir) continue;
|
|
206
|
+
const targetDir = path.join(projectDir, platform.installer.target_dir);
|
|
207
|
+
if (await fs.pathExists(targetDir)) {
|
|
208
|
+
await cleanUcgSkills(targetDir);
|
|
209
|
+
// Also clean legacy targets
|
|
210
|
+
await cleanLegacyTargets(projectDir, platform);
|
|
211
|
+
removed.push(platform.installer.target_dir);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return removed;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { installSkillsToIdes, getAvailablePlatforms, getDetectionMarkers, removeAllUcgSkills, loadPlatforms };
|