liteagents 2.4.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 (215) hide show
  1. package/CHANGELOG.md +441 -0
  2. package/LICENSE +21 -0
  3. package/README.md +179 -0
  4. package/cli.js +230 -0
  5. package/docs/.gitkeep +1 -0
  6. package/docs/CONTRIBUTING.md +739 -0
  7. package/docs/DUAL_PUBLISH_SUMMARY.md +177 -0
  8. package/docs/ERROR_HANDLING_IMPLEMENTATION.md +327 -0
  9. package/docs/GITHUB_PACKAGES.md +181 -0
  10. package/docs/GITHUB_SETUP.md +158 -0
  11. package/docs/INSTALLATION_DEMO.md +691 -0
  12. package/docs/INSTALLATION_LOCATIONS.md +299 -0
  13. package/docs/INSTALLER_GUIDE.md +1586 -0
  14. package/docs/INTEGRATION_ISSUES_9.1.md +341 -0
  15. package/docs/KNOWLEDGE_BASE.md +727 -0
  16. package/docs/MIGRATION.md +384 -0
  17. package/docs/PACKAGE_BASELINE.md +557 -0
  18. package/docs/PACKAGE_VALIDATION_REPORT.md +427 -0
  19. package/docs/PASS_INTEGRATION.md +307 -0
  20. package/docs/PASS_QUICK_START.md +150 -0
  21. package/docs/PRIVACY.md +203 -0
  22. package/docs/PUBLISHING.md +494 -0
  23. package/docs/QUICK-START.md +318 -0
  24. package/docs/RELEASE_NOTES_1.2.0.md +323 -0
  25. package/docs/SECURITY.md +317 -0
  26. package/docs/SILENT_MODE_GUIDE.md +526 -0
  27. package/docs/SKILLS_CONVERSION.md +154 -0
  28. package/docs/TESTING.md +582 -0
  29. package/docs/TEST_COVERAGE.md +347 -0
  30. package/docs/TROUBLESHOOTING.md +788 -0
  31. package/docs/UPDATED_VARIANT_CONFIGURATION.md +274 -0
  32. package/docs/VARIANT_CONFIGURATION.md +440 -0
  33. package/installer/cli.js +761 -0
  34. package/installer/installation-engine.js +1536 -0
  35. package/installer/package-manager.js +640 -0
  36. package/installer/path-manager.js +427 -0
  37. package/installer/report-template.js +298 -0
  38. package/installer/verification-system.js +274 -0
  39. package/package.json +83 -0
  40. package/packages/ampcode/AGENT.md +58 -0
  41. package/packages/ampcode/README.md +17 -0
  42. package/packages/ampcode/agents/1-create-prd.md +175 -0
  43. package/packages/ampcode/agents/2-generate-tasks.md +190 -0
  44. package/packages/ampcode/agents/3-process-task-list.md +225 -0
  45. package/packages/ampcode/agents/code-developer.md +198 -0
  46. package/packages/ampcode/agents/context-builder.md +142 -0
  47. package/packages/ampcode/agents/feature-planner.md +199 -0
  48. package/packages/ampcode/agents/market-researcher.md +89 -0
  49. package/packages/ampcode/agents/orchestrator.md +116 -0
  50. package/packages/ampcode/agents/quality-assurance.md +115 -0
  51. package/packages/ampcode/agents/system-architect.md +135 -0
  52. package/packages/ampcode/agents/ui-designer.md +184 -0
  53. package/packages/ampcode/commands/brainstorming.md +56 -0
  54. package/packages/ampcode/commands/code-review.md +107 -0
  55. package/packages/ampcode/commands/condition-based-waiting/example.ts +158 -0
  56. package/packages/ampcode/commands/condition-based-waiting.md +122 -0
  57. package/packages/ampcode/commands/debug.md +20 -0
  58. package/packages/ampcode/commands/docs-builder/templates.md +572 -0
  59. package/packages/ampcode/commands/docs-builder.md +106 -0
  60. package/packages/ampcode/commands/explain.md +18 -0
  61. package/packages/ampcode/commands/git-commit.md +14 -0
  62. package/packages/ampcode/commands/optimize.md +20 -0
  63. package/packages/ampcode/commands/refactor.md +21 -0
  64. package/packages/ampcode/commands/review.md +18 -0
  65. package/packages/ampcode/commands/root-cause-tracing/find-polluter.sh +63 -0
  66. package/packages/ampcode/commands/root-cause-tracing.md +176 -0
  67. package/packages/ampcode/commands/security.md +21 -0
  68. package/packages/ampcode/commands/ship.md +18 -0
  69. package/packages/ampcode/commands/skill-creator/scripts/init_skill.py +303 -0
  70. package/packages/ampcode/commands/skill-creator/scripts/package_skill.py +110 -0
  71. package/packages/ampcode/commands/skill-creator/scripts/quick_validate.py +65 -0
  72. package/packages/ampcode/commands/skill-creator.md +211 -0
  73. package/packages/ampcode/commands/stash.md +45 -0
  74. package/packages/ampcode/commands/systematic-debugging.md +297 -0
  75. package/packages/ampcode/commands/test-driven-development.md +390 -0
  76. package/packages/ampcode/commands/test-generate.md +18 -0
  77. package/packages/ampcode/commands/testing-anti-patterns.md +304 -0
  78. package/packages/ampcode/commands/verification-before-completion.md +152 -0
  79. package/packages/ampcode/settings.json +13 -0
  80. package/packages/ampcode/variants.json +8 -0
  81. package/packages/claude/CLAUDE.md +58 -0
  82. package/packages/claude/README.md +23 -0
  83. package/packages/claude/agents/1-create-prd.md +175 -0
  84. package/packages/claude/agents/2-generate-tasks.md +190 -0
  85. package/packages/claude/agents/3-process-task-list.md +225 -0
  86. package/packages/claude/agents/code-developer.md +198 -0
  87. package/packages/claude/agents/context-builder.md +142 -0
  88. package/packages/claude/agents/feature-planner.md +199 -0
  89. package/packages/claude/agents/market-researcher.md +89 -0
  90. package/packages/claude/agents/orchestrator.md +117 -0
  91. package/packages/claude/agents/quality-assurance.md +115 -0
  92. package/packages/claude/agents/system-architect.md +135 -0
  93. package/packages/claude/agents/ui-designer.md +184 -0
  94. package/packages/claude/commands/debug.md +20 -0
  95. package/packages/claude/commands/explain.md +18 -0
  96. package/packages/claude/commands/git-commit.md +14 -0
  97. package/packages/claude/commands/optimize.md +20 -0
  98. package/packages/claude/commands/refactor.md +21 -0
  99. package/packages/claude/commands/review.md +18 -0
  100. package/packages/claude/commands/security.md +21 -0
  101. package/packages/claude/commands/ship.md +18 -0
  102. package/packages/claude/commands/stash.md +45 -0
  103. package/packages/claude/commands/test-generate.md +18 -0
  104. package/packages/claude/skills/brainstorming/SKILL.md +56 -0
  105. package/packages/claude/skills/code-review/SKILL.md +107 -0
  106. package/packages/claude/skills/code-review/code-reviewer.md +146 -0
  107. package/packages/claude/skills/condition-based-waiting/SKILL.md +122 -0
  108. package/packages/claude/skills/condition-based-waiting/example.ts +158 -0
  109. package/packages/claude/skills/docs-builder/SKILL.md +106 -0
  110. package/packages/claude/skills/docs-builder/references/templates.md +572 -0
  111. package/packages/claude/skills/root-cause-tracing/SKILL.md +176 -0
  112. package/packages/claude/skills/root-cause-tracing/find-polluter.sh +63 -0
  113. package/packages/claude/skills/skill-creator/LICENSE.txt +202 -0
  114. package/packages/claude/skills/skill-creator/SKILL.md +211 -0
  115. package/packages/claude/skills/skill-creator/scripts/init_skill.py +303 -0
  116. package/packages/claude/skills/skill-creator/scripts/package_skill.py +110 -0
  117. package/packages/claude/skills/skill-creator/scripts/quick_validate.py +65 -0
  118. package/packages/claude/skills/systematic-debugging/CREATION-LOG.md +119 -0
  119. package/packages/claude/skills/systematic-debugging/SKILL.md +296 -0
  120. package/packages/claude/skills/systematic-debugging/test-academic.md +14 -0
  121. package/packages/claude/skills/systematic-debugging/test-pressure-1.md +58 -0
  122. package/packages/claude/skills/systematic-debugging/test-pressure-2.md +68 -0
  123. package/packages/claude/skills/systematic-debugging/test-pressure-3.md +69 -0
  124. package/packages/claude/skills/test-driven-development/SKILL.md +392 -0
  125. package/packages/claude/skills/testing-anti-patterns/SKILL.md +304 -0
  126. package/packages/claude/skills/verification-before-completion/SKILL.md +152 -0
  127. package/packages/claude/variants.json +9 -0
  128. package/packages/droid/AGENTS.md +52 -0
  129. package/packages/droid/README.md +17 -0
  130. package/packages/droid/change_settings.json +61 -0
  131. package/packages/droid/commands/brainstorming.md +56 -0
  132. package/packages/droid/commands/code-review.md +107 -0
  133. package/packages/droid/commands/condition-based-waiting/example.ts +158 -0
  134. package/packages/droid/commands/condition-based-waiting.md +122 -0
  135. package/packages/droid/commands/debug.md +20 -0
  136. package/packages/droid/commands/docs-builder/templates.md +572 -0
  137. package/packages/droid/commands/docs-builder.md +106 -0
  138. package/packages/droid/commands/explain.md +18 -0
  139. package/packages/droid/commands/git-commit.md +14 -0
  140. package/packages/droid/commands/optimize.md +20 -0
  141. package/packages/droid/commands/refactor.md +21 -0
  142. package/packages/droid/commands/review.md +18 -0
  143. package/packages/droid/commands/root-cause-tracing/find-polluter.sh +63 -0
  144. package/packages/droid/commands/root-cause-tracing.md +176 -0
  145. package/packages/droid/commands/security.md +21 -0
  146. package/packages/droid/commands/ship.md +18 -0
  147. package/packages/droid/commands/skill-creator/scripts/init_skill.py +303 -0
  148. package/packages/droid/commands/skill-creator/scripts/package_skill.py +110 -0
  149. package/packages/droid/commands/skill-creator/scripts/quick_validate.py +65 -0
  150. package/packages/droid/commands/skill-creator.md +211 -0
  151. package/packages/droid/commands/stash.md +45 -0
  152. package/packages/droid/commands/systematic-debugging.md +297 -0
  153. package/packages/droid/commands/test-driven-development.md +390 -0
  154. package/packages/droid/commands/test-generate.md +18 -0
  155. package/packages/droid/commands/testing-anti-patterns.md +304 -0
  156. package/packages/droid/commands/verification-before-completion.md +152 -0
  157. package/packages/droid/droids/1-create-prd.md +170 -0
  158. package/packages/droid/droids/2-generate-tasks.md +190 -0
  159. package/packages/droid/droids/3-process-task-list.md +225 -0
  160. package/packages/droid/droids/code-developer.md +198 -0
  161. package/packages/droid/droids/context-builder.md +142 -0
  162. package/packages/droid/droids/feature-planner.md +199 -0
  163. package/packages/droid/droids/market-researcher.md +89 -0
  164. package/packages/droid/droids/orchestrator.md +116 -0
  165. package/packages/droid/droids/quality-assurance.md +115 -0
  166. package/packages/droid/droids/system-architect.md +135 -0
  167. package/packages/droid/droids/ui-designer.md +184 -0
  168. package/packages/droid/variants.json +8 -0
  169. package/packages/opencode/AGENTS.md +52 -0
  170. package/packages/opencode/README.md +17 -0
  171. package/packages/opencode/agent/1-create-prd.md +179 -0
  172. package/packages/opencode/agent/2-generate-tasks.md +194 -0
  173. package/packages/opencode/agent/3-process-task-list.md +229 -0
  174. package/packages/opencode/agent/code-developer.md +202 -0
  175. package/packages/opencode/agent/context-builder.md +146 -0
  176. package/packages/opencode/agent/feature-planner.md +203 -0
  177. package/packages/opencode/agent/market-researcher.md +93 -0
  178. package/packages/opencode/agent/orchestrator.md +120 -0
  179. package/packages/opencode/agent/quality-assurance.md +119 -0
  180. package/packages/opencode/agent/system-architect.md +139 -0
  181. package/packages/opencode/agent/ui-designer.md +188 -0
  182. package/packages/opencode/command/brainstorming.md +56 -0
  183. package/packages/opencode/command/code-review.md +107 -0
  184. package/packages/opencode/command/condition-based-waiting/example.ts +158 -0
  185. package/packages/opencode/command/condition-based-waiting.md +122 -0
  186. package/packages/opencode/command/debug.md +20 -0
  187. package/packages/opencode/command/docs-builder/templates.md +572 -0
  188. package/packages/opencode/command/docs-builder.md +106 -0
  189. package/packages/opencode/command/explain.md +18 -0
  190. package/packages/opencode/command/git-commit.md +14 -0
  191. package/packages/opencode/command/optimize.md +20 -0
  192. package/packages/opencode/command/refactor.md +21 -0
  193. package/packages/opencode/command/review.md +18 -0
  194. package/packages/opencode/command/root-cause-tracing/find-polluter.sh +63 -0
  195. package/packages/opencode/command/root-cause-tracing.md +176 -0
  196. package/packages/opencode/command/security.md +21 -0
  197. package/packages/opencode/command/ship.md +18 -0
  198. package/packages/opencode/command/skill-creator/scripts/init_skill.py +303 -0
  199. package/packages/opencode/command/skill-creator/scripts/package_skill.py +110 -0
  200. package/packages/opencode/command/skill-creator/scripts/quick_validate.py +65 -0
  201. package/packages/opencode/command/skill-creator.md +211 -0
  202. package/packages/opencode/command/stash.md +45 -0
  203. package/packages/opencode/command/systematic-debugging.md +297 -0
  204. package/packages/opencode/command/test-driven-development.md +390 -0
  205. package/packages/opencode/command/test-generate.md +18 -0
  206. package/packages/opencode/command/testing-anti-patterns.md +304 -0
  207. package/packages/opencode/command/verification-before-completion.md +152 -0
  208. package/packages/opencode/opencode.jsonc +201 -0
  209. package/packages/opencode/variants.json +8 -0
  210. package/packages/subagentic-manual.md +349 -0
  211. package/postinstall.js +21 -0
  212. package/tools/ampcode/manifest-template.json +14 -0
  213. package/tools/claude/manifest-template.json +14 -0
  214. package/tools/droid/manifest-template.json +14 -0
  215. package/tools/opencode/manifest-template.json +14 -0
