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.
- package/CHANGELOG.md +441 -0
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/cli.js +230 -0
- package/docs/.gitkeep +1 -0
- package/docs/CONTRIBUTING.md +739 -0
- package/docs/DUAL_PUBLISH_SUMMARY.md +177 -0
- package/docs/ERROR_HANDLING_IMPLEMENTATION.md +327 -0
- package/docs/GITHUB_PACKAGES.md +181 -0
- package/docs/GITHUB_SETUP.md +158 -0
- package/docs/INSTALLATION_DEMO.md +691 -0
- package/docs/INSTALLATION_LOCATIONS.md +299 -0
- package/docs/INSTALLER_GUIDE.md +1586 -0
- package/docs/INTEGRATION_ISSUES_9.1.md +341 -0
- package/docs/KNOWLEDGE_BASE.md +727 -0
- package/docs/MIGRATION.md +384 -0
- package/docs/PACKAGE_BASELINE.md +557 -0
- package/docs/PACKAGE_VALIDATION_REPORT.md +427 -0
- package/docs/PASS_INTEGRATION.md +307 -0
- package/docs/PASS_QUICK_START.md +150 -0
- package/docs/PRIVACY.md +203 -0
- package/docs/PUBLISHING.md +494 -0
- package/docs/QUICK-START.md +318 -0
- package/docs/RELEASE_NOTES_1.2.0.md +323 -0
- package/docs/SECURITY.md +317 -0
- package/docs/SILENT_MODE_GUIDE.md +526 -0
- package/docs/SKILLS_CONVERSION.md +154 -0
- package/docs/TESTING.md +582 -0
- package/docs/TEST_COVERAGE.md +347 -0
- package/docs/TROUBLESHOOTING.md +788 -0
- package/docs/UPDATED_VARIANT_CONFIGURATION.md +274 -0
- package/docs/VARIANT_CONFIGURATION.md +440 -0
- package/installer/cli.js +761 -0
- package/installer/installation-engine.js +1536 -0
- package/installer/package-manager.js +640 -0
- package/installer/path-manager.js +427 -0
- package/installer/report-template.js +298 -0
- package/installer/verification-system.js +274 -0
- package/package.json +83 -0
- package/packages/ampcode/AGENT.md +58 -0
- package/packages/ampcode/README.md +17 -0
- package/packages/ampcode/agents/1-create-prd.md +175 -0
- package/packages/ampcode/agents/2-generate-tasks.md +190 -0
- package/packages/ampcode/agents/3-process-task-list.md +225 -0
- package/packages/ampcode/agents/code-developer.md +198 -0
- package/packages/ampcode/agents/context-builder.md +142 -0
- package/packages/ampcode/agents/feature-planner.md +199 -0
- package/packages/ampcode/agents/market-researcher.md +89 -0
- package/packages/ampcode/agents/orchestrator.md +116 -0
- package/packages/ampcode/agents/quality-assurance.md +115 -0
- package/packages/ampcode/agents/system-architect.md +135 -0
- package/packages/ampcode/agents/ui-designer.md +184 -0
- package/packages/ampcode/commands/brainstorming.md +56 -0
- package/packages/ampcode/commands/code-review.md +107 -0
- package/packages/ampcode/commands/condition-based-waiting/example.ts +158 -0
- package/packages/ampcode/commands/condition-based-waiting.md +122 -0
- package/packages/ampcode/commands/debug.md +20 -0
- package/packages/ampcode/commands/docs-builder/templates.md +572 -0
- package/packages/ampcode/commands/docs-builder.md +106 -0
- package/packages/ampcode/commands/explain.md +18 -0
- package/packages/ampcode/commands/git-commit.md +14 -0
- package/packages/ampcode/commands/optimize.md +20 -0
- package/packages/ampcode/commands/refactor.md +21 -0
- package/packages/ampcode/commands/review.md +18 -0
- package/packages/ampcode/commands/root-cause-tracing/find-polluter.sh +63 -0
- package/packages/ampcode/commands/root-cause-tracing.md +176 -0
- package/packages/ampcode/commands/security.md +21 -0
- package/packages/ampcode/commands/ship.md +18 -0
- package/packages/ampcode/commands/skill-creator/scripts/init_skill.py +303 -0
- package/packages/ampcode/commands/skill-creator/scripts/package_skill.py +110 -0
- package/packages/ampcode/commands/skill-creator/scripts/quick_validate.py +65 -0
- package/packages/ampcode/commands/skill-creator.md +211 -0
- package/packages/ampcode/commands/stash.md +45 -0
- package/packages/ampcode/commands/systematic-debugging.md +297 -0
- package/packages/ampcode/commands/test-driven-development.md +390 -0
- package/packages/ampcode/commands/test-generate.md +18 -0
- package/packages/ampcode/commands/testing-anti-patterns.md +304 -0
- package/packages/ampcode/commands/verification-before-completion.md +152 -0
- package/packages/ampcode/settings.json +13 -0
- package/packages/ampcode/variants.json +8 -0
- package/packages/claude/CLAUDE.md +58 -0
- package/packages/claude/README.md +23 -0
- package/packages/claude/agents/1-create-prd.md +175 -0
- package/packages/claude/agents/2-generate-tasks.md +190 -0
- package/packages/claude/agents/3-process-task-list.md +225 -0
- package/packages/claude/agents/code-developer.md +198 -0
- package/packages/claude/agents/context-builder.md +142 -0
- package/packages/claude/agents/feature-planner.md +199 -0
- package/packages/claude/agents/market-researcher.md +89 -0
- package/packages/claude/agents/orchestrator.md +117 -0
- package/packages/claude/agents/quality-assurance.md +115 -0
- package/packages/claude/agents/system-architect.md +135 -0
- package/packages/claude/agents/ui-designer.md +184 -0
- package/packages/claude/commands/debug.md +20 -0
- package/packages/claude/commands/explain.md +18 -0
- package/packages/claude/commands/git-commit.md +14 -0
- package/packages/claude/commands/optimize.md +20 -0
- package/packages/claude/commands/refactor.md +21 -0
- package/packages/claude/commands/review.md +18 -0
- package/packages/claude/commands/security.md +21 -0
- package/packages/claude/commands/ship.md +18 -0
- package/packages/claude/commands/stash.md +45 -0
- package/packages/claude/commands/test-generate.md +18 -0
- package/packages/claude/skills/brainstorming/SKILL.md +56 -0
- package/packages/claude/skills/code-review/SKILL.md +107 -0
- package/packages/claude/skills/code-review/code-reviewer.md +146 -0
- package/packages/claude/skills/condition-based-waiting/SKILL.md +122 -0
- package/packages/claude/skills/condition-based-waiting/example.ts +158 -0
- package/packages/claude/skills/docs-builder/SKILL.md +106 -0
- package/packages/claude/skills/docs-builder/references/templates.md +572 -0
- package/packages/claude/skills/root-cause-tracing/SKILL.md +176 -0
- package/packages/claude/skills/root-cause-tracing/find-polluter.sh +63 -0
- package/packages/claude/skills/skill-creator/LICENSE.txt +202 -0
- package/packages/claude/skills/skill-creator/SKILL.md +211 -0
- package/packages/claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/packages/claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/packages/claude/skills/skill-creator/scripts/quick_validate.py +65 -0
- package/packages/claude/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/packages/claude/skills/systematic-debugging/SKILL.md +296 -0
- package/packages/claude/skills/systematic-debugging/test-academic.md +14 -0
- package/packages/claude/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/packages/claude/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/packages/claude/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/packages/claude/skills/test-driven-development/SKILL.md +392 -0
- package/packages/claude/skills/testing-anti-patterns/SKILL.md +304 -0
- package/packages/claude/skills/verification-before-completion/SKILL.md +152 -0
- package/packages/claude/variants.json +9 -0
- package/packages/droid/AGENTS.md +52 -0
- package/packages/droid/README.md +17 -0
- package/packages/droid/change_settings.json +61 -0
- package/packages/droid/commands/brainstorming.md +56 -0
- package/packages/droid/commands/code-review.md +107 -0
- package/packages/droid/commands/condition-based-waiting/example.ts +158 -0
- package/packages/droid/commands/condition-based-waiting.md +122 -0
- package/packages/droid/commands/debug.md +20 -0
- package/packages/droid/commands/docs-builder/templates.md +572 -0
- package/packages/droid/commands/docs-builder.md +106 -0
- package/packages/droid/commands/explain.md +18 -0
- package/packages/droid/commands/git-commit.md +14 -0
- package/packages/droid/commands/optimize.md +20 -0
- package/packages/droid/commands/refactor.md +21 -0
- package/packages/droid/commands/review.md +18 -0
- package/packages/droid/commands/root-cause-tracing/find-polluter.sh +63 -0
- package/packages/droid/commands/root-cause-tracing.md +176 -0
- package/packages/droid/commands/security.md +21 -0
- package/packages/droid/commands/ship.md +18 -0
- package/packages/droid/commands/skill-creator/scripts/init_skill.py +303 -0
- package/packages/droid/commands/skill-creator/scripts/package_skill.py +110 -0
- package/packages/droid/commands/skill-creator/scripts/quick_validate.py +65 -0
- package/packages/droid/commands/skill-creator.md +211 -0
- package/packages/droid/commands/stash.md +45 -0
- package/packages/droid/commands/systematic-debugging.md +297 -0
- package/packages/droid/commands/test-driven-development.md +390 -0
- package/packages/droid/commands/test-generate.md +18 -0
- package/packages/droid/commands/testing-anti-patterns.md +304 -0
- package/packages/droid/commands/verification-before-completion.md +152 -0
- package/packages/droid/droids/1-create-prd.md +170 -0
- package/packages/droid/droids/2-generate-tasks.md +190 -0
- package/packages/droid/droids/3-process-task-list.md +225 -0
- package/packages/droid/droids/code-developer.md +198 -0
- package/packages/droid/droids/context-builder.md +142 -0
- package/packages/droid/droids/feature-planner.md +199 -0
- package/packages/droid/droids/market-researcher.md +89 -0
- package/packages/droid/droids/orchestrator.md +116 -0
- package/packages/droid/droids/quality-assurance.md +115 -0
- package/packages/droid/droids/system-architect.md +135 -0
- package/packages/droid/droids/ui-designer.md +184 -0
- package/packages/droid/variants.json +8 -0
- package/packages/opencode/AGENTS.md +52 -0
- package/packages/opencode/README.md +17 -0
- package/packages/opencode/agent/1-create-prd.md +179 -0
- package/packages/opencode/agent/2-generate-tasks.md +194 -0
- package/packages/opencode/agent/3-process-task-list.md +229 -0
- package/packages/opencode/agent/code-developer.md +202 -0
- package/packages/opencode/agent/context-builder.md +146 -0
- package/packages/opencode/agent/feature-planner.md +203 -0
- package/packages/opencode/agent/market-researcher.md +93 -0
- package/packages/opencode/agent/orchestrator.md +120 -0
- package/packages/opencode/agent/quality-assurance.md +119 -0
- package/packages/opencode/agent/system-architect.md +139 -0
- package/packages/opencode/agent/ui-designer.md +188 -0
- package/packages/opencode/command/brainstorming.md +56 -0
- package/packages/opencode/command/code-review.md +107 -0
- package/packages/opencode/command/condition-based-waiting/example.ts +158 -0
- package/packages/opencode/command/condition-based-waiting.md +122 -0
- package/packages/opencode/command/debug.md +20 -0
- package/packages/opencode/command/docs-builder/templates.md +572 -0
- package/packages/opencode/command/docs-builder.md +106 -0
- package/packages/opencode/command/explain.md +18 -0
- package/packages/opencode/command/git-commit.md +14 -0
- package/packages/opencode/command/optimize.md +20 -0
- package/packages/opencode/command/refactor.md +21 -0
- package/packages/opencode/command/review.md +18 -0
- package/packages/opencode/command/root-cause-tracing/find-polluter.sh +63 -0
- package/packages/opencode/command/root-cause-tracing.md +176 -0
- package/packages/opencode/command/security.md +21 -0
- package/packages/opencode/command/ship.md +18 -0
- package/packages/opencode/command/skill-creator/scripts/init_skill.py +303 -0
- package/packages/opencode/command/skill-creator/scripts/package_skill.py +110 -0
- package/packages/opencode/command/skill-creator/scripts/quick_validate.py +65 -0
- package/packages/opencode/command/skill-creator.md +211 -0
- package/packages/opencode/command/stash.md +45 -0
- package/packages/opencode/command/systematic-debugging.md +297 -0
- package/packages/opencode/command/test-driven-development.md +390 -0
- package/packages/opencode/command/test-generate.md +18 -0
- package/packages/opencode/command/testing-anti-patterns.md +304 -0
- package/packages/opencode/command/verification-before-completion.md +152 -0
- package/packages/opencode/opencode.jsonc +201 -0
- package/packages/opencode/variants.json +8 -0
- package/packages/subagentic-manual.md +349 -0
- package/postinstall.js +21 -0
- package/tools/ampcode/manifest-template.json +14 -0
- package/tools/claude/manifest-template.json +14 -0
- package/tools/droid/manifest-template.json +14 -0
- 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;
|