@dzhechkov/skills-bto 1.0.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/bin/cli.js +5 -0
- package/package.json +43 -0
- package/src/cli.js +150 -0
- package/src/commands/doctor.js +366 -0
- package/src/commands/init.js +188 -0
- package/src/commands/list.js +161 -0
- package/src/commands/remove.js +211 -0
- package/src/commands/update.js +198 -0
- package/src/utils.js +398 -0
- package/templates/.claude/agents/bto-judge-panel.md +192 -0
- package/templates/.claude/agents/bto-optimizer-worker.md +181 -0
- package/templates/.claude/commands/bto-build.md +169 -0
- package/templates/.claude/commands/bto-optimize.md +208 -0
- package/templates/.claude/commands/bto-test.md +186 -0
- package/templates/.claude/commands/bto.md +171 -0
- package/templates/.claude/rules/bto-quality-gates.md +91 -0
- package/templates/.claude/skills/bto/SKILL.md +266 -0
- package/templates/.claude/skills/bto/examples/sample-eval-report.md +436 -0
- package/templates/.claude/skills/bto/modules/build.md +189 -0
- package/templates/.claude/skills/bto/modules/optimize.md +201 -0
- package/templates/.claude/skills/bto/modules/test.md +348 -0
- package/templates/.claude/skills/bto/references/eval-patterns.md +202 -0
- package/templates/.claude/skills/bto/references/judge-rubrics.md +183 -0
- package/templates/.claude/skills/bto/references/optimization-methods.md +139 -0
- package/templates/.claude/skills/bto/references/quality-checklist.md +220 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const {
|
|
6
|
+
green, yellow, cyan, bold, dim,
|
|
7
|
+
info, success, warn, error: logError, step,
|
|
8
|
+
copyDirRecursive, copyDirFiltered, fileExists, readJSON,
|
|
9
|
+
ensureDir, getRelativePaths, getRelativePathsFiltered,
|
|
10
|
+
createManifest, writeManifest, getTemplatesDir,
|
|
11
|
+
COMPONENTS, MANIFEST_FILE, getComponentFilter,
|
|
12
|
+
} = require('../utils');
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Keysarium integration detection
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const KEYSARIUM_MANIFEST = '.keysarium.json';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if @dzhechkov/keysarium is installed in the target directory.
|
|
22
|
+
* Returns the keysarium manifest or null if not found.
|
|
23
|
+
*/
|
|
24
|
+
function detectKeysarium(targetDir) {
|
|
25
|
+
const manifestPath = path.join(targetDir, KEYSARIUM_MANIFEST);
|
|
26
|
+
if (!fileExists(manifestPath)) return null;
|
|
27
|
+
return readJSON(manifestPath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Show integration message when keysarium is detected.
|
|
32
|
+
*/
|
|
33
|
+
function showKeysariumIntegration(keysariumManifest) {
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(cyan(' ┌──────────────────────────────────────────────────┐'));
|
|
36
|
+
console.log(cyan(' │') + bold(' @dzhechkov/keysarium detected!') + ' ' + cyan('│'));
|
|
37
|
+
console.log(cyan(' │') + ` Version: ${dim(keysariumManifest.version)}` + ' '.repeat(39 - keysariumManifest.version.length) + cyan('│'));
|
|
38
|
+
console.log(cyan(' │') + ' BTO will integrate with existing Keysarium setup. ' + cyan('│'));
|
|
39
|
+
console.log(cyan(' │') + ' Shared directories: .claude/commands, rules, agents' + cyan('│'));
|
|
40
|
+
console.log(cyan(' └──────────────────────────────────────────────────┘'));
|
|
41
|
+
console.log('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Helpers
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Copy a single BTO component from templates to the target directory.
|
|
50
|
+
* Handles both full-directory (skill) and filtered (commands, rules, agents) components.
|
|
51
|
+
* Returns array of relative file paths that were installed.
|
|
52
|
+
*/
|
|
53
|
+
function installComponent(key, comp, templatesDir, targetDir) {
|
|
54
|
+
const src = path.join(templatesDir, comp.src);
|
|
55
|
+
const dest = path.join(targetDir, comp.src);
|
|
56
|
+
|
|
57
|
+
if (!fileExists(src)) {
|
|
58
|
+
warn(`Template source not found: ${comp.src} — skipping.`);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const filterFn = getComponentFilter(comp);
|
|
63
|
+
|
|
64
|
+
if (filterFn) {
|
|
65
|
+
// Filtered component — only copy matching files from shared directory
|
|
66
|
+
copyDirFiltered(src, dest, filterFn);
|
|
67
|
+
return getRelativePathsFiltered(dest, filterFn).map((rel) => path.join(comp.src, rel));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Non-filtered component — copy entire directory or file
|
|
71
|
+
if (comp.isFile) {
|
|
72
|
+
ensureDir(path.dirname(dest));
|
|
73
|
+
fs.copyFileSync(src, dest);
|
|
74
|
+
return [comp.src];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
copyDirRecursive(src, dest);
|
|
78
|
+
return getRelativePaths(dest).map((rel) => path.join(comp.src, rel));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Main command
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* `@dzhechkov/skills-bto init` — Install the BTO skill pack into the target project.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} options
|
|
89
|
+
* @param {boolean} options.force — Overwrite existing installation
|
|
90
|
+
* @param {boolean} options.dryRun — Preview without writing anything
|
|
91
|
+
* @param {string} options.targetDir — Destination project root
|
|
92
|
+
*/
|
|
93
|
+
async function run(options) {
|
|
94
|
+
const { force, dryRun, targetDir } = options;
|
|
95
|
+
const manifestPath = path.join(targetDir, MANIFEST_FILE);
|
|
96
|
+
|
|
97
|
+
// ── a) Check for existing BTO installation ─────────────────────────────
|
|
98
|
+
if (fileExists(manifestPath)) {
|
|
99
|
+
if (!force) {
|
|
100
|
+
warn('BTO skill pack is already installed in this directory.');
|
|
101
|
+
info(`Run ${cyan('@dzhechkov/skills-bto update')} to update, or use ${yellow('--force')} to overwrite.`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
warn('Existing BTO installation found — overwriting (--force).');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── b) Detect keysarium integration ─────────────────────────────────────
|
|
108
|
+
const keysariumManifest = detectKeysarium(targetDir);
|
|
109
|
+
if (keysariumManifest) {
|
|
110
|
+
showKeysariumIntegration(keysariumManifest);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── c) Determine components ─────────────────────────────────────────────
|
|
114
|
+
const componentKeys = Object.keys(COMPONENTS);
|
|
115
|
+
const templatesDir = getTemplatesDir();
|
|
116
|
+
|
|
117
|
+
// ── d) Show plan ────────────────────────────────────────────────────────
|
|
118
|
+
console.log('');
|
|
119
|
+
info(bold('Installation plan:'));
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
122
|
+
for (const key of componentKeys) {
|
|
123
|
+
const comp = COMPONENTS[key];
|
|
124
|
+
const filterNote = comp.filter ? dim(` (filtered: ${comp.filter}*)`): '';
|
|
125
|
+
console.log(` ${green('+')} ${comp.label}${filterNote}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log('');
|
|
129
|
+
|
|
130
|
+
if (dryRun) {
|
|
131
|
+
warn('Dry run — no files were written.');
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── e) Install components ───────────────────────────────────────────────
|
|
136
|
+
const totalComponents = componentKeys.length;
|
|
137
|
+
const installedFiles = [];
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < totalComponents; i++) {
|
|
140
|
+
const key = componentKeys[i];
|
|
141
|
+
const comp = COMPONENTS[key];
|
|
142
|
+
step(i + 1, totalComponents, `Installing ${comp.label}...`);
|
|
143
|
+
|
|
144
|
+
const files = installComponent(key, comp, templatesDir, targetDir);
|
|
145
|
+
installedFiles.push(...files);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── f) Write manifest ──────────────────────────────────────────────────
|
|
149
|
+
const pkgPath = path.resolve(__dirname, '../../package.json');
|
|
150
|
+
const pkg = readJSON(pkgPath);
|
|
151
|
+
const version = pkg ? pkg.version : '0.0.0';
|
|
152
|
+
|
|
153
|
+
const manifest = createManifest(version, componentKeys, installedFiles.sort());
|
|
154
|
+
writeManifest(targetDir, manifest);
|
|
155
|
+
info(`Created ${MANIFEST_FILE} manifest`);
|
|
156
|
+
|
|
157
|
+
// ── g) Success summary ─────────────────────────────────────────────────
|
|
158
|
+
console.log('');
|
|
159
|
+
success(bold('BTO skill pack installed!'));
|
|
160
|
+
console.log('');
|
|
161
|
+
|
|
162
|
+
console.log(bold('Installed components:'));
|
|
163
|
+
for (const key of componentKeys) {
|
|
164
|
+
const comp = COMPONENTS[key];
|
|
165
|
+
console.log(` ${green('\u2713')} ${comp.label}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log(bold('Next steps:'));
|
|
170
|
+
console.log(` 1. Open ${cyan('Claude Code')} in this directory`);
|
|
171
|
+
console.log(` 2. Run ${cyan('/bto [description]')} for the full BTO pipeline`);
|
|
172
|
+
console.log(` 3. Or use individual phases:`);
|
|
173
|
+
console.log(` ${cyan('/bto-build')} ${dim('\u2014 Generate skill or command')}`);
|
|
174
|
+
console.log(` ${cyan('/bto-test')} ${dim('\u2014 Multi-agent evaluation')}`);
|
|
175
|
+
console.log('');
|
|
176
|
+
|
|
177
|
+
if (keysariumManifest) {
|
|
178
|
+
console.log(bold('Integration:'));
|
|
179
|
+
console.log(` BTO commands are available alongside your Keysarium pipeline.`);
|
|
180
|
+
console.log(` Run ${cyan('@dzhechkov/keysarium list')} to see all components.`);
|
|
181
|
+
console.log('');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = run;
|
|
188
|
+
module.exports.run = run;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const {
|
|
6
|
+
green, red, cyan, bold, dim,
|
|
7
|
+
info, warn, error: logError,
|
|
8
|
+
fileExists, readManifest, getRelativePaths, getRelativePathsFiltered,
|
|
9
|
+
COMPONENTS, MANIFEST_FILE, getComponentFilter,
|
|
10
|
+
} = require('../utils');
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Count files on disk for a given BTO component in the target directory.
|
|
18
|
+
* Respects the component's filter to only count BTO-owned files.
|
|
19
|
+
* Returns the number of files that actually exist.
|
|
20
|
+
*/
|
|
21
|
+
function countComponentFiles(comp, targetDir) {
|
|
22
|
+
const destPath = path.join(targetDir, comp.src);
|
|
23
|
+
|
|
24
|
+
if (!fileExists(destPath)) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (comp.isFile) {
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const stat = fs.statSync(destPath);
|
|
34
|
+
if (stat.isDirectory()) {
|
|
35
|
+
const filterFn = getComponentFilter(comp);
|
|
36
|
+
if (filterFn) {
|
|
37
|
+
return getRelativePathsFiltered(destPath, filterFn).length;
|
|
38
|
+
}
|
|
39
|
+
return getRelativePaths(destPath).length;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pad or truncate a string to a fixed width.
|
|
50
|
+
*/
|
|
51
|
+
function padRight(str, width) {
|
|
52
|
+
if (str.length >= width) return str.slice(0, width);
|
|
53
|
+
return str + ' '.repeat(width - str.length);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Main command
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `@dzhechkov/skills-bto list` — Show installed BTO components and their status.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} options
|
|
64
|
+
* @param {string} options.targetDir — Project root directory
|
|
65
|
+
*/
|
|
66
|
+
async function run(options) {
|
|
67
|
+
const { targetDir } = options;
|
|
68
|
+
const manifestPath = path.join(targetDir, MANIFEST_FILE);
|
|
69
|
+
|
|
70
|
+
// ── a) Read manifest ───────────────────────────────────────────────────
|
|
71
|
+
if (!fileExists(manifestPath)) {
|
|
72
|
+
warn('BTO skill pack is not installed in this directory.');
|
|
73
|
+
info(`Run ${cyan('@dzhechkov/skills-bto init')} to install.`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const manifest = readManifest(targetDir);
|
|
78
|
+
if (!manifest) {
|
|
79
|
+
logError(`Failed to read ${MANIFEST_FILE} — file may be corrupted.`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const installedKeys = new Set(manifest.components || []);
|
|
84
|
+
|
|
85
|
+
// ── Header ─────────────────────────────────────────────────────────────
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(bold(`@dzhechkov/skills-bto v${manifest.version}`));
|
|
88
|
+
console.log(`Installed: ${manifest.installedAt ? manifest.installedAt.split('T')[0] : 'unknown'}`);
|
|
89
|
+
if (manifest.updatedAt) {
|
|
90
|
+
console.log(`Updated: ${manifest.updatedAt.split('T')[0]}`);
|
|
91
|
+
}
|
|
92
|
+
console.log('');
|
|
93
|
+
|
|
94
|
+
// ── b) Component table ─────────────────────────────────────────────────
|
|
95
|
+
const nameWidth = 28;
|
|
96
|
+
const statusWidth = 18;
|
|
97
|
+
|
|
98
|
+
console.log(
|
|
99
|
+
padRight(bold('Component'), nameWidth) +
|
|
100
|
+
padRight(bold('Status'), statusWidth) +
|
|
101
|
+
bold('Files')
|
|
102
|
+
);
|
|
103
|
+
console.log('\u2500'.repeat(55));
|
|
104
|
+
|
|
105
|
+
const allKeys = Object.keys(COMPONENTS);
|
|
106
|
+
let totalFiles = 0;
|
|
107
|
+
|
|
108
|
+
for (const key of allKeys) {
|
|
109
|
+
const comp = COMPONENTS[key];
|
|
110
|
+
const isInstalled = installedKeys.has(key);
|
|
111
|
+
|
|
112
|
+
// ── c) Count actual files on disk ───────────────────────────────────
|
|
113
|
+
let fileCount = 0;
|
|
114
|
+
let statusText;
|
|
115
|
+
|
|
116
|
+
if (isInstalled) {
|
|
117
|
+
fileCount = countComponentFiles(comp, targetDir);
|
|
118
|
+
totalFiles += fileCount;
|
|
119
|
+
|
|
120
|
+
if (fileCount > 0) {
|
|
121
|
+
statusText = green('\u2713 OK');
|
|
122
|
+
} else {
|
|
123
|
+
statusText = red('\u2717 Missing');
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
statusText = dim('\u2717 Not installed');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Extract short label (before the parenthetical)
|
|
130
|
+
const shortLabel = comp.label.split('(')[0].trim();
|
|
131
|
+
const filesStr = isInstalled ? `${fileCount} file${fileCount !== 1 ? 's' : ''}` : '';
|
|
132
|
+
const filterTag = comp.filter ? dim(` [${comp.filter}*]`) : '';
|
|
133
|
+
|
|
134
|
+
console.log(
|
|
135
|
+
padRight(shortLabel, nameWidth) +
|
|
136
|
+
padRight(statusText, statusWidth + 9) + // +9 to account for ANSI escape codes
|
|
137
|
+
dim(filesStr) +
|
|
138
|
+
filterTag
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log('\u2500'.repeat(55));
|
|
143
|
+
console.log(
|
|
144
|
+
padRight(bold('Total'), nameWidth) +
|
|
145
|
+
padRight('', statusWidth) +
|
|
146
|
+
bold(`${totalFiles} files`)
|
|
147
|
+
);
|
|
148
|
+
console.log('');
|
|
149
|
+
|
|
150
|
+
// ── d) Integration info ─────────────────────────────────────────────────
|
|
151
|
+
const keysariumPath = path.join(targetDir, '.keysarium.json');
|
|
152
|
+
if (fileExists(keysariumPath)) {
|
|
153
|
+
info(`Keysarium integration: ${green('active')}`);
|
|
154
|
+
console.log('');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = run;
|
|
161
|
+
module.exports.run = run;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const {
|
|
7
|
+
green, red, yellow, cyan, bold, dim,
|
|
8
|
+
info, success, warn, error: logError, step,
|
|
9
|
+
fileExists, readManifest,
|
|
10
|
+
MANIFEST_FILE,
|
|
11
|
+
} = require('../utils');
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Prompt the user for confirmation via readline.
|
|
19
|
+
* Resolves to true if user answers 'y' or 'yes', false otherwise.
|
|
20
|
+
*/
|
|
21
|
+
function confirmPrompt(question) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const rl = readline.createInterface({
|
|
24
|
+
input: process.stdin,
|
|
25
|
+
output: process.stdout,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
rl.question(question, (answer) => {
|
|
29
|
+
rl.close();
|
|
30
|
+
const normalized = answer.trim().toLowerCase();
|
|
31
|
+
resolve(normalized === 'y' || normalized === 'yes');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Remove a file if it exists. Returns true if removed, false otherwise.
|
|
38
|
+
*/
|
|
39
|
+
function removeFile(filePath) {
|
|
40
|
+
try {
|
|
41
|
+
if (fileExists(filePath)) {
|
|
42
|
+
fs.unlinkSync(filePath);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
warn(`Could not remove ${filePath}: ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Remove a directory if it exists and is empty.
|
|
53
|
+
* Walks up the directory tree removing empty parents up to (but not including) stopDir.
|
|
54
|
+
*
|
|
55
|
+
* IMPORTANT: For BTO, we only clean up directories that are BTO-exclusive
|
|
56
|
+
* (like .claude/skills/bto/). We do NOT remove shared directories
|
|
57
|
+
* (.claude/commands/, .claude/rules/, .claude/agents/) as they may contain
|
|
58
|
+
* files from other skill packs.
|
|
59
|
+
*/
|
|
60
|
+
function removeEmptyDirs(dirPath, stopDir) {
|
|
61
|
+
try {
|
|
62
|
+
let current = dirPath;
|
|
63
|
+
while (current !== stopDir && current !== path.dirname(current)) {
|
|
64
|
+
if (!fileExists(current)) {
|
|
65
|
+
current = path.dirname(current);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const stat = fs.statSync(current);
|
|
70
|
+
if (!stat.isDirectory()) break;
|
|
71
|
+
|
|
72
|
+
const entries = fs.readdirSync(current);
|
|
73
|
+
if (entries.length === 0) {
|
|
74
|
+
fs.rmdirSync(current);
|
|
75
|
+
current = path.dirname(current);
|
|
76
|
+
} else {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Silently ignore — directory cleanup is best-effort
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Main command
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* `@dzhechkov/skills-bto remove` — Clean uninstall of the BTO skill pack.
|
|
91
|
+
*
|
|
92
|
+
* Only removes BTO-owned files. Shared directories (.claude/commands/,
|
|
93
|
+
* .claude/rules/, .claude/agents/) are preserved if they contain non-BTO files.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} options
|
|
96
|
+
* @param {boolean} options.force — Skip confirmation prompt
|
|
97
|
+
* @param {boolean} options.dryRun — Preview without removing anything
|
|
98
|
+
* @param {string} options.targetDir — Project root directory
|
|
99
|
+
*/
|
|
100
|
+
async function run(options) {
|
|
101
|
+
const { force, dryRun, targetDir } = options;
|
|
102
|
+
const manifestPath = path.join(targetDir, MANIFEST_FILE);
|
|
103
|
+
|
|
104
|
+
// ── a) Read manifest ───────────────────────────────────────────────────
|
|
105
|
+
if (!fileExists(manifestPath)) {
|
|
106
|
+
warn('BTO skill pack is not installed in this directory — nothing to remove.');
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const manifest = readManifest(targetDir);
|
|
111
|
+
if (!manifest) {
|
|
112
|
+
logError(`Failed to read ${MANIFEST_FILE} — file may be corrupted.`);
|
|
113
|
+
info(`You can manually delete ${MANIFEST_FILE} and the installed files.`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const files = manifest.files || [];
|
|
118
|
+
const components = manifest.components || [];
|
|
119
|
+
|
|
120
|
+
// ── b) Show what will be removed ───────────────────────────────────────
|
|
121
|
+
console.log('');
|
|
122
|
+
info(bold('The following BTO components will be removed:'));
|
|
123
|
+
console.log('');
|
|
124
|
+
|
|
125
|
+
for (const key of components) {
|
|
126
|
+
console.log(` ${red('-')} ${key}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log(` ${dim(`${files.length} file(s) total`)}`);
|
|
131
|
+
console.log(` ${dim(`+ ${MANIFEST_FILE} manifest`)}`);
|
|
132
|
+
console.log('');
|
|
133
|
+
|
|
134
|
+
// Check for keysarium presence
|
|
135
|
+
const keysariumPath = path.join(targetDir, '.keysarium.json');
|
|
136
|
+
if (fileExists(keysariumPath)) {
|
|
137
|
+
info('@dzhechkov/keysarium detected — shared directories will be preserved.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (dryRun) {
|
|
141
|
+
console.log(bold('Files to be removed:'));
|
|
142
|
+
for (const relPath of files) {
|
|
143
|
+
const absPath = path.join(targetDir, relPath);
|
|
144
|
+
const exists = fileExists(absPath);
|
|
145
|
+
const marker = exists ? red('- DEL') : dim('- N/A');
|
|
146
|
+
console.log(` ${marker} ${relPath}`);
|
|
147
|
+
}
|
|
148
|
+
console.log(` ${red('- DEL')} ${MANIFEST_FILE}`);
|
|
149
|
+
console.log('');
|
|
150
|
+
warn('Dry run — no files were removed.');
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── c) Confirm unless --force ──────────────────────────────────────────
|
|
155
|
+
if (!force) {
|
|
156
|
+
const confirmed = await confirmPrompt(
|
|
157
|
+
yellow('This will remove all BTO skill pack files. Continue? (y/N) ')
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (!confirmed) {
|
|
161
|
+
info('Aborted — no files were removed.');
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── d) Remove files ────────────────────────────────────────────────────
|
|
167
|
+
let removedCount = 0;
|
|
168
|
+
let skippedCount = 0;
|
|
169
|
+
const dirsToCheck = new Set();
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < files.length; i++) {
|
|
172
|
+
const relPath = files[i];
|
|
173
|
+
const absPath = path.join(targetDir, relPath);
|
|
174
|
+
|
|
175
|
+
step(i + 1, files.length, `Removing ${relPath}`);
|
|
176
|
+
|
|
177
|
+
if (removeFile(absPath)) {
|
|
178
|
+
removedCount++;
|
|
179
|
+
// Track parent directories for cleanup
|
|
180
|
+
dirsToCheck.add(path.dirname(absPath));
|
|
181
|
+
} else {
|
|
182
|
+
skippedCount++;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Remove empty directories (bottom-up), but only BTO-exclusive ones
|
|
187
|
+
const sortedDirs = Array.from(dirsToCheck).sort((a, b) => b.length - a.length);
|
|
188
|
+
for (const dir of sortedDirs) {
|
|
189
|
+
removeEmptyDirs(dir, targetDir);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── e) Remove manifest ─────────────────────────────────────────────────
|
|
193
|
+
removeFile(manifestPath);
|
|
194
|
+
info(`Removed ${MANIFEST_FILE} manifest`);
|
|
195
|
+
|
|
196
|
+
// ── f) Summary ─────────────────────────────────────────────────────────
|
|
197
|
+
console.log('');
|
|
198
|
+
success(bold('BTO skill pack removal complete!'));
|
|
199
|
+
console.log(` ${green('\u2713')} ${removedCount} file(s) removed`);
|
|
200
|
+
if (skippedCount > 0) {
|
|
201
|
+
console.log(` ${dim('-')} ${skippedCount} file(s) already missing (skipped)`);
|
|
202
|
+
}
|
|
203
|
+
console.log('');
|
|
204
|
+
info(`To reinstall, run: ${cyan('@dzhechkov/skills-bto init')}`);
|
|
205
|
+
console.log('');
|
|
206
|
+
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = run;
|
|
211
|
+
module.exports.run = run;
|