@dzhechkov/keysarium 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.
Files changed (84) hide show
  1. package/README.md +289 -0
  2. package/bin/cli.js +5 -0
  3. package/package.json +43 -0
  4. package/src/cli.js +157 -0
  5. package/src/commands/doctor.js +469 -0
  6. package/src/commands/init.js +171 -0
  7. package/src/commands/list.js +147 -0
  8. package/src/commands/remove.js +197 -0
  9. package/src/commands/update.js +191 -0
  10. package/src/utils.js +274 -0
  11. package/templates/.claude/agents/cjm-variant-worker.md +60 -0
  12. package/templates/.claude/agents/discovery-worker.md +45 -0
  13. package/templates/.claude/agents/parallel-research-orchestrator.md +70 -0
  14. package/templates/.claude/agents/presentation-worker.md +55 -0
  15. package/templates/.claude/agents/research-worker.md +43 -0
  16. package/templates/.claude/commands/architecture-phase.md +37 -0
  17. package/templates/.claude/commands/casarium.md +163 -0
  18. package/templates/.claude/commands/cjm-prototype.md +42 -0
  19. package/templates/.claude/commands/discovery.md +34 -0
  20. package/templates/.claude/commands/explore-case.md +22 -0
  21. package/templates/.claude/commands/harvest.md +67 -0
  22. package/templates/.claude/commands/new-research.md +69 -0
  23. package/templates/.claude/commands/parallel-research.md +45 -0
  24. package/templates/.claude/commands/presentation.md +49 -0
  25. package/templates/.claude/commands/research.md +32 -0
  26. package/templates/.claude/commands/solve.md +36 -0
  27. package/templates/.claude/rules/agent-swarm.md +31 -0
  28. package/templates/.claude/rules/anti-patterns.md +19 -0
  29. package/templates/.claude/rules/checkpoint-protocol.md +23 -0
  30. package/templates/.claude/rules/domain-specific.md +32 -0
  31. package/templates/.claude/rules/file-conventions.md +38 -0
  32. package/templates/.claude/rules/modular-reuse.md +35 -0
  33. package/templates/.claude/rules/research-quality.md +20 -0
  34. package/templates/.claude/settings.json +37 -0
  35. package/templates/.claude/skills/explore/SKILL.md +215 -0
  36. package/templates/.claude/skills/explore/references/questioning-techniques.md +151 -0
  37. package/templates/.claude/skills/explore/references/task-brief-templates.md +355 -0
  38. package/templates/.claude/skills/frontend-design/LICENSE.txt +177 -0
  39. package/templates/.claude/skills/frontend-design/SKILL.md +42 -0
  40. package/templates/.claude/skills/goap-research-ed25519/SKILL.md +415 -0
  41. package/templates/.claude/skills/goap-research-ed25519/references/ed25519-verification.md +658 -0
  42. package/templates/.claude/skills/goap-research-ed25519/references/research-actions.md +544 -0
  43. package/templates/.claude/skills/goap-research-ed25519/references/source-evaluation.md +560 -0
  44. package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +662 -0
  45. package/templates/.claude/skills/goap-research-ed25519/scripts/goap_planner.py +720 -0
  46. package/templates/.claude/skills/presentation-storyteller/SKILL.md +371 -0
  47. package/templates/.claude/skills/presentation-storyteller/references/example-presentation.md +273 -0
  48. package/templates/.claude/skills/presentation-storyteller/references/slide-types.md +426 -0
  49. package/templates/.claude/skills/presentation-storyteller/references/sources-index-template.md +213 -0
  50. package/templates/.claude/skills/presentation-storyteller/references/speaker-script-patterns.md +324 -0
  51. package/templates/.claude/skills/presentation-storyteller/references/storytelling-frameworks.md +270 -0
  52. package/templates/.claude/skills/problem-solver-enhanced/SKILL.md +562 -0
  53. package/templates/.claude/skills/reverse-engineering-unicorn/SKILL.md +194 -0
  54. package/templates/.claude/skills/reverse-engineering-unicorn/examples/noom-cjm-example.md +100 -0
  55. package/templates/.claude/skills/reverse-engineering-unicorn/examples/noom-module1-example.md +88 -0
  56. package/templates/.claude/skills/reverse-engineering-unicorn/modules/01-intelligence.md +328 -0
  57. package/templates/.claude/skills/reverse-engineering-unicorn/modules/02-product-customers.md +221 -0
  58. package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +206 -0
  59. package/templates/.claude/skills/reverse-engineering-unicorn/modules/03-market-competition.md +402 -0
  60. package/templates/.claude/skills/reverse-engineering-unicorn/modules/04-business-finance.md +287 -0
  61. package/templates/.claude/skills/reverse-engineering-unicorn/modules/05-growth-engine.md +290 -0
  62. package/templates/.claude/skills/reverse-engineering-unicorn/modules/06-playbook-synthesis.md +286 -0
  63. package/templates/.claude/skills/reverse-engineering-unicorn/references/blue-ocean-canvas.md +39 -0
  64. package/templates/.claude/skills/reverse-engineering-unicorn/references/industry-benchmarks.md +67 -0
  65. package/templates/.claude/skills/reverse-engineering-unicorn/references/jtbd-canvas.md +48 -0
  66. package/templates/CLAUDE.md +185 -0
  67. package/templates/TOOLKIT_HARVEST.md +29 -0
  68. package/templates/docs/keysarium/01-deployment-guide.md +768 -0
  69. package/templates/docs/keysarium/02-admin-guide.md +1170 -0
  70. package/templates/docs/keysarium/03-user-guide.md +884 -0
  71. package/templates/docs/keysarium/04-infrastructure-requirements.md +967 -0
  72. package/templates/docs/keysarium/05-architecture.md +706 -0
  73. package/templates/docs/keysarium/06-user-flows.md +925 -0
  74. package/templates/docs/skills-bto/01-deployment-guide.md +828 -0
  75. package/templates/docs/skills-bto/02-admin-guide.md +1052 -0
  76. package/templates/docs/skills-bto/03-user-guide.md +960 -0
  77. package/templates/docs/skills-bto/04-infrastructure-requirements.md +610 -0
  78. package/templates/docs/skills-bto/05-architecture.md +1112 -0
  79. package/templates/docs/skills-bto/06-user-flows.md +977 -0
  80. package/templates/lib/agent-patterns.md +115 -0
  81. package/templates/lib/domain-templates.md +172 -0
  82. package/templates/lib/phase-utils.md +107 -0
  83. package/templates/lib/skill-loader.md +78 -0
  84. package/templates/researches/.gitkeep +0 -0
