coder-config 0.40.1

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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +553 -0
  3. package/cli.js +431 -0
  4. package/config-loader.js +294 -0
  5. package/hooks/activity-track.sh +56 -0
  6. package/hooks/codex-workstream.sh +44 -0
  7. package/hooks/gemini-workstream.sh +44 -0
  8. package/hooks/workstream-inject.sh +20 -0
  9. package/lib/activity.js +283 -0
  10. package/lib/apply.js +344 -0
  11. package/lib/cli.js +267 -0
  12. package/lib/config.js +171 -0
  13. package/lib/constants.js +55 -0
  14. package/lib/env.js +114 -0
  15. package/lib/index.js +47 -0
  16. package/lib/init.js +122 -0
  17. package/lib/mcps.js +139 -0
  18. package/lib/memory.js +201 -0
  19. package/lib/projects.js +138 -0
  20. package/lib/registry.js +83 -0
  21. package/lib/utils.js +129 -0
  22. package/lib/workstreams.js +652 -0
  23. package/package.json +80 -0
  24. package/scripts/capture-screenshots.js +142 -0
  25. package/scripts/postinstall.js +122 -0
  26. package/scripts/release.sh +71 -0
  27. package/scripts/sync-version.js +77 -0
  28. package/scripts/tauri-prepare.js +328 -0
  29. package/shared/mcp-registry.json +76 -0
  30. package/ui/dist/assets/index-DbZ3_HBD.js +3204 -0
  31. package/ui/dist/assets/index-DjLdm3Mr.css +32 -0
  32. package/ui/dist/icons/icon-192.svg +16 -0
  33. package/ui/dist/icons/icon-512.svg +16 -0
  34. package/ui/dist/index.html +39 -0
  35. package/ui/dist/manifest.json +25 -0
  36. package/ui/dist/sw.js +24 -0
  37. package/ui/dist/tutorial/claude-settings.png +0 -0
  38. package/ui/dist/tutorial/header.png +0 -0
  39. package/ui/dist/tutorial/mcp-registry.png +0 -0
  40. package/ui/dist/tutorial/memory-view.png +0 -0
  41. package/ui/dist/tutorial/permissions.png +0 -0
  42. package/ui/dist/tutorial/plugins-view.png +0 -0
  43. package/ui/dist/tutorial/project-explorer.png +0 -0
  44. package/ui/dist/tutorial/projects-view.png +0 -0
  45. package/ui/dist/tutorial/sidebar.png +0 -0
  46. package/ui/dist/tutorial/tutorial-view.png +0 -0
  47. package/ui/dist/tutorial/workstreams-view.png +0 -0
  48. package/ui/routes/activity.js +58 -0
  49. package/ui/routes/commands.js +74 -0
  50. package/ui/routes/configs.js +329 -0
  51. package/ui/routes/env.js +40 -0
  52. package/ui/routes/file-explorer.js +668 -0
  53. package/ui/routes/index.js +41 -0
  54. package/ui/routes/mcp-discovery.js +235 -0
  55. package/ui/routes/memory.js +385 -0
  56. package/ui/routes/package.json +3 -0
  57. package/ui/routes/plugins.js +466 -0
  58. package/ui/routes/projects.js +198 -0
  59. package/ui/routes/registry.js +30 -0
  60. package/ui/routes/rules.js +74 -0
  61. package/ui/routes/search.js +125 -0
  62. package/ui/routes/settings.js +381 -0
  63. package/ui/routes/subprojects.js +208 -0
  64. package/ui/routes/tool-sync.js +127 -0
  65. package/ui/routes/updates.js +339 -0
  66. package/ui/routes/workstreams.js +224 -0
  67. package/ui/server.cjs +773 -0
  68. package/ui/terminal-server.cjs +160 -0
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Subprojects Routes
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ /**
10
+ * Get subprojects for a directory
11
+ */
12
+ function getSubprojectsForDir(manager, config, dir) {
13
+ const subprojects = [];
14
+
15
+ try {
16
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
17
+
18
+ for (const entry of entries) {
19
+ if (!entry.isDirectory()) continue;
20
+ if (entry.name.startsWith('.')) continue;
21
+
22
+ const fullPath = path.join(dir, entry.name);
23
+ const hasGit = fs.existsSync(path.join(fullPath, '.git'));
24
+
25
+ if (hasGit) {
26
+ const hasClaudeDir = fs.existsSync(path.join(fullPath, '.claude'));
27
+ const hasPackageJson = fs.existsSync(path.join(fullPath, 'package.json'));
28
+ const hasPyproject = fs.existsSync(path.join(fullPath, 'pyproject.toml'));
29
+ const hasCargoToml = fs.existsSync(path.join(fullPath, 'Cargo.toml'));
30
+
31
+ const configPath = path.join(fullPath, '.claude', 'mcps.json');
32
+ const hasConfig = fs.existsSync(configPath);
33
+ const mcpConfig = hasConfig ? manager.loadJson(configPath) : null;
34
+
35
+ subprojects.push({
36
+ dir: fullPath,
37
+ name: entry.name,
38
+ relativePath: entry.name,
39
+ hasConfig,
40
+ markers: {
41
+ claude: hasClaudeDir,
42
+ git: hasGit,
43
+ npm: hasPackageJson,
44
+ python: hasPyproject,
45
+ rust: hasCargoToml
46
+ },
47
+ mcpCount: mcpConfig ? (mcpConfig.include?.length || 0) + Object.keys(mcpConfig.mcpServers || {}).length : 0
48
+ });
49
+ }
50
+ }
51
+ } catch (e) {
52
+ // Permission denied or other errors - skip
53
+ }
54
+
55
+ // Add manual sub-projects from config
56
+ const manualSubprojects = config.manualSubprojects?.[dir] || [];
57
+ for (const subDir of manualSubprojects) {
58
+ if (subprojects.some(p => p.dir === subDir)) continue;
59
+ if (!fs.existsSync(subDir)) continue;
60
+
61
+ const name = path.basename(subDir);
62
+ const hasGit = fs.existsSync(path.join(subDir, '.git'));
63
+ const hasClaudeDir = fs.existsSync(path.join(subDir, '.claude'));
64
+ const hasPackageJson = fs.existsSync(path.join(subDir, 'package.json'));
65
+ const hasPyproject = fs.existsSync(path.join(subDir, 'pyproject.toml'));
66
+ const hasCargoToml = fs.existsSync(path.join(subDir, 'Cargo.toml'));
67
+
68
+ const configPath = path.join(subDir, '.claude', 'mcps.json');
69
+ const hasConfig = fs.existsSync(configPath);
70
+ const mcpConfig = hasConfig ? manager.loadJson(configPath) : null;
71
+
72
+ let relativePath = path.relative(dir, subDir);
73
+ if (relativePath.startsWith('..')) {
74
+ relativePath = subDir.replace(os.homedir(), '~');
75
+ }
76
+
77
+ subprojects.push({
78
+ dir: subDir,
79
+ name,
80
+ relativePath,
81
+ hasConfig,
82
+ isManual: true,
83
+ markers: {
84
+ claude: hasClaudeDir,
85
+ git: hasGit,
86
+ npm: hasPackageJson,
87
+ python: hasPyproject,
88
+ rust: hasCargoToml
89
+ },
90
+ mcpCount: mcpConfig ? (mcpConfig.include?.length || 0) + Object.keys(mcpConfig.mcpServers || {}).length : 0
91
+ });
92
+ }
93
+
94
+ // Filter out hidden sub-projects
95
+ const hiddenList = config.hiddenSubprojects?.[dir] || [];
96
+ return subprojects.filter(sub => !hiddenList.includes(sub.dir));
97
+ }
98
+
99
+ /**
100
+ * Add a manual sub-project
101
+ */
102
+ function addManualSubproject(config, saveConfig, projectDir, subprojectDir) {
103
+ const resolvedProject = path.resolve(projectDir.replace(/^~/, os.homedir()));
104
+ const resolvedSubproject = path.resolve(subprojectDir.replace(/^~/, os.homedir()));
105
+
106
+ if (!fs.existsSync(resolvedSubproject)) {
107
+ return { success: false, error: 'Directory not found' };
108
+ }
109
+
110
+ if (!config.manualSubprojects) {
111
+ config.manualSubprojects = {};
112
+ }
113
+ if (!config.manualSubprojects[resolvedProject]) {
114
+ config.manualSubprojects[resolvedProject] = [];
115
+ }
116
+
117
+ if (!config.manualSubprojects[resolvedProject].includes(resolvedSubproject)) {
118
+ config.manualSubprojects[resolvedProject].push(resolvedSubproject);
119
+ saveConfig(config);
120
+ }
121
+
122
+ return { success: true, resolvedPath: resolvedSubproject };
123
+ }
124
+
125
+ /**
126
+ * Remove a manual sub-project
127
+ */
128
+ function removeManualSubproject(config, saveConfig, projectDir, subprojectDir) {
129
+ const resolvedProject = path.resolve(projectDir.replace(/^~/, os.homedir()));
130
+ const resolvedSubproject = path.resolve(subprojectDir.replace(/^~/, os.homedir()));
131
+
132
+ if (config.manualSubprojects?.[resolvedProject]) {
133
+ config.manualSubprojects[resolvedProject] =
134
+ config.manualSubprojects[resolvedProject].filter(d => d !== resolvedSubproject);
135
+
136
+ if (config.manualSubprojects[resolvedProject].length === 0) {
137
+ delete config.manualSubprojects[resolvedProject];
138
+ }
139
+ saveConfig(config);
140
+ }
141
+
142
+ return { success: true };
143
+ }
144
+
145
+ /**
146
+ * Hide a sub-project
147
+ */
148
+ function hideSubproject(config, saveConfig, projectDir, subprojectDir) {
149
+ const resolvedProject = path.resolve(projectDir.replace(/^~/, os.homedir()));
150
+ const resolvedSubproject = path.resolve(subprojectDir.replace(/^~/, os.homedir()));
151
+
152
+ if (!config.hiddenSubprojects) {
153
+ config.hiddenSubprojects = {};
154
+ }
155
+ if (!config.hiddenSubprojects[resolvedProject]) {
156
+ config.hiddenSubprojects[resolvedProject] = [];
157
+ }
158
+
159
+ if (!config.hiddenSubprojects[resolvedProject].includes(resolvedSubproject)) {
160
+ config.hiddenSubprojects[resolvedProject].push(resolvedSubproject);
161
+ saveConfig(config);
162
+ }
163
+
164
+ return { success: true };
165
+ }
166
+
167
+ /**
168
+ * Unhide a sub-project
169
+ */
170
+ function unhideSubproject(config, saveConfig, projectDir, subprojectDir) {
171
+ const resolvedProject = path.resolve(projectDir.replace(/^~/, os.homedir()));
172
+ const resolvedSubproject = path.resolve(subprojectDir.replace(/^~/, os.homedir()));
173
+
174
+ if (config.hiddenSubprojects?.[resolvedProject]) {
175
+ config.hiddenSubprojects[resolvedProject] =
176
+ config.hiddenSubprojects[resolvedProject].filter(d => d !== resolvedSubproject);
177
+
178
+ if (config.hiddenSubprojects[resolvedProject].length === 0) {
179
+ delete config.hiddenSubprojects[resolvedProject];
180
+ }
181
+ saveConfig(config);
182
+ }
183
+
184
+ return { success: true };
185
+ }
186
+
187
+ /**
188
+ * Get hidden subprojects
189
+ */
190
+ function getHiddenSubprojects(config, projectDir) {
191
+ const resolvedProject = path.resolve(projectDir.replace(/^~/, os.homedir()));
192
+ const hiddenList = config.hiddenSubprojects?.[resolvedProject] || [];
193
+
194
+ return hiddenList.map(dir => ({
195
+ dir,
196
+ name: path.basename(dir),
197
+ exists: fs.existsSync(dir)
198
+ }));
199
+ }
200
+
201
+ module.exports = {
202
+ getSubprojectsForDir,
203
+ addManualSubproject,
204
+ removeManualSubproject,
205
+ hideSubproject,
206
+ unhideSubproject,
207
+ getHiddenSubprojects,
208
+ };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Tool Sync Routes (Claude <-> Gemini <-> Antigravity)
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ /**
9
+ * Get the folder name for a tool
10
+ */
11
+ function getToolFolder(tool) {
12
+ const folders = {
13
+ claude: '.claude',
14
+ gemini: '.gemini',
15
+ antigravity: '.agent'
16
+ };
17
+ return folders[tool] || '.claude';
18
+ }
19
+
20
+ /**
21
+ * Get preview of files that would be synced between tools
22
+ */
23
+ function getSyncPreview(projectDir, source = 'claude', target = 'antigravity') {
24
+ const sourceFolder = getToolFolder(source);
25
+ const targetFolder = getToolFolder(target);
26
+ const sourceRulesDir = path.join(projectDir, sourceFolder, 'rules');
27
+ const targetRulesDir = path.join(projectDir, targetFolder, 'rules');
28
+
29
+ const result = {
30
+ source: { tool: source, folder: sourceFolder, rulesDir: sourceRulesDir },
31
+ target: { tool: target, folder: targetFolder, rulesDir: targetRulesDir },
32
+ files: [],
33
+ sourceExists: fs.existsSync(sourceRulesDir),
34
+ targetExists: fs.existsSync(targetRulesDir),
35
+ };
36
+
37
+ if (!result.sourceExists) {
38
+ return { ...result, error: `Source rules folder not found: ${sourceRulesDir}` };
39
+ }
40
+
41
+ // Get source files
42
+ const sourceFiles = fs.readdirSync(sourceRulesDir).filter(f => f.endsWith('.md'));
43
+
44
+ // Get target files for comparison
45
+ const targetFiles = result.targetExists
46
+ ? fs.readdirSync(targetRulesDir).filter(f => f.endsWith('.md'))
47
+ : [];
48
+
49
+ for (const file of sourceFiles) {
50
+ const sourcePath = path.join(sourceRulesDir, file);
51
+ const targetPath = path.join(targetRulesDir, file);
52
+ const sourceContent = fs.readFileSync(sourcePath, 'utf8');
53
+ const existsInTarget = targetFiles.includes(file);
54
+
55
+ let status = 'new';
56
+ let targetContent = null;
57
+
58
+ if (existsInTarget) {
59
+ targetContent = fs.readFileSync(targetPath, 'utf8');
60
+ status = sourceContent === targetContent ? 'identical' : 'different';
61
+ }
62
+
63
+ result.files.push({
64
+ name: file,
65
+ sourcePath,
66
+ targetPath,
67
+ status,
68
+ sourceSize: sourceContent.length,
69
+ targetSize: targetContent?.length || 0,
70
+ });
71
+ }
72
+
73
+ return result;
74
+ }
75
+
76
+ /**
77
+ * Sync rules between tools
78
+ */
79
+ function syncRules(projectDir, source = 'claude', target = 'antigravity', files = null) {
80
+ const sourceFolder = getToolFolder(source);
81
+ const targetFolder = getToolFolder(target);
82
+ const sourceRulesDir = path.join(projectDir, sourceFolder, 'rules');
83
+ const targetRulesDir = path.join(projectDir, targetFolder, 'rules');
84
+
85
+ if (!fs.existsSync(sourceRulesDir)) {
86
+ return { success: false, error: `Source rules folder not found: ${sourceRulesDir}` };
87
+ }
88
+
89
+ // Create target rules directory if it doesn't exist
90
+ if (!fs.existsSync(targetRulesDir)) {
91
+ fs.mkdirSync(targetRulesDir, { recursive: true });
92
+ }
93
+
94
+ // Get files to sync
95
+ const sourceFiles = fs.readdirSync(sourceRulesDir).filter(f => f.endsWith('.md'));
96
+ const filesToSync = files ? sourceFiles.filter(f => files.includes(f)) : sourceFiles;
97
+
98
+ const results = {
99
+ success: true,
100
+ synced: [],
101
+ skipped: [],
102
+ errors: [],
103
+ };
104
+
105
+ for (const file of filesToSync) {
106
+ try {
107
+ const sourcePath = path.join(sourceRulesDir, file);
108
+ const targetPath = path.join(targetRulesDir, file);
109
+ const content = fs.readFileSync(sourcePath, 'utf8');
110
+ fs.writeFileSync(targetPath, content);
111
+ results.synced.push(file);
112
+ } catch (err) {
113
+ results.errors.push({ file, error: err.message });
114
+ }
115
+ }
116
+
117
+ if (results.errors.length > 0) {
118
+ results.success = results.synced.length > 0;
119
+ }
120
+
121
+ return results;
122
+ }
123
+
124
+ module.exports = {
125
+ getSyncPreview,
126
+ syncRules,
127
+ };
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Updates Routes
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const https = require('https');
8
+ const { execSync } = require('child_process');
9
+
10
+ /**
11
+ * Get version from file (checks both config-loader.js and lib/constants.js)
12
+ */
13
+ function getVersionFromFile(filePath) {
14
+ try {
15
+ if (!fs.existsSync(filePath)) return null;
16
+
17
+ // First try the file directly
18
+ let content = fs.readFileSync(filePath, 'utf8');
19
+ let match = content.match(/const VERSION = ['"]([^'"]+)['"]/);
20
+ if (match) return match[1];
21
+
22
+ // If not found and this is config-loader.js, check lib/constants.js
23
+ if (filePath.endsWith('config-loader.js')) {
24
+ const constantsPath = path.join(path.dirname(filePath), 'lib', 'constants.js');
25
+ if (fs.existsSync(constantsPath)) {
26
+ content = fs.readFileSync(constantsPath, 'utf8');
27
+ match = content.match(/const VERSION = ['"]([^'"]+)['"]/);
28
+ if (match) return match[1];
29
+ }
30
+ }
31
+
32
+ return null;
33
+ } catch (e) {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Fetch npm version and verify it's installable
40
+ * Returns version only if the tarball is accessible (CDN propagated)
41
+ */
42
+ function fetchNpmVersion() {
43
+ return new Promise((resolve) => {
44
+ const url = 'https://registry.npmjs.org/@regression-io/claude-config/latest';
45
+ const req = https.get(url, (res) => {
46
+ let data = '';
47
+ res.on('data', chunk => data += chunk);
48
+ res.on('end', () => {
49
+ try {
50
+ const parsed = JSON.parse(data);
51
+ const version = parsed.version;
52
+ const tarballUrl = parsed.dist?.tarball;
53
+
54
+ if (!version || !tarballUrl) {
55
+ console.log('[update-check] npm registry response missing version or tarball');
56
+ resolve(null);
57
+ return;
58
+ }
59
+
60
+ // Verify tarball is accessible (HEAD request)
61
+ const tarball = new URL(tarballUrl);
62
+ const options = {
63
+ hostname: tarball.hostname,
64
+ path: tarball.pathname,
65
+ method: 'HEAD'
66
+ };
67
+
68
+ const verifyReq = https.request(options, (verifyRes) => {
69
+ // 200 = accessible, anything else = not yet propagated
70
+ if (verifyRes.statusCode === 200) {
71
+ resolve(version);
72
+ } else {
73
+ console.log(`[update-check] tarball not accessible (status ${verifyRes.statusCode})`);
74
+ resolve(null);
75
+ }
76
+ });
77
+ verifyReq.setTimeout(5000, () => {
78
+ console.log('[update-check] tarball verification timed out');
79
+ verifyReq.destroy();
80
+ resolve(null);
81
+ });
82
+ verifyReq.on('error', (e) => {
83
+ console.log('[update-check] tarball verification error:', e.message);
84
+ resolve(null);
85
+ });
86
+ verifyReq.end();
87
+ } catch (e) {
88
+ console.log('[update-check] failed to parse npm response:', e.message);
89
+ resolve(null);
90
+ }
91
+ });
92
+ });
93
+ req.setTimeout(10000, () => {
94
+ console.log('[update-check] npm registry request timed out');
95
+ req.destroy();
96
+ resolve(null);
97
+ });
98
+ req.on('error', (e) => {
99
+ console.log('[update-check] npm registry error:', e.message);
100
+ resolve(null);
101
+ });
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Check if source is newer version
107
+ */
108
+ function isNewerVersion(source, installed) {
109
+ if (!source || !installed) return false;
110
+
111
+ const parseVersion = (v) => v.split('.').map(n => parseInt(n, 10) || 0);
112
+ const s = parseVersion(source);
113
+ const i = parseVersion(installed);
114
+
115
+ for (let j = 0; j < Math.max(s.length, i.length); j++) {
116
+ const sv = s[j] || 0;
117
+ const iv = i[j] || 0;
118
+ if (sv > iv) return true;
119
+ if (sv < iv) return false;
120
+ }
121
+ return false;
122
+ }
123
+
124
+ /**
125
+ * Check for updates
126
+ */
127
+ async function checkForUpdates(manager, dirname) {
128
+ // Get current installed version
129
+ const installedVersion = getVersionFromFile(
130
+ path.join(dirname, '..', 'config-loader.js')
131
+ );
132
+
133
+ // Check npm for latest version
134
+ const npmVersion = await fetchNpmVersion();
135
+
136
+ console.log(`[update-check] installed: ${installedVersion}, npm: ${npmVersion}`);
137
+
138
+ if (npmVersion && isNewerVersion(npmVersion, installedVersion)) {
139
+ console.log('[update-check] update available');
140
+ return {
141
+ updateAvailable: true,
142
+ installedVersion,
143
+ latestVersion: npmVersion,
144
+ sourceVersion: npmVersion, // legacy alias for v0.37.0 compatibility
145
+ updateMethod: 'npm',
146
+ installDir: manager.installDir
147
+ };
148
+ }
149
+
150
+ // No update available from npm
151
+ return {
152
+ updateAvailable: false,
153
+ installedVersion,
154
+ latestVersion: npmVersion || installedVersion,
155
+ sourceVersion: npmVersion || installedVersion, // legacy alias for v0.37.0 compatibility
156
+ installDir: manager.installDir
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Perform npm update
162
+ */
163
+ async function performNpmUpdate(targetVersion) {
164
+ const maxRetries = 3;
165
+ const retryDelayMs = 5000;
166
+
167
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
168
+ try {
169
+ // Use npm install @latest instead of npm update for reliable updates
170
+ execSync('npm install -g @regression-io/claude-config@latest', {
171
+ stdio: 'pipe',
172
+ timeout: 120000
173
+ });
174
+
175
+ return {
176
+ success: true,
177
+ updateMethod: 'npm',
178
+ newVersion: targetVersion || 'latest',
179
+ message: 'Updated via npm. Please restart the UI to use the new version.'
180
+ };
181
+ } catch (error) {
182
+ const isEtarget = error.message.includes('ETARGET') || error.message.includes('No matching version');
183
+
184
+ // Retry on ETARGET (CDN propagation delay) if we have retries left
185
+ if (isEtarget && attempt < maxRetries) {
186
+ await new Promise(resolve => setTimeout(resolve, retryDelayMs));
187
+ continue;
188
+ }
189
+
190
+ // Final attempt failed or non-retryable error
191
+ if (isEtarget) {
192
+ return {
193
+ success: false,
194
+ error: `Version not yet available on npm CDN. Please try again in a minute. (${error.message})`
195
+ };
196
+ }
197
+ return { success: false, error: error.message };
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Copy directory recursively
204
+ */
205
+ function copyDirRecursive(src, dest) {
206
+ if (!fs.existsSync(dest)) {
207
+ fs.mkdirSync(dest, { recursive: true });
208
+ }
209
+
210
+ const entries = fs.readdirSync(src, { withFileTypes: true });
211
+ for (const entry of entries) {
212
+ const srcPath = path.join(src, entry.name);
213
+ const destPath = path.join(dest, entry.name);
214
+
215
+ if (entry.isDirectory()) {
216
+ copyDirRecursive(srcPath, destPath);
217
+ } else {
218
+ fs.copyFileSync(srcPath, destPath);
219
+ }
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Perform local update
225
+ */
226
+ function performLocalUpdate(sourcePath, manager) {
227
+ try {
228
+ const installDir = manager.installDir;
229
+
230
+ if (!fs.existsSync(sourcePath)) {
231
+ return { success: false, error: 'Source path not found' };
232
+ }
233
+
234
+ const sourceLoaderPath = path.join(sourcePath, 'config-loader.js');
235
+ if (!fs.existsSync(sourceLoaderPath)) {
236
+ return { success: false, error: 'config-loader.js not found in source' };
237
+ }
238
+
239
+ const updated = [];
240
+
241
+ // Copy core files
242
+ const filesToCopy = [
243
+ { src: 'config-loader.js', dest: 'config-loader.js' },
244
+ { src: 'shared/mcp-registry.json', dest: 'shared/mcp-registry.json' },
245
+ { src: 'shell/claude-config.zsh', dest: 'shell/claude-config.zsh' },
246
+ { src: 'bin/claude-config', dest: 'bin/claude-config' }
247
+ ];
248
+
249
+ for (const { src, dest } of filesToCopy) {
250
+ const srcPath = path.join(sourcePath, src);
251
+ const destPath = path.join(installDir, dest);
252
+
253
+ if (fs.existsSync(srcPath)) {
254
+ const destDir = path.dirname(destPath);
255
+ if (!fs.existsSync(destDir)) {
256
+ fs.mkdirSync(destDir, { recursive: true });
257
+ }
258
+ fs.copyFileSync(srcPath, destPath);
259
+ updated.push(src);
260
+ }
261
+ }
262
+
263
+ // Copy UI dist files
264
+ const uiDistSourceDir = path.join(sourcePath, 'ui', 'dist');
265
+ const uiDistDestDir = path.join(installDir, 'ui', 'dist');
266
+ if (fs.existsSync(uiDistSourceDir)) {
267
+ copyDirRecursive(uiDistSourceDir, uiDistDestDir);
268
+ updated.push('ui/dist/');
269
+ }
270
+
271
+ // Copy UI server files
272
+ const uiServerFiles = ['server.cjs', 'terminal-server.cjs'];
273
+ for (const file of uiServerFiles) {
274
+ const uiServerSrc = path.join(sourcePath, 'ui', file);
275
+ const uiServerDest = path.join(installDir, 'ui', file);
276
+ if (fs.existsSync(uiServerSrc)) {
277
+ const uiDir = path.dirname(uiServerDest);
278
+ if (!fs.existsSync(uiDir)) {
279
+ fs.mkdirSync(uiDir, { recursive: true });
280
+ }
281
+ fs.copyFileSync(uiServerSrc, uiServerDest);
282
+ updated.push('ui/' + file);
283
+ }
284
+ }
285
+
286
+ // Copy templates
287
+ const templatesSourceDir = path.join(sourcePath, 'templates');
288
+ const templatesDestDir = path.join(installDir, 'templates');
289
+ if (fs.existsSync(templatesSourceDir)) {
290
+ copyDirRecursive(templatesSourceDir, templatesDestDir);
291
+ updated.push('templates/');
292
+ }
293
+
294
+ // Make bin script executable
295
+ const binPath = path.join(installDir, 'bin', 'claude-config');
296
+ if (fs.existsSync(binPath)) {
297
+ fs.chmodSync(binPath, '755');
298
+ }
299
+
300
+ const newVersion = getVersionFromFile(path.join(installDir, 'config-loader.js'));
301
+
302
+ return {
303
+ success: true,
304
+ updateMethod: 'local',
305
+ updated,
306
+ newVersion
307
+ };
308
+
309
+ } catch (error) {
310
+ return { success: false, error: error.message };
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Perform update
316
+ */
317
+ async function performUpdate(options, manager) {
318
+ const { updateMethod, sourcePath, targetVersion } = options;
319
+
320
+ if (updateMethod === 'npm') {
321
+ return await performNpmUpdate(targetVersion);
322
+ }
323
+
324
+ if (sourcePath) {
325
+ return performLocalUpdate(sourcePath, manager);
326
+ }
327
+
328
+ return { success: false, error: 'No update method specified' };
329
+ }
330
+
331
+ module.exports = {
332
+ getVersionFromFile,
333
+ fetchNpmVersion,
334
+ isNewerVersion,
335
+ checkForUpdates,
336
+ performUpdate,
337
+ performNpmUpdate,
338
+ performLocalUpdate,
339
+ };