@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.
@@ -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;