@@ -0,0 +1,147 @@
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,
9
+ COMPONENTS, MANIFEST_FILE,
10
+ } = require('../utils');
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Count files on disk for a given component in the target directory.
18
+ * Returns the number of files that actually exist.
19
+ */
20
+ function countComponentFiles(comp, targetDir) {
21
+ const destPath = path.join(targetDir, comp.src);
22
+
23
+ if (!fileExists(destPath)) {
24
+ return 0;
25
+ }
26
+
27
+ if (comp.isFile) {
28
+ return 1;
29
+ }
30
+
31
+ try {
32
+ const stat = fs.statSync(destPath);
33
+ if (stat.isDirectory()) {
34
+ return getRelativePaths(destPath).length;
35
+ }
36
+ } catch {
37
+ return 0;
38
+ }
39
+
40
+ return 0;
41
+ }
42
+
43
+ /**
44
+ * Pad or truncate a string to a fixed width.
45
+ */
46
+ function padRight(str, width) {
47
+ if (str.length >= width) return str.slice(0, width);
48
+ return str + ' '.repeat(width - str.length);
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Main command
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /**
56
+ * `@dzhechko/keysarium list` — Show installed components and their status.
57
+ *
58
+ * @param {object} options
59
+ * @param {string} options.targetDir — Project root directory
60
+ */
61
+ async function run(options) {
62
+ const { targetDir } = options;
63
+ const manifestPath = path.join(targetDir, MANIFEST_FILE);
64
+
65
+ // ── a) Read manifest ───────────────────────────────────────────────────
66
+ if (!fileExists(manifestPath)) {
67
+ warn('Keysarium is not installed in this directory.');
68
+ info(`Run ${cyan('@dzhechko/keysarium init')} to install.`);
69
+ process.exit(1);
70
+ }
71
+
72
+ const manifest = readManifest(targetDir);
73
+ if (!manifest) {
74
+ logError('Failed to read .keysarium.json — file may be corrupted.');
75
+ process.exit(1);
76
+ }
77
+
78
+ const installedKeys = new Set(manifest.components || []);
79
+
80
+ // ── Header ─────────────────────────────────────────────────────────────
81
+ console.log('');
82
+ console.log(bold(`@dzhechko/keysarium v${manifest.version}`));
83
+ console.log(`Installed: ${manifest.installedAt ? manifest.installedAt.split('T')[0] : 'unknown'}`);
84
+ if (manifest.updatedAt) {
85
+ console.log(`Updated: ${manifest.updatedAt.split('T')[0]}`);
86
+ }
87
+ console.log('');
88
+
89
+ // ── b) Component table ─────────────────────────────────────────────────
90
+ const nameWidth = 28;
91
+ const statusWidth = 18;
92
+
93
+ console.log(
94
+ padRight(bold('Component'), nameWidth) +
95
+ padRight(bold('Status'), statusWidth) +
96
+ bold('Files')
97
+ );
98
+ console.log('\u2500'.repeat(55));
99
+
100
+ const allKeys = Object.keys(COMPONENTS);
101
+ let totalFiles = 0;
102
+
103
+ for (const key of allKeys) {
104
+ const comp = COMPONENTS[key];
105
+ const isInstalled = installedKeys.has(key);
106
+
107
+ // ── c) Count actual files on disk ───────────────────────────────────
108
+ let fileCount = 0;
109
+ let statusText;
110
+
111
+ if (isInstalled) {
112
+ fileCount = countComponentFiles(comp, targetDir);
113
+ totalFiles += fileCount;
114
+
115
+ if (fileCount > 0) {
116
+ statusText = green('\u2713 OK');
117
+ } else {
118
+ statusText = red('\u2717 Missing');
119
+ }
120
+ } else {
121
+ statusText = dim('\u2717 Not installed');
122
+ }
123
+
124
+ // Extract short label (before the parenthetical)
125
+ const shortLabel = comp.label.split('(')[0].trim();
126
+ const filesStr = isInstalled ? `${fileCount} file${fileCount !== 1 ? 's' : ''}` : '';
127
+
128
+ console.log(
129
+ padRight(shortLabel, nameWidth) +
130
+ padRight(statusText, statusWidth + 9) + // +9 to account for ANSI escape codes
131
+ dim(filesStr)
132
+ );
133
+ }
134
+
135
+ console.log('\u2500'.repeat(55));
136
+ console.log(
137
+ padRight(bold('Total'), nameWidth) +
138
+ padRight('', statusWidth) +
139
+ bold(`${totalFiles} files`)
140
+ );
141
+ console.log('');
142
+
143
+ process.exit(0);
144
+ }
145
+
146
+ module.exports = run;
147
+ module.exports.run = run;
@@ -0,0 +1,197 @@
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
+ function removeEmptyDirs(dirPath, stopDir) {
56
+ try {
57
+ let current = dirPath;
58
+ while (current !== stopDir && current !== path.dirname(current)) {
59
+ if (!fileExists(current)) {
60
+ current = path.dirname(current);
61
+ continue;
62
+ }
63
+
64
+ const stat = fs.statSync(current);
65
+ if (!stat.isDirectory()) break;
66
+
67
+ const entries = fs.readdirSync(current);
68
+ if (entries.length === 0) {
69
+ fs.rmdirSync(current);
70
+ current = path.dirname(current);
71
+ } else {
72
+ break;
73
+ }
74
+ }
75
+ } catch {
76
+ // Silently ignore — directory cleanup is best-effort
77
+ }
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Main command
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /**
85
+ * `@dzhechko/keysarium remove` — Clean uninstall of the toolkit.
86
+ *
87
+ * @param {object} options
88
+ * @param {boolean} options.force — Skip confirmation prompt
89
+ * @param {boolean} options.dryRun — Preview without removing anything
90
+ * @param {string} options.targetDir — Project root directory
91
+ */
92
+ async function run(options) {
93
+ const { force, dryRun, targetDir } = options;
94
+ const manifestPath = path.join(targetDir, MANIFEST_FILE);
95
+
96
+ // ── a) Read manifest ───────────────────────────────────────────────────
97
+ if (!fileExists(manifestPath)) {
98
+ warn('Keysarium is not installed in this directory — nothing to remove.');
99
+ process.exit(0);
100
+ }
101
+
102
+ const manifest = readManifest(targetDir);
103
+ if (!manifest) {
104
+ logError('Failed to read .keysarium.json — file may be corrupted.');
105
+ info('You can manually delete .keysarium.json and the installed files.');
106
+ process.exit(1);
107
+ }
108
+
109
+ const files = manifest.files || [];
110
+ const components = manifest.components || [];
111
+
112
+ // ── b) Show what will be removed ───────────────────────────────────────
113
+ console.log('');
114
+ info(bold('The following will be removed:'));
115
+ console.log('');
116
+
117
+ for (const key of components) {
118
+ console.log(` ${red('-')} ${key}`);
119
+ }
120
+
121
+ console.log('');
122
+ console.log(` ${dim(`${files.length} file(s) total`)}`);
123
+ console.log(` ${dim('+ .keysarium.json manifest')}`);
124
+ console.log('');
125
+
126
+ if (dryRun) {
127
+ console.log(bold('Files to be removed:'));
128
+ for (const relPath of files) {
129
+ const absPath = path.join(targetDir, relPath);
130
+ const exists = fileExists(absPath);
131
+ const marker = exists ? red('- DEL') : dim('- N/A');
132
+ console.log(` ${marker} ${relPath}`);
133
+ }
134
+ console.log(` ${red('- DEL')} ${MANIFEST_FILE}`);
135
+ console.log('');
136
+ warn('Dry run — no files were removed.');
137
+ process.exit(0);
138
+ }
139
+
140
+ // ── c) Confirm unless --force ──────────────────────────────────────────
141
+ if (!force) {
142
+ const confirmed = await confirmPrompt(
143
+ yellow('This will remove all Keysarium files. Continue? (y/N) ')
144
+ );
145
+
146
+ if (!confirmed) {
147
+ info('Aborted — no files were removed.');
148
+ process.exit(0);
149
+ }
150
+ }
151
+
152
+ // ── d) Remove files ────────────────────────────────────────────────────
153
+ let removedCount = 0;
154
+ let skippedCount = 0;
155
+ const dirsToCheck = new Set();
156
+
157
+ for (let i = 0; i < files.length; i++) {
158
+ const relPath = files[i];
159
+ const absPath = path.join(targetDir, relPath);
160
+
161
+ step(i + 1, files.length, `Removing ${relPath}`);
162
+
163
+ if (removeFile(absPath)) {
164
+ removedCount++;
165
+ // Track parent directories for cleanup
166
+ dirsToCheck.add(path.dirname(absPath));
167
+ } else {
168
+ skippedCount++;
169
+ }
170
+ }
171
+
172
+ // Remove empty directories (bottom-up)
173
+ const sortedDirs = Array.from(dirsToCheck).sort((a, b) => b.length - a.length);
174
+ for (const dir of sortedDirs) {
175
+ removeEmptyDirs(dir, targetDir);
176
+ }
177
+
178
+ // ── e) Remove manifest ─────────────────────────────────────────────────
179
+ removeFile(manifestPath);
180
+ info('Removed .keysarium.json manifest');
181
+
182
+ // ── f) Summary ─────────────────────────────────────────────────────────
183
+ console.log('');
184
+ success(bold('Removal complete!'));
185
+ console.log(` ${green('\u2713')} ${removedCount} file(s) removed`);
186
+ if (skippedCount > 0) {
187
+ console.log(` ${dim('-')} ${skippedCount} file(s) already missing (skipped)`);
188
+ }
189
+ console.log('');
190
+ info(`To reinstall, run: ${cyan('@dzhechko/keysarium init')}`);
191
+ console.log('');
192
+
193
+ process.exit(0);
194
+ }
195
+
196
+ module.exports = run;
197
+ module.exports.run = run;
@@ -0,0 +1,191 @@
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, fileExists, readJSON,
9
+ ensureDir, getRelativePaths, diffFiles,
10
+ readManifest, writeManifest, getTemplatesDir,
11
+ COMPONENTS, MANIFEST_FILE,
12
+ } = require('../utils');
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Main command
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * `@dzhechko/keysarium update` — Update an existing toolkit installation.
20
+ *
21
+ * @param {object} options
22
+ * @param {boolean} options.dryRun — Preview without writing anything
23
+ * @param {boolean} options.force — Passed through (unused here)
24
+ * @param {string} options.targetDir — Destination project root
25
+ */
26
+ async function run(options) {
27
+ const { dryRun, targetDir } = options;
28
+ const manifestPath = path.join(targetDir, MANIFEST_FILE);
29
+
30
+ // ── a) Read existing manifest ───────────────────────────────────────────
31
+ if (!fileExists(manifestPath)) {
32
+ logError('Keysarium is not installed in this directory.');
33
+ info(`Run ${cyan('@dzhechko/keysarium init')} to install.`);
34
+ process.exit(1);
35
+ }
36
+
37
+ const manifest = readManifest(targetDir);
38
+ if (!manifest) {
39
+ logError('Failed to read .keysarium.json — file may be corrupted.');
40
+ process.exit(1);
41
+ }
42
+
43
+ const templatesDir = getTemplatesDir();
44
+
45
+ // ── b) Get installed components ─────────────────────────────────────────
46
+ const installedKeys = manifest.components || [];
47
+ if (installedKeys.length === 0) {
48
+ warn('Manifest lists no installed components. Consider running init instead.');
49
+ process.exit(1);
50
+ }
51
+
52
+ info(`Current version: ${dim(manifest.version)}`);
53
+ const pkgPath = path.resolve(__dirname, '../../package.json');
54
+ const pkg = readJSON(pkgPath);
55
+ const newVersion = pkg ? pkg.version : manifest.version;
56
+ info(`Available version: ${bold(newVersion)}`);
57
+ console.log('');
58
+
59
+ // ── c) Diff each component ──────────────────────────────────────────────
60
+ let totalAdded = 0;
61
+ let totalModified = 0;
62
+ let totalUnchanged = 0;
63
+ const filesToCopy = []; // { src, dest, status, relPath }
64
+
65
+ for (const key of installedKeys) {
66
+ const comp = COMPONENTS[key];
67
+ if (!comp) {
68
+ warn(`Unknown component "${key}" in manifest — skipping.`);
69
+ continue;
70
+ }
71
+
72
+ const srcBase = path.join(templatesDir, comp.src);
73
+ const destBase = path.join(targetDir, comp.src);
74
+
75
+ if (!fileExists(srcBase)) {
76
+ warn(`Template source not found for "${key}" — skipping.`);
77
+ continue;
78
+ }
79
+
80
+ if (comp.isFile) {
81
+ // Single file comparison
82
+ if (!fileExists(destBase)) {
83
+ filesToCopy.push({ src: srcBase, dest: destBase, status: 'added', relPath: comp.src });
84
+ totalAdded++;
85
+ } else {
86
+ const srcContent = fs.readFileSync(srcBase);
87
+ const destContent = fs.readFileSync(destBase);
88
+ if (srcContent.equals(destContent)) {
89
+ totalUnchanged++;
90
+ } else {
91
+ filesToCopy.push({ src: srcBase, dest: destBase, status: 'modified', relPath: comp.src });
92
+ totalModified++;
93
+ }
94
+ }
95
+ } else {
96
+ // Directory — use diffFiles utility
97
+ const diff = diffFiles(srcBase, destBase);
98
+
99
+ for (const rel of diff.added) {
100
+ filesToCopy.push({
101
+ src: path.join(srcBase, rel),
102
+ dest: path.join(destBase, rel),
103
+ status: 'added',
104
+ relPath: path.join(comp.src, rel),
105
+ });
106
+ totalAdded++;
107
+ }
108
+
109
+ for (const rel of diff.modified) {
110
+ filesToCopy.push({
111
+ src: path.join(srcBase, rel),
112
+ dest: path.join(destBase, rel),
113
+ status: 'modified',
114
+ relPath: path.join(comp.src, rel),
115
+ });
116
+ totalModified++;
117
+ }
118
+
119
+ totalUnchanged += diff.unchanged.length;
120
+ }
121
+ }
122
+
123
+ // ── Show diff summary ───────────────────────────────────────────────────
124
+ info(bold('Update summary:'));
125
+ console.log(` ${green('+')} ${totalAdded} file(s) to add`);
126
+ console.log(` ${yellow('~')} ${totalModified} file(s) to update`);
127
+ console.log(` ${dim('=')} ${totalUnchanged} file(s) unchanged`);
128
+ console.log('');
129
+
130
+ if (totalAdded === 0 && totalModified === 0) {
131
+ success('Everything is up to date!');
132
+ process.exit(0);
133
+ }
134
+
135
+ if (dryRun) {
136
+ console.log(bold('Files to be changed:'));
137
+ for (const f of filesToCopy) {
138
+ const marker = f.status === 'added' ? green('+ ADD') : yellow('~ MOD');
139
+ console.log(` ${marker} ${f.relPath}`);
140
+ }
141
+ console.log('');
142
+ warn('Dry run — no files were written.');
143
+ process.exit(0);
144
+ }
145
+
146
+ // ── d) Copy updated files ──────────────────────────────────────────────
147
+ for (let i = 0; i < filesToCopy.length; i++) {
148
+ const f = filesToCopy[i];
149
+ const label = f.status === 'added' ? 'Adding' : 'Updating';
150
+ step(i + 1, filesToCopy.length, `${label} ${f.relPath}`);
151
+
152
+ ensureDir(path.dirname(f.dest));
153
+ fs.copyFileSync(f.src, f.dest);
154
+ }
155
+
156
+ // ── e) Update manifest ─────────────────────────────────────────────────
157
+ const allFiles = [];
158
+ for (const key of installedKeys) {
159
+ const comp = COMPONENTS[key];
160
+ if (!comp) continue;
161
+ const destPath = path.join(targetDir, comp.src);
162
+ if (comp.isFile) {
163
+ if (fileExists(destPath)) {
164
+ allFiles.push(comp.src);
165
+ }
166
+ } else if (fileExists(destPath)) {
167
+ const paths = getRelativePaths(destPath);
168
+ allFiles.push(...paths.map((rel) => path.join(comp.src, rel)));
169
+ }
170
+ }
171
+
172
+ manifest.version = newVersion;
173
+ manifest.updatedAt = new Date().toISOString();
174
+ manifest.files = allFiles.sort();
175
+
176
+ writeManifest(targetDir, manifest);
177
+ info('Updated .keysarium.json manifest');
178
+
179
+ // ── f) Summary ─────────────────────────────────────────────────────────
180
+ console.log('');
181
+ success(bold('Update complete!'));
182
+ console.log(` ${green('+')} ${totalAdded} file(s) added`);
183
+ console.log(` ${yellow('~')} ${totalModified} file(s) updated`);
184
+ console.log(` ${dim('=')} ${totalUnchanged} file(s) unchanged`);
185
+ console.log('');
186
+
187
+ process.exit(0);
188
+ }
189
+
190
+ module.exports = run;
191
+ module.exports.run = run;