@@ -0,0 +1,1536 @@
1
+ /**
2
+ * Installation Engine for Agentic Kit Installer
3
+ *
4
+ * Handles file operations, installation progress, and rollback functionality
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ class InstallationEngine {
11
+ constructor(pathManager, packageManager) {
12
+ this.pathManager = pathManager;
13
+ this.packageManager = packageManager;
14
+ this.installationLog = [];
15
+ this.backupLog = [];
16
+ this.rollbackLog = [];
17
+ this.sessionLog = {
18
+ installedFiles: [],
19
+ targetPath: null,
20
+ tool: null
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Install multiple tools with state management for resume capability
26
+ *
27
+ * @param {string} variant - Variant name ('lite', 'standard', 'pro')
28
+ * @param {Array<string>} tools - Array of tool IDs to install
29
+ * @param {Object} paths - Map of tool IDs to installation paths
30
+ * @param {function} progressCallback - Optional callback for progress updates
31
+ * @param {boolean} resume - Whether this is resuming a previous installation
32
+ * @returns {Object} - Installation result summary
33
+ */
34
+ async installMultipleTools(variant, tools, paths, progressCallback = null, resume = false) {
35
+ const results = {
36
+ successful: [],
37
+ failed: [],
38
+ skipped: []
39
+ };
40
+
41
+ // Initialize or load state
42
+ if (!resume) {
43
+ this.stateManager.initializeState(variant, tools, paths);
44
+ await this.stateManager.saveState({ stage: 'initializing' });
45
+ }
46
+
47
+ const state = this.stateManager.getState();
48
+ if (!state) {
49
+ throw new Error('Failed to initialize installation state');
50
+ }
51
+
52
+ try {
53
+ // Process each tool
54
+ for (const toolId of tools) {
55
+ // Skip if already completed
56
+ if (state.completedTools.includes(toolId)) {
57
+ console.log(`Skipping ${toolId} (already completed)`);
58
+ results.skipped.push(toolId);
59
+ continue;
60
+ }
61
+
62
+ // Skip if previously failed (don't retry automatically)
63
+ if (state.failedTools.some(f => f.toolId === toolId)) {
64
+ console.log(`Skipping ${toolId} (previously failed)`);
65
+ results.skipped.push(toolId);
66
+ continue;
67
+ }
68
+
69
+ try {
70
+ // Install this tool
71
+ await this.installPackage(toolId, variant, paths[toolId], progressCallback);
72
+
73
+ // Mark tool as completed in state
74
+ await this.stateManager.completeCurrentTool();
75
+
76
+ results.successful.push(toolId);
77
+
78
+ } catch (error) {
79
+ console.error(`Failed to install ${toolId}: ${error.message}`);
80
+
81
+ // Mark tool as failed in state
82
+ await this.stateManager.failCurrentTool(error);
83
+
84
+ results.failed.push({
85
+ toolId: toolId,
86
+ error: error.message
87
+ });
88
+
89
+ // Continue with next tool (don't stop entire installation)
90
+ }
91
+ }
92
+
93
+ // All tools processed - clear state if all successful
94
+ if (results.failed.length === 0) {
95
+ await this.stateManager.clearState();
96
+ }
97
+
98
+ return results;
99
+
100
+ } catch (error) {
101
+ console.error(`Installation process failed: ${error.message}`);
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Install package for a specific tool and variant
108
+ * Uses variant-aware PackageManager methods to install only selected content
109
+ *
110
+ * @param {string} toolId - Tool identifier (e.g., 'claude', 'opencode')
111
+ * @param {string} variant - Variant name ('lite', 'standard', 'pro')
112
+ * @param {string} targetPath - Installation target path
113
+ * @param {function} progressCallback - Optional callback for progress updates
114
+ */
115
+ async installPackage(toolId, variant, targetPath, progressCallback = null) {
116
+ // Use base package directory from PackageManager (not variant-specific)
117
+ const sourceDir = path.join(this.packageManager.packagesDir, toolId);
118
+ const expandedTargetPath = this.pathManager.expandPath(targetPath);
119
+
120
+ // Initialize session log for this installation
121
+ this.sessionLog = {
122
+ installedFiles: [],
123
+ targetPath: expandedTargetPath,
124
+ tool: toolId
125
+ };
126
+
127
+ try {
128
+ // Validate source package with variant support
129
+ const validation = await this.packageManager.validatePackage(toolId, variant);
130
+ if (!validation.valid) {
131
+ throw new Error(`Invalid package: ${validation.error}`);
132
+ }
133
+
134
+ // Check for existing installation
135
+ const existing = await this.pathManager.checkExistingInstallation(targetPath);
136
+ if (existing.exists) {
137
+ await this.createBackup(expandedTargetPath);
138
+ }
139
+
140
+ // Create target directory
141
+ await fs.promises.mkdir(expandedTargetPath, { recursive: true });
142
+
143
+ // Get variant-selected content from PackageManager
144
+ const packageContents = await this.packageManager.getPackageContents(toolId, variant);
145
+
146
+ // Copy only variant-selected files (not entire directory)
147
+ await this.copySelectedFiles(sourceDir, expandedTargetPath, packageContents, progressCallback);
148
+
149
+ // Generate manifest with variant information
150
+ await this.generateManifest(toolId, variant, expandedTargetPath);
151
+
152
+ // Log installation
153
+ this.installationLog.push({
154
+ tool: toolId,
155
+ variant,
156
+ source: sourceDir,
157
+ target: expandedTargetPath,
158
+ timestamp: new Date().toISOString()
159
+ });
160
+
161
+ } catch (error) {
162
+ console.error(`✗ Failed to install ${toolId}: ${error.message}`);
163
+
164
+ // Attempt rollback
165
+ await this.rollbackInstallation(toolId, expandedTargetPath);
166
+
167
+ throw error;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Copy only variant-selected files to target directory
173
+ * Maintains directory structure for each component category
174
+ * Calls progress callback with real-time progress information
175
+ *
176
+ * @param {string} sourceBase - Base source directory
177
+ * @param {string} targetPath - Target installation path
178
+ * @param {object} packageContents - Variant-selected content from PackageManager
179
+ * @param {function} progressCallback - Optional callback for progress updates
180
+ */
181
+ async copySelectedFiles(sourceBase, targetPath, packageContents, progressCallback = null) {
182
+ // Calculate total file count and total bytes for all content
183
+ let totalFiles = 0;
184
+ let totalBytes = 0;
185
+ const filesToCopy = [];
186
+
187
+ // Helper function to get file size
188
+ const getFileSize = async (filePath) => {
189
+ try {
190
+ const stat = await fs.promises.stat(filePath);
191
+ return stat.size;
192
+ } catch (error) {
193
+ return 0;
194
+ }
195
+ };
196
+
197
+ // Helper function to recursively collect all files in a directory
198
+ const collectDirectoryFiles = async (dirPath, relativePath = '') => {
199
+ const items = await fs.promises.readdir(dirPath);
200
+ const filesInDir = [];
201
+
202
+ for (const item of items) {
203
+ const itemPath = path.join(dirPath, item);
204
+ const itemRelativePath = path.join(relativePath, item);
205
+ const stat = await fs.promises.stat(itemPath);
206
+
207
+ if (stat.isDirectory()) {
208
+ const subFiles = await collectDirectoryFiles(itemPath, itemRelativePath);
209
+ filesInDir.push(...subFiles);
210
+ } else {
211
+ filesInDir.push({
212
+ sourcePath: itemPath,
213
+ relativePath: itemRelativePath,
214
+ size: stat.size
215
+ });
216
+ }
217
+ }
218
+
219
+ return filesInDir;
220
+ };
221
+
222
+ // Collect all agent files
223
+ for (const agentPath of packageContents.agents) {
224
+ const relativePath = path.relative(sourceBase, agentPath);
225
+ const size = await getFileSize(agentPath);
226
+ filesToCopy.push({
227
+ sourcePath: agentPath,
228
+ relativePath: relativePath,
229
+ size: size,
230
+ type: 'agent'
231
+ });
232
+ totalFiles++;
233
+ totalBytes += size;
234
+ }
235
+
236
+ // Collect all skill files (skills are directories, need to traverse)
237
+ for (const skillPath of packageContents.skills) {
238
+ const baseRelativePath = path.relative(sourceBase, skillPath);
239
+ const skillFiles = await collectDirectoryFiles(skillPath, baseRelativePath);
240
+
241
+ for (const file of skillFiles) {
242
+ filesToCopy.push({
243
+ sourcePath: file.sourcePath,
244
+ relativePath: file.relativePath,
245
+ size: file.size,
246
+ type: 'skill'
247
+ });
248
+ totalFiles++;
249
+ totalBytes += file.size;
250
+ }
251
+ }
252
+
253
+ // Collect all command files (for opencode/droid which use commands instead of skills)
254
+ for (const commandPath of (packageContents.commands || [])) {
255
+ const stat = await fs.promises.stat(commandPath);
256
+ if (stat.isDirectory()) {
257
+ // Command subdirectory (like docs-builder/)
258
+ const baseRelativePath = path.relative(sourceBase, commandPath);
259
+ const commandFiles = await collectDirectoryFiles(commandPath, baseRelativePath);
260
+ for (const file of commandFiles) {
261
+ filesToCopy.push({
262
+ sourcePath: file.sourcePath,
263
+ relativePath: file.relativePath,
264
+ size: file.size,
265
+ type: 'command'
266
+ });
267
+ totalFiles++;
268
+ totalBytes += file.size;
269
+ }
270
+ } else {
271
+ // Single command file
272
+ const relativePath = path.relative(sourceBase, commandPath);
273
+ const size = await getFileSize(commandPath);
274
+ filesToCopy.push({
275
+ sourcePath: commandPath,
276
+ relativePath: relativePath,
277
+ size: size,
278
+ type: 'command'
279
+ });
280
+ totalFiles++;
281
+ totalBytes += size;
282
+ }
283
+ }
284
+
285
+ // Collect all resource files
286
+ for (const resourcePath of packageContents.resources) {
287
+ const relativePath = path.relative(sourceBase, resourcePath);
288
+ const size = await getFileSize(resourcePath);
289
+ filesToCopy.push({
290
+ sourcePath: resourcePath,
291
+ relativePath: relativePath,
292
+ size: size,
293
+ type: 'resource'
294
+ });
295
+ totalFiles++;
296
+ totalBytes += size;
297
+ }
298
+
299
+ // Collect all hook files
300
+ for (const hookPath of packageContents.hooks) {
301
+ const relativePath = path.relative(sourceBase, hookPath);
302
+ const size = await getFileSize(hookPath);
303
+ filesToCopy.push({
304
+ sourcePath: hookPath,
305
+ relativePath: relativePath,
306
+ size: size,
307
+ type: 'hook'
308
+ });
309
+ totalFiles++;
310
+ totalBytes += size;
311
+ }
312
+
313
+ // Now copy all files with progress tracking
314
+ let filesCompleted = 0;
315
+ let bytesTransferred = 0;
316
+
317
+ for (const file of filesToCopy) {
318
+ // Copy the file
319
+ const targetFilePath = path.join(targetPath, file.relativePath);
320
+ const targetDir = path.dirname(targetFilePath);
321
+
322
+ await fs.promises.mkdir(targetDir, { recursive: true });
323
+ await fs.promises.copyFile(file.sourcePath, targetFilePath);
324
+
325
+ // Track installed file for rollback
326
+ this.sessionLog.installedFiles.push(targetFilePath);
327
+
328
+ // Update progress
329
+ filesCompleted++;
330
+ bytesTransferred += file.size;
331
+
332
+ // Save state after each file (for resume capability)
333
+ if (this.stateManager && this.stateManager.getState()) {
334
+ await this.stateManager.updateFileProgress(
335
+ file.relativePath,
336
+ file.size,
337
+ totalFiles,
338
+ totalBytes
339
+ );
340
+ }
341
+
342
+ // Call progress callback if provided
343
+ if (progressCallback) {
344
+ const percentage = Math.round((filesCompleted / totalFiles) * 100);
345
+
346
+ progressCallback({
347
+ currentFile: file.relativePath,
348
+ filesCompleted: filesCompleted,
349
+ totalFiles: totalFiles,
350
+ percentage: percentage,
351
+ bytesTransferred: bytesTransferred,
352
+ totalBytes: totalBytes
353
+ });
354
+ }
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Copy directory recursively with progress tracking
360
+ * (Legacy method - kept for backup/restore functionality)
361
+ */
362
+ async copyDirectory(source, target) {
363
+ await fs.promises.mkdir(target, { recursive: true });
364
+ const items = await fs.promises.readdir(source);
365
+
366
+ for (const item of items) {
367
+ const sourcePath = path.join(source, item);
368
+ const targetPath = path.join(target, item);
369
+
370
+ try {
371
+ const stat = await fs.promises.lstat(sourcePath); // Use lstat to detect symlinks
372
+
373
+ if (stat.isSymbolicLink()) {
374
+ // Skip symlinks during backup
375
+ continue;
376
+ }
377
+
378
+ if (stat.isDirectory()) {
379
+ await this.copyDirectory(sourcePath, targetPath);
380
+ } else if (stat.isFile()) {
381
+ await fs.promises.copyFile(sourcePath, targetPath);
382
+ }
383
+ } catch (error) {
384
+ // Skip files that can't be copied (permissions, special files, etc.)
385
+ continue;
386
+ }
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Generate installation manifest with variant information
392
+ *
393
+ * Creates a comprehensive manifest including:
394
+ * - Variant metadata (description, useCase, targetUsers)
395
+ * - Lists of installed components by category
396
+ * - Component counts and size information
397
+ *
398
+ * @param {string} toolId - Tool identifier
399
+ * @param {string} variant - Variant name
400
+ * @param {string} targetPath - Installation target path
401
+ */
402
+ async generateManifest(toolId, variant, targetPath) {
403
+ const template = this.packageManager.getManifestTemplate(toolId);
404
+ const contents = await this.packageManager.getPackageContents(toolId, variant);
405
+ const size = await this.packageManager.getPackageSize(toolId, variant);
406
+ const variantMetadata = await this.packageManager.getVariantMetadata(toolId, variant);
407
+
408
+ // Extract filenames/directory names from paths for cleaner manifest
409
+ // For agents: remove .md extension (e.g., "master.md" -> "master")
410
+ // For skills: just basename (e.g., "pdf" directory)
411
+ // For resources/hooks: keep full filename with extension
412
+ const extractAgentName = (fullPath) => {
413
+ const basename = path.basename(fullPath);
414
+ return basename.replace(/\.md$/, '');
415
+ };
416
+
417
+ const extractSkillName = (fullPath) => {
418
+ return path.basename(fullPath);
419
+ };
420
+
421
+ const manifest = {
422
+ ...template,
423
+ variant,
424
+ version: '1.1.0',
425
+ installed_at: new Date().toISOString(),
426
+ variantInfo: {
427
+ name: variantMetadata.name,
428
+ description: variantMetadata.description,
429
+ useCase: variantMetadata.useCase,
430
+ targetUsers: variantMetadata.targetUsers
431
+ },
432
+ components: {
433
+ agents: contents.agents.length,
434
+ skills: contents.skills.length,
435
+ resources: contents.resources.length,
436
+ hooks: contents.hooks.length
437
+ },
438
+ installedFiles: {
439
+ agents: contents.agents.map(extractAgentName),
440
+ skills: contents.skills.map(extractSkillName),
441
+ resources: contents.resources.map(p => path.basename(p)),
442
+ hooks: contents.hooks.map(p => path.basename(p))
443
+ },
444
+ paths: {
445
+ agents: path.join(targetPath, 'agents'),
446
+ skills: path.join(targetPath, 'skills'),
447
+ resources: path.join(targetPath, 'resources'),
448
+ hooks: path.join(targetPath, 'hooks')
449
+ },
450
+ files: {
451
+ total: contents.totalFiles,
452
+ size: size.formattedSize
453
+ }
454
+ };
455
+
456
+ const manifestPath = path.join(targetPath, 'manifest.json');
457
+ await fs.promises.writeFile(
458
+ manifestPath,
459
+ JSON.stringify(manifest, null, 2)
460
+ );
461
+
462
+ // Track manifest file for rollback
463
+ if (this.sessionLog && this.sessionLog.installedFiles) {
464
+ this.sessionLog.installedFiles.push(manifestPath);
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Create backup of existing installation
470
+ */
471
+ async createBackup(targetPath) {
472
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
473
+ const backupPath = `${targetPath}.backup.${timestamp}`;
474
+
475
+ try {
476
+ await this.copyDirectory(targetPath, backupPath);
477
+
478
+ this.backupLog.push({
479
+ original: targetPath,
480
+ backup: backupPath,
481
+ timestamp: new Date().toISOString()
482
+ });
483
+ } catch (error) {
484
+ // Silently continue if backup fails (e.g., special files, symlinks)
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Rollback installation on failure
490
+ * Enhanced to remove only installed files (not entire directory)
491
+ * Preserves user-created files and restores from backup if available
492
+ *
493
+ * @param {string} toolId - Tool identifier
494
+ * @param {string} targetPath - Installation target path
495
+ */
496
+ async rollbackInstallation(toolId, targetPath) {
497
+ console.log(`Rolling back ${toolId} installation...`);
498
+
499
+ let filesRemoved = 0;
500
+ const errors = [];
501
+
502
+ try {
503
+ // Strategy 1: Remove files tracked in session log (most accurate)
504
+ if (this.sessionLog &&
505
+ this.sessionLog.installedFiles &&
506
+ this.sessionLog.installedFiles.length > 0 &&
507
+ this.sessionLog.targetPath === targetPath) {
508
+
509
+ console.log(`Removing ${this.sessionLog.installedFiles.length} tracked files...`);
510
+
511
+ // Remove files in reverse order (manifest first, then files)
512
+ for (let i = this.sessionLog.installedFiles.length - 1; i >= 0; i--) {
513
+ const filePath = this.sessionLog.installedFiles[i];
514
+
515
+ try {
516
+ if (fs.existsSync(filePath)) {
517
+ await fs.promises.unlink(filePath);
518
+ filesRemoved++;
519
+ }
520
+ } catch (error) {
521
+ errors.push(`Failed to remove ${filePath}: ${error.message}`);
522
+ }
523
+ }
524
+
525
+ // Clean up empty directories
526
+ await this.cleanupEmptyDirectories(targetPath);
527
+
528
+ }
529
+ // Strategy 2: If no session log, try to read manifest and remove those files
530
+ else {
531
+ const manifestPath = path.join(targetPath, 'manifest.json');
532
+
533
+ if (fs.existsSync(manifestPath)) {
534
+ console.log(`Using manifest to determine files to remove...`);
535
+
536
+ try {
537
+ const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'));
538
+
539
+ // Remove files listed in manifest
540
+ if (manifest.installedFiles) {
541
+ for (const category of ['agents', 'skills', 'resources', 'hooks']) {
542
+ if (manifest.installedFiles[category] && manifest.paths && manifest.paths[category]) {
543
+ const categoryPath = manifest.paths[category];
544
+
545
+ for (const item of manifest.installedFiles[category]) {
546
+ let itemPath;
547
+
548
+ if (category === 'agents') {
549
+ // Agents have .md extension
550
+ itemPath = path.join(categoryPath, `${item}.md`);
551
+ } else if (category === 'skills') {
552
+ // Skills are directories
553
+ itemPath = path.join(categoryPath, item);
554
+ } else {
555
+ // Resources and hooks have their full names
556
+ itemPath = path.join(categoryPath, item);
557
+ }
558
+
559
+ try {
560
+ if (fs.existsSync(itemPath)) {
561
+ const stat = await fs.promises.stat(itemPath);
562
+
563
+ if (stat.isDirectory()) {
564
+ // Remove directory recursively
565
+ await fs.promises.rm(itemPath, { recursive: true, force: true });
566
+ filesRemoved++;
567
+ } else {
568
+ // Remove file
569
+ await fs.promises.unlink(itemPath);
570
+ filesRemoved++;
571
+ }
572
+ }
573
+ } catch (error) {
574
+ errors.push(`Failed to remove ${itemPath}: ${error.message}`);
575
+ }
576
+ }
577
+ }
578
+ }
579
+ }
580
+
581
+ // Remove manifest
582
+ try {
583
+ await fs.promises.unlink(manifestPath);
584
+ filesRemoved++;
585
+ } catch (error) {
586
+ errors.push(`Failed to remove manifest: ${error.message}`);
587
+ }
588
+
589
+ // Clean up empty directories
590
+ await this.cleanupEmptyDirectories(targetPath);
591
+
592
+ } catch (error) {
593
+ console.warn(`Could not read manifest for rollback: ${error.message}`);
594
+ // Fall back to Strategy 3
595
+ }
596
+ }
597
+
598
+ // Strategy 3: If no manifest, restore from backup (legacy behavior)
599
+ if (filesRemoved === 0) {
600
+ const backup = this.backupLog.find(b => b.original === targetPath);
601
+
602
+ if (backup && fs.existsSync(backup.backup)) {
603
+ console.log(`Restoring from backup: ${backup.backup}`);
604
+
605
+ // Remove current installation
606
+ if (fs.existsSync(targetPath)) {
607
+ await fs.promises.rm(targetPath, { recursive: true, force: true });
608
+ }
609
+
610
+ // Restore from backup
611
+ await this.copyDirectory(backup.backup, targetPath);
612
+ console.log(`Restored from backup: ${backup.backup}`);
613
+ } else {
614
+ console.warn(`No backup available and no manifest found. Cannot perform granular rollback.`);
615
+ }
616
+ }
617
+ }
618
+
619
+ // Log rollback action
620
+ this.rollbackLog.push({
621
+ tool: toolId,
622
+ targetPath: targetPath,
623
+ filesRemoved: filesRemoved,
624
+ errors: errors,
625
+ timestamp: new Date().toISOString()
626
+ });
627
+
628
+ if (errors.length > 0) {
629
+ console.warn(`Rollback completed with ${errors.length} errors`);
630
+ } else {
631
+ console.log(`Rollback completed successfully. Removed ${filesRemoved} files.`);
632
+ }
633
+
634
+ } catch (error) {
635
+ console.error(`Rollback failed: ${error.message}`);
636
+ this.rollbackLog.push({
637
+ tool: toolId,
638
+ targetPath: targetPath,
639
+ filesRemoved: filesRemoved,
640
+ errors: [...errors, error.message],
641
+ timestamp: new Date().toISOString()
642
+ });
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Clean up empty directories after file removal
648
+ * Recursively removes empty subdirectories
649
+ *
650
+ * @param {string} targetPath - Base installation path
651
+ */
652
+ async cleanupEmptyDirectories(targetPath) {
653
+ const categories = ['agents', 'skills', 'resources', 'hooks'];
654
+
655
+ for (const category of categories) {
656
+ const categoryPath = path.join(targetPath, category);
657
+
658
+ if (fs.existsSync(categoryPath)) {
659
+ await this.removeEmptyDirectoriesRecursive(categoryPath);
660
+
661
+ // If category directory itself is now empty, remove it
662
+ try {
663
+ const items = await fs.promises.readdir(categoryPath);
664
+ if (items.length === 0) {
665
+ await fs.promises.rmdir(categoryPath);
666
+ }
667
+ } catch (error) {
668
+ // Ignore errors (directory might not be empty or might not exist)
669
+ }
670
+ }
671
+ }
672
+
673
+ // If target path is now completely empty, remove it
674
+ try {
675
+ if (fs.existsSync(targetPath)) {
676
+ const items = await fs.promises.readdir(targetPath);
677
+ if (items.length === 0) {
678
+ await fs.promises.rmdir(targetPath);
679
+ }
680
+ }
681
+ } catch (error) {
682
+ // Ignore errors
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Recursively remove empty directories
688
+ *
689
+ * @param {string} dirPath - Directory to check and clean
690
+ */
691
+ async removeEmptyDirectoriesRecursive(dirPath) {
692
+ if (!fs.existsSync(dirPath)) {
693
+ return;
694
+ }
695
+
696
+ const items = await fs.promises.readdir(dirPath);
697
+
698
+ // Process subdirectories first
699
+ for (const item of items) {
700
+ const itemPath = path.join(dirPath, item);
701
+ const stat = await fs.promises.stat(itemPath);
702
+
703
+ if (stat.isDirectory()) {
704
+ await this.removeEmptyDirectoriesRecursive(itemPath);
705
+
706
+ // Check if subdirectory is now empty and remove it
707
+ try {
708
+ const subItems = await fs.promises.readdir(itemPath);
709
+ if (subItems.length === 0) {
710
+ await fs.promises.rmdir(itemPath);
711
+ }
712
+ } catch (error) {
713
+ // Ignore errors
714
+ }
715
+ }
716
+ }
717
+ }
718
+
719
+ /**
720
+ * Get installation summary
721
+ */
722
+ getInstallationSummary() {
723
+ return {
724
+ installations: this.installationLog,
725
+ backups: this.backupLog,
726
+ totalTools: this.installationLog.length,
727
+ timestamp: new Date().toISOString()
728
+ };
729
+ }
730
+
731
+ /**
732
+ * Get session log (files installed in current session)
733
+ */
734
+ getSessionLog() {
735
+ return this.sessionLog;
736
+ }
737
+
738
+ /**
739
+ * Get rollback log (all rollback actions)
740
+ */
741
+ getRollbackLog() {
742
+ return this.rollbackLog;
743
+ }
744
+
745
+ /**
746
+ * Verify installation integrity
747
+ * Performs comprehensive verification of installed files and directories
748
+ *
749
+ * @param {string} toolId - Tool identifier
750
+ * @param {string} targetPath - Installation target path
751
+ * @returns {object} Verification result with detailed status and issues
752
+ */
753
+ async verifyInstallation(toolId, targetPath) {
754
+ const result = {
755
+ valid: true,
756
+ toolId: toolId,
757
+ targetPath: targetPath,
758
+ manifest: null,
759
+ issues: [],
760
+ warnings: [],
761
+ components: {
762
+ agents: { expected: 0, found: 0, missing: [] },
763
+ skills: { expected: 0, found: 0, missing: [] },
764
+ resources: { expected: 0, found: 0, missing: [] },
765
+ hooks: { expected: 0, found: 0, missing: [] }
766
+ },
767
+ timestamp: new Date().toISOString()
768
+ };
769
+
770
+ const manifestPath = path.join(targetPath, 'manifest.json');
771
+
772
+ // Check if manifest exists
773
+ if (!fs.existsSync(manifestPath)) {
774
+ result.valid = false;
775
+ result.issues.push({
776
+ severity: 'error',
777
+ message: 'Manifest file not found',
778
+ component: 'manifest'
779
+ });
780
+ return result;
781
+ }
782
+
783
+ try {
784
+ // Read and parse manifest
785
+ const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'));
786
+ result.manifest = manifest;
787
+
788
+ // Verify all required directories exist
789
+ for (const [component, componentPath] of Object.entries(manifest.paths || {})) {
790
+ if (!fs.existsSync(componentPath)) {
791
+ result.valid = false;
792
+ result.issues.push({
793
+ severity: 'error',
794
+ message: `Missing component directory: ${component}`,
795
+ component: component,
796
+ path: componentPath
797
+ });
798
+ }
799
+ }
800
+
801
+ // Verify installed files for each component category
802
+ for (const category of ['agents', 'skills', 'resources', 'hooks']) {
803
+ if (manifest.installedFiles && manifest.installedFiles[category]) {
804
+ const expectedFiles = manifest.installedFiles[category];
805
+ result.components[category].expected = expectedFiles.length;
806
+
807
+ const categoryPath = manifest.paths[category];
808
+
809
+ for (const item of expectedFiles) {
810
+ let itemPath;
811
+
812
+ // Construct full path based on category
813
+ if (category === 'agents') {
814
+ itemPath = path.join(categoryPath, `${item}.md`);
815
+ } else if (category === 'skills') {
816
+ itemPath = path.join(categoryPath, item);
817
+ } else {
818
+ itemPath = path.join(categoryPath, item);
819
+ }
820
+
821
+ // Check if file/directory exists
822
+ if (fs.existsSync(itemPath)) {
823
+ result.components[category].found++;
824
+ } else {
825
+ result.components[category].missing.push(item);
826
+ result.issues.push({
827
+ severity: 'error',
828
+ message: `Missing ${category.slice(0, -1)}: ${item}`,
829
+ component: category,
830
+ item: item,
831
+ path: itemPath
832
+ });
833
+ result.valid = false;
834
+ }
835
+ }
836
+ }
837
+ }
838
+
839
+ // Check for extra warnings
840
+ if (manifest.variant) {
841
+ result.variant = manifest.variant;
842
+ }
843
+
844
+ if (manifest.version) {
845
+ result.version = manifest.version;
846
+ }
847
+
848
+ // Verify file counts match
849
+ if (manifest.components) {
850
+ for (const category of ['agents', 'skills', 'resources', 'hooks']) {
851
+ if (manifest.components[category] !== undefined) {
852
+ const expectedCount = manifest.components[category];
853
+ const foundCount = result.components[category].found;
854
+
855
+ if (expectedCount !== foundCount) {
856
+ result.warnings.push({
857
+ severity: 'warning',
858
+ message: `${category} count mismatch: expected ${expectedCount}, found ${foundCount}`,
859
+ component: category
860
+ });
861
+ }
862
+ }
863
+ }
864
+ }
865
+
866
+ // Add overall summary
867
+ result.summary = {
868
+ totalExpected: Object.values(result.components).reduce((sum, c) => sum + c.expected, 0),
869
+ totalFound: Object.values(result.components).reduce((sum, c) => sum + c.found, 0),
870
+ totalMissing: Object.values(result.components).reduce((sum, c) => sum + c.missing.length, 0),
871
+ issueCount: result.issues.length,
872
+ warningCount: result.warnings.length
873
+ };
874
+
875
+ return result;
876
+
877
+ } catch (error) {
878
+ result.valid = false;
879
+ result.issues.push({
880
+ severity: 'error',
881
+ message: `Manifest parsing error: ${error.message}`,
882
+ component: 'manifest'
883
+ });
884
+ return result;
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Get state manager (stub - state management removed)
890
+ * @returns {Object} - Stub state manager
891
+ */
892
+ getStateManager() {
893
+ return {
894
+ initializeState: () => {},
895
+ saveState: async () => {},
896
+ getState: () => null,
897
+ completeCurrentTool: async () => {},
898
+ failCurrentTool: async () => {},
899
+ clearState: async () => {},
900
+ updateFileProgress: async () => {}
901
+ };
902
+ }
903
+
904
+ /**
905
+ * Check for interrupted installation (stub - always returns false)
906
+ * @returns {Promise<boolean>}
907
+ */
908
+ async hasInterruptedInstallation() {
909
+ return false;
910
+ }
911
+
912
+ /**
913
+ * Get resume summary (stub - always returns null)
914
+ * @returns {Promise<null>}
915
+ */
916
+ async getResumeSummary() {
917
+ return null;
918
+ }
919
+
920
+ /**
921
+ * Uninstall a tool by reading its manifest and removing installed files
922
+ *
923
+ * This method:
924
+ * - Reads the manifest.json from the target path
925
+ * - Prompts user for confirmation with file counts
926
+ * - Creates a backup before uninstalling
927
+ * - Removes all files listed in the manifest
928
+ * - Cleans up empty directories
929
+ * - Preserves user-created files not in the manifest
930
+ * - Provides progress feedback during uninstall
931
+ * - Returns a detailed uninstall report
932
+ *
933
+ * @param {string} toolId - Tool identifier (e.g., 'claude', 'opencode')
934
+ * @param {string} targetPath - Installation target path
935
+ * @param {function} confirmCallback - Async callback for user confirmation (returns boolean)
936
+ * @param {function} progressCallback - Optional callback for progress updates
937
+ * @returns {Object} - Uninstall result summary
938
+ */
939
+ async uninstall(toolId, targetPath, confirmCallback = null, progressCallback = null) {
940
+ const expandedTargetPath = this.pathManager.expandPath(targetPath);
941
+ const manifestPath = path.join(expandedTargetPath, 'manifest.json');
942
+
943
+ const result = {
944
+ success: false,
945
+ toolId: toolId,
946
+ targetPath: expandedTargetPath,
947
+ filesRemoved: 0,
948
+ directoriesRemoved: 0,
949
+ backupPath: null,
950
+ errors: [],
951
+ warnings: [],
952
+ timestamp: new Date().toISOString()
953
+ };
954
+
955
+ try {
956
+ // Step 1: Check if manifest exists
957
+ if (!fs.existsSync(manifestPath)) {
958
+ throw new Error(`No installation found at ${expandedTargetPath}. Manifest file is missing.`);
959
+ }
960
+
961
+ // Step 2: Read and parse manifest
962
+ const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'));
963
+
964
+ // Step 3: Calculate total files to remove
965
+ let totalFiles = 0;
966
+ const filesToRemove = [];
967
+
968
+ // Collect all files from manifest
969
+ if (manifest.installedFiles) {
970
+ for (const category of ['agents', 'skills', 'resources', 'hooks']) {
971
+ if (manifest.installedFiles[category] && manifest.paths && manifest.paths[category]) {
972
+ const categoryPath = manifest.paths[category];
973
+
974
+ for (const item of manifest.installedFiles[category]) {
975
+ let itemPath;
976
+
977
+ if (category === 'agents') {
978
+ // Agents have .md extension
979
+ itemPath = path.join(categoryPath, `${item}.md`);
980
+ } else if (category === 'skills') {
981
+ // Skills are directories - need to count all files inside
982
+ itemPath = path.join(categoryPath, item);
983
+ } else {
984
+ // Resources and hooks have their full names
985
+ itemPath = path.join(categoryPath, item);
986
+ }
987
+
988
+ filesToRemove.push({
989
+ path: itemPath,
990
+ category: category,
991
+ name: item,
992
+ isDirectory: category === 'skills'
993
+ });
994
+ }
995
+ }
996
+ }
997
+ }
998
+
999
+ // Count files (including files inside skill directories)
1000
+ for (const item of filesToRemove) {
1001
+ if (item.isDirectory && fs.existsSync(item.path)) {
1002
+ const fileCount = await this.countFilesInDirectory(item.path);
1003
+ totalFiles += fileCount;
1004
+ } else if (fs.existsSync(item.path)) {
1005
+ totalFiles += 1;
1006
+ }
1007
+ }
1008
+
1009
+ // Add manifest itself
1010
+ totalFiles += 1;
1011
+
1012
+ // Step 4: Prompt user for confirmation
1013
+ if (confirmCallback) {
1014
+ const confirmed = await confirmCallback({
1015
+ toolId: toolId,
1016
+ targetPath: expandedTargetPath,
1017
+ fileCount: totalFiles,
1018
+ variant: manifest.variant || 'unknown',
1019
+ components: manifest.components || {}
1020
+ });
1021
+
1022
+ if (!confirmed) {
1023
+ result.success = false;
1024
+ result.warnings.push('Uninstall cancelled by user');
1025
+ return result;
1026
+ }
1027
+ }
1028
+
1029
+ // Step 5: Create backup before uninstalling
1030
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1031
+ const backupPath = `${expandedTargetPath}.uninstall-backup.${timestamp}`;
1032
+
1033
+ try {
1034
+ await this.copyDirectory(expandedTargetPath, backupPath);
1035
+ result.backupPath = backupPath;
1036
+ console.log(`Backup created: ${backupPath}`);
1037
+ } catch (error) {
1038
+ // Silently continue if backup fails
1039
+ }
1040
+
1041
+ // Step 6: Remove files with progress tracking
1042
+ let filesRemoved = 0;
1043
+ let directoriesRemoved = 0;
1044
+
1045
+ for (const item of filesToRemove) {
1046
+ try {
1047
+ if (!fs.existsSync(item.path)) {
1048
+ result.warnings.push(`File not found: ${item.path}`);
1049
+ continue;
1050
+ }
1051
+
1052
+ const stat = await fs.promises.stat(item.path);
1053
+
1054
+ if (stat.isDirectory()) {
1055
+ // Remove directory recursively and track files
1056
+ const fileCount = await this.countFilesInDirectory(item.path);
1057
+ await fs.promises.rm(item.path, { recursive: true, force: true });
1058
+ filesRemoved += fileCount;
1059
+ directoriesRemoved += 1;
1060
+
1061
+ // Report progress
1062
+ if (progressCallback) {
1063
+ progressCallback({
1064
+ type: 'removing',
1065
+ category: item.category,
1066
+ name: item.name,
1067
+ filesRemoved: filesRemoved,
1068
+ totalFiles: totalFiles,
1069
+ percentage: Math.round((filesRemoved / totalFiles) * 100)
1070
+ });
1071
+ }
1072
+
1073
+ console.log(`Removing: ${item.category}/${item.name} (directory)`);
1074
+
1075
+ } else {
1076
+ // Remove single file
1077
+ await fs.promises.unlink(item.path);
1078
+ filesRemoved += 1;
1079
+
1080
+ // Report progress
1081
+ if (progressCallback) {
1082
+ progressCallback({
1083
+ type: 'removing',
1084
+ category: item.category,
1085
+ name: item.name,
1086
+ filesRemoved: filesRemoved,
1087
+ totalFiles: totalFiles,
1088
+ percentage: Math.round((filesRemoved / totalFiles) * 100)
1089
+ });
1090
+ }
1091
+
1092
+ console.log(`Removing: ${item.category}/${item.name}`);
1093
+ }
1094
+
1095
+ } catch (error) {
1096
+ result.errors.push(`Failed to remove ${item.path}: ${error.message}`);
1097
+ }
1098
+ }
1099
+
1100
+ // Step 7: Remove manifest file
1101
+ try {
1102
+ await fs.promises.unlink(manifestPath);
1103
+ filesRemoved += 1;
1104
+ console.log(`Removing: manifest.json`);
1105
+
1106
+ // Report final progress
1107
+ if (progressCallback) {
1108
+ progressCallback({
1109
+ type: 'removing',
1110
+ category: 'manifest',
1111
+ name: 'manifest.json',
1112
+ filesRemoved: filesRemoved,
1113
+ totalFiles: totalFiles,
1114
+ percentage: 100
1115
+ });
1116
+ }
1117
+
1118
+ } catch (error) {
1119
+ result.errors.push(`Failed to remove manifest: ${error.message}`);
1120
+ }
1121
+
1122
+ // Step 8: Clean up empty directories
1123
+ await this.cleanupEmptyDirectories(expandedTargetPath);
1124
+
1125
+ // Count directories removed during cleanup
1126
+ const dirsRemoved = await this.countRemovedDirectories(expandedTargetPath);
1127
+ directoriesRemoved += dirsRemoved;
1128
+
1129
+ // Step 9: Update result
1130
+ result.success = result.errors.length === 0;
1131
+ result.filesRemoved = filesRemoved;
1132
+ result.directoriesRemoved = directoriesRemoved;
1133
+
1134
+ // Step 10: Display summary
1135
+ console.log(`\n✓ ${toolId} uninstalled successfully`);
1136
+ console.log(` Files removed: ${filesRemoved}`);
1137
+ console.log(` Directories removed: ${directoriesRemoved}`);
1138
+ if (result.backupPath) {
1139
+ console.log(` Backup: ${result.backupPath}`);
1140
+ }
1141
+
1142
+ if (result.errors.length > 0) {
1143
+ console.log(`\n⚠ Uninstall completed with ${result.errors.length} errors:`);
1144
+ result.errors.forEach(err => console.log(` - ${err}`));
1145
+ }
1146
+
1147
+ if (result.warnings.length > 0) {
1148
+ console.log(`\n⚠ Warnings:`);
1149
+ result.warnings.forEach(warn => console.log(` - ${warn}`));
1150
+ }
1151
+
1152
+ return result;
1153
+
1154
+ } catch (error) {
1155
+ result.success = false;
1156
+ result.errors.push(error.message);
1157
+ console.error(`✗ Failed to uninstall ${toolId}: ${error.message}`);
1158
+ return result;
1159
+ }
1160
+ }
1161
+
1162
+ /**
1163
+ * Count total files in a directory recursively
1164
+ *
1165
+ * @param {string} dirPath - Directory path
1166
+ * @returns {Promise<number>} - Total file count
1167
+ */
1168
+ async countFilesInDirectory(dirPath) {
1169
+ let count = 0;
1170
+
1171
+ const items = await fs.promises.readdir(dirPath);
1172
+
1173
+ for (const item of items) {
1174
+ const itemPath = path.join(dirPath, item);
1175
+ const stat = await fs.promises.stat(itemPath);
1176
+
1177
+ if (stat.isDirectory()) {
1178
+ count += await this.countFilesInDirectory(itemPath);
1179
+ } else {
1180
+ count += 1;
1181
+ }
1182
+ }
1183
+
1184
+ return count;
1185
+ }
1186
+
1187
+ /**
1188
+ * Count directories removed during cleanup
1189
+ * (Checks which category directories no longer exist)
1190
+ *
1191
+ * @param {string} targetPath - Base installation path
1192
+ * @returns {Promise<number>} - Number of directories removed
1193
+ */
1194
+ async countRemovedDirectories(targetPath) {
1195
+ let count = 0;
1196
+ const categories = ['agents', 'skills', 'resources', 'hooks'];
1197
+
1198
+ for (const category of categories) {
1199
+ const categoryPath = path.join(targetPath, category);
1200
+ if (!fs.existsSync(categoryPath)) {
1201
+ count += 1;
1202
+ }
1203
+ }
1204
+
1205
+ return count;
1206
+ }
1207
+
1208
+ /**
1209
+ * Upgrade or downgrade variant of an installed tool
1210
+ *
1211
+ * Compares current and new variants, adds/removes files as needed,
1212
+ * creates backup, and verifies the result.
1213
+ *
1214
+ * @param {string} toolId - Tool identifier (claude, opencode, etc.)
1215
+ * @param {string} newVariant - Target variant (lite, standard, pro)
1216
+ * @param {string} targetPath - Installation directory path
1217
+ * @param {function} confirmCallback - Optional callback to confirm upgrade (receives summary, returns boolean)
1218
+ * @param {function} progressCallback - Optional callback for progress updates
1219
+ * @returns {Promise<Object>} - Upgrade result with success status, file counts, backup path
1220
+ */
1221
+ async upgradeVariant(toolId, newVariant, targetPath, confirmCallback = null, progressCallback = null) {
1222
+ const result = {
1223
+ success: false,
1224
+ fromVariant: null,
1225
+ toVariant: newVariant,
1226
+ filesAdded: 0,
1227
+ filesRemoved: 0,
1228
+ backupPath: null,
1229
+ verification: null,
1230
+ error: null
1231
+ };
1232
+
1233
+ try {
1234
+ // Step 1: Read existing manifest to get current variant
1235
+ const manifestPath = path.join(targetPath, 'manifest.json');
1236
+ if (!fs.existsSync(manifestPath)) {
1237
+ result.error = 'No installation found at target path (manifest.json missing)';
1238
+ return result;
1239
+ }
1240
+
1241
+ const manifestContent = await fs.promises.readFile(manifestPath, 'utf8');
1242
+ const manifest = JSON.parse(manifestContent);
1243
+ const currentVariant = manifest.variant;
1244
+ result.fromVariant = currentVariant;
1245
+
1246
+ if (progressCallback) {
1247
+ progressCallback({ stage: 'reading_manifest', variant: currentVariant });
1248
+ }
1249
+
1250
+ // Step 2: Check if same variant (no-op)
1251
+ if (currentVariant === newVariant) {
1252
+ result.success = true;
1253
+ result.verification = { valid: true, issues: [] };
1254
+ return result;
1255
+ }
1256
+
1257
+ // Step 3: Get current and new variant contents
1258
+ const currentContents = await this.packageManager.getPackageContents(toolId, currentVariant);
1259
+ const newContents = await this.packageManager.getPackageContents(toolId, newVariant);
1260
+
1261
+ if (progressCallback) {
1262
+ progressCallback({ stage: 'comparing_variants', from: currentVariant, to: newVariant });
1263
+ }
1264
+
1265
+ // Step 4: Determine files to add and remove
1266
+ const changes = this._compareVariantContents(currentContents, newContents);
1267
+
1268
+ // Step 5: Call confirm callback if provided
1269
+ if (confirmCallback) {
1270
+ const confirmData = {
1271
+ fromVariant: currentVariant,
1272
+ toVariant: newVariant,
1273
+ filesAdded: changes.toAdd.length,
1274
+ filesRemoved: changes.toRemove.length,
1275
+ filesToAdd: changes.toAdd,
1276
+ filesToRemove: changes.toRemove
1277
+ };
1278
+
1279
+ const confirmed = confirmCallback(confirmData);
1280
+ if (!confirmed) {
1281
+ result.error = 'Upgrade cancelled by user';
1282
+ return result;
1283
+ }
1284
+ }
1285
+
1286
+ // Step 6: Create backup before changes
1287
+ if (progressCallback) {
1288
+ progressCallback({ stage: 'creating_backup' });
1289
+ }
1290
+
1291
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1292
+ const backupPath = `${targetPath}.upgrade-backup.${timestamp}`;
1293
+ await this.copyDirectory(targetPath, backupPath);
1294
+ result.backupPath = backupPath;
1295
+
1296
+ // Step 7: Remove files (for downgrade)
1297
+ if (changes.toRemove.length > 0) {
1298
+ if (progressCallback) {
1299
+ progressCallback({ stage: 'removing_files', count: changes.toRemove.length });
1300
+ }
1301
+
1302
+ await this._removeVariantFiles(targetPath, changes.toRemove, manifest, progressCallback);
1303
+ result.filesRemoved = changes.toRemove.length;
1304
+ }
1305
+
1306
+ // Step 8: Add files (for upgrade)
1307
+ if (changes.toAdd.length > 0) {
1308
+ if (progressCallback) {
1309
+ progressCallback({ stage: 'adding_files', count: changes.toAdd.length });
1310
+ }
1311
+
1312
+ const sourceBase = path.join(this.packageManager.packagesDir, toolId);
1313
+ await this._addVariantFiles(sourceBase, targetPath, changes.toAdd, progressCallback);
1314
+ result.filesAdded = changes.toAdd.length;
1315
+ }
1316
+
1317
+ // Step 9: Ensure all category directories exist (even if empty)
1318
+ const categories = ['agents', 'skills', 'resources', 'hooks'];
1319
+ for (const category of categories) {
1320
+ const categoryPath = path.join(targetPath, category);
1321
+ if (!fs.existsSync(categoryPath)) {
1322
+ await fs.promises.mkdir(categoryPath, { recursive: true });
1323
+ }
1324
+ }
1325
+
1326
+ // Step 10: Update manifest with new variant information
1327
+ if (progressCallback) {
1328
+ progressCallback({ stage: 'updating_manifest' });
1329
+ }
1330
+
1331
+ await this.generateManifest(toolId, newVariant, targetPath);
1332
+
1333
+ // Step 11: Verify the upgraded installation
1334
+ if (progressCallback) {
1335
+ progressCallback({ stage: 'verifying' });
1336
+ }
1337
+
1338
+ const verification = await this.verifyInstallation(toolId, targetPath);
1339
+ result.verification = verification;
1340
+
1341
+ if (!verification.valid) {
1342
+ result.error = 'Verification failed after upgrade';
1343
+ result.success = false;
1344
+ return result;
1345
+ }
1346
+
1347
+ result.success = true;
1348
+
1349
+ if (progressCallback) {
1350
+ progressCallback({ stage: 'complete', success: true });
1351
+ }
1352
+
1353
+ return result;
1354
+
1355
+ } catch (error) {
1356
+ result.error = error.message;
1357
+ result.success = false;
1358
+ return result;
1359
+ }
1360
+ }
1361
+
1362
+ /**
1363
+ * Compare two variant contents to determine what files need to be added or removed
1364
+ * @private
1365
+ */
1366
+ _compareVariantContents(currentContents, newContents) {
1367
+ const toAdd = [];
1368
+ const toRemove = [];
1369
+
1370
+ // Helper to extract basenames for comparison
1371
+ const getBasename = (filePath) => path.basename(filePath).replace(/\.md$/, '');
1372
+
1373
+ // Compare agents
1374
+ const currentAgents = new Set(currentContents.agents.map(getBasename));
1375
+ const newAgents = new Set(newContents.agents.map(getBasename));
1376
+
1377
+ for (const agent of newContents.agents) {
1378
+ const basename = getBasename(agent);
1379
+ if (!currentAgents.has(basename)) {
1380
+ // Agent files have .md extension
1381
+ toAdd.push({ type: 'agent', source: agent, name: path.basename(agent) });
1382
+ }
1383
+ }
1384
+
1385
+ for (const agent of currentContents.agents) {
1386
+ const basename = getBasename(agent);
1387
+ if (!newAgents.has(basename)) {
1388
+ toRemove.push({ type: 'agent', name: path.basename(agent), category: 'agents' });
1389
+ }
1390
+ }
1391
+
1392
+ // Compare skills (directories)
1393
+ const currentSkills = new Set(currentContents.skills.map(p => path.basename(p)));
1394
+ const newSkills = new Set(newContents.skills.map(p => path.basename(p)));
1395
+
1396
+ for (const skill of newContents.skills) {
1397
+ const basename = path.basename(skill);
1398
+ if (!currentSkills.has(basename)) {
1399
+ toAdd.push({ type: 'skill', source: skill, name: basename });
1400
+ }
1401
+ }
1402
+
1403
+ for (const skill of currentContents.skills) {
1404
+ const basename = path.basename(skill);
1405
+ if (!newSkills.has(basename)) {
1406
+ toRemove.push({ type: 'skill', name: basename, category: 'skills' });
1407
+ }
1408
+ }
1409
+
1410
+ // Compare resources
1411
+ const currentResources = new Set(currentContents.resources.map(p => path.basename(p)));
1412
+ const newResources = new Set(newContents.resources.map(p => path.basename(p)));
1413
+
1414
+ for (const resource of newContents.resources) {
1415
+ const basename = path.basename(resource);
1416
+ if (!currentResources.has(basename)) {
1417
+ toAdd.push({ type: 'resource', source: resource, name: basename });
1418
+ }
1419
+ }
1420
+
1421
+ for (const resource of currentContents.resources) {
1422
+ const basename = path.basename(resource);
1423
+ if (!newResources.has(basename)) {
1424
+ toRemove.push({ type: 'resource', name: basename, category: 'resources' });
1425
+ }
1426
+ }
1427
+
1428
+ // Compare hooks
1429
+ const currentHooks = new Set(currentContents.hooks.map(p => path.basename(p)));
1430
+ const newHooks = new Set(newContents.hooks.map(p => path.basename(p)));
1431
+
1432
+ for (const hook of newContents.hooks) {
1433
+ const basename = path.basename(hook);
1434
+ if (!currentHooks.has(basename)) {
1435
+ toAdd.push({ type: 'hook', source: hook, name: basename });
1436
+ }
1437
+ }
1438
+
1439
+ for (const hook of currentContents.hooks) {
1440
+ const basename = path.basename(hook);
1441
+ if (!newHooks.has(basename)) {
1442
+ toRemove.push({ type: 'hook', name: basename, category: 'hooks' });
1443
+ }
1444
+ }
1445
+
1446
+ return { toAdd, toRemove };
1447
+ }
1448
+
1449
+ /**
1450
+ * Remove files during variant downgrade
1451
+ * Only removes files that are in the manifest (never removes user-created files)
1452
+ * @private
1453
+ */
1454
+ async _removeVariantFiles(targetPath, filesToRemove, manifest, progressCallback = null) {
1455
+ const manifestFiles = {
1456
+ agents: new Set(manifest.installedFiles.agents),
1457
+ skills: new Set(manifest.installedFiles.skills),
1458
+ resources: new Set(manifest.installedFiles.resources),
1459
+ hooks: new Set(manifest.installedFiles.hooks)
1460
+ };
1461
+
1462
+ for (const file of filesToRemove) {
1463
+ // Only remove if it's in the manifest (not user-created)
1464
+ const fileNameWithoutExt = file.name.replace(/\.md$/, '');
1465
+ const isInManifest = manifestFiles[file.category] &&
1466
+ (manifestFiles[file.category].has(file.name) ||
1467
+ manifestFiles[file.category].has(fileNameWithoutExt));
1468
+
1469
+ if (!isInManifest) {
1470
+ // Skip user-created files
1471
+ continue;
1472
+ }
1473
+
1474
+ const filePath = path.join(targetPath, file.category, file.name);
1475
+
1476
+ if (fs.existsSync(filePath)) {
1477
+ const stat = await fs.promises.stat(filePath);
1478
+
1479
+ if (stat.isDirectory()) {
1480
+ // Remove directory and all contents
1481
+ await fs.promises.rm(filePath, { recursive: true, force: true });
1482
+ } else {
1483
+ // Remove file
1484
+ await fs.promises.unlink(filePath);
1485
+ }
1486
+
1487
+ if (progressCallback) {
1488
+ progressCallback({ stage: 'removing_file', file: file.name, category: file.category });
1489
+ }
1490
+ }
1491
+ }
1492
+
1493
+ // Clean up empty directories
1494
+ await this.cleanupEmptyDirectories(targetPath);
1495
+ }
1496
+
1497
+ /**
1498
+ * Add files during variant upgrade
1499
+ * @private
1500
+ */
1501
+ async _addVariantFiles(sourceBase, targetPath, filesToAdd, progressCallback = null) {
1502
+ for (const file of filesToAdd) {
1503
+ const sourcePath = file.source;
1504
+ const targetCategory = file.type === 'agent' ? 'agents' :
1505
+ file.type === 'skill' ? 'skills' :
1506
+ file.type === 'resource' ? 'resources' : 'hooks';
1507
+
1508
+ const targetDir = path.join(targetPath, targetCategory);
1509
+
1510
+ // Ensure target directory exists
1511
+ if (!fs.existsSync(targetDir)) {
1512
+ await fs.promises.mkdir(targetDir, { recursive: true });
1513
+ }
1514
+
1515
+ const targetFilePath = path.join(targetDir, file.name);
1516
+
1517
+ // Check if it's a directory (skills are directories)
1518
+ const sourceStat = await fs.promises.stat(sourcePath);
1519
+
1520
+ if (sourceStat.isDirectory()) {
1521
+ // Create target directory first, then copy contents
1522
+ await fs.promises.mkdir(targetFilePath, { recursive: true });
1523
+ await this.copyDirectory(sourcePath, targetFilePath);
1524
+ } else {
1525
+ // Copy single file
1526
+ await fs.promises.copyFile(sourcePath, targetFilePath);
1527
+ }
1528
+
1529
+ if (progressCallback) {
1530
+ progressCallback({ stage: 'adding_file', file: file.name, category: targetCategory });
1531
+ }
1532
+ }
1533
+ }
1534
+ }
1535
+
1536
+ module.exports = InstallationEngine;