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.
- package/LICENSE +21 -0
- package/README.md +553 -0
- package/cli.js +431 -0
- package/config-loader.js +294 -0
- package/hooks/activity-track.sh +56 -0
- package/hooks/codex-workstream.sh +44 -0
- package/hooks/gemini-workstream.sh +44 -0
- package/hooks/workstream-inject.sh +20 -0
- package/lib/activity.js +283 -0
- package/lib/apply.js +344 -0
- package/lib/cli.js +267 -0
- package/lib/config.js +171 -0
- package/lib/constants.js +55 -0
- package/lib/env.js +114 -0
- package/lib/index.js +47 -0
- package/lib/init.js +122 -0
- package/lib/mcps.js +139 -0
- package/lib/memory.js +201 -0
- package/lib/projects.js +138 -0
- package/lib/registry.js +83 -0
- package/lib/utils.js +129 -0
- package/lib/workstreams.js +652 -0
- package/package.json +80 -0
- package/scripts/capture-screenshots.js +142 -0
- package/scripts/postinstall.js +122 -0
- package/scripts/release.sh +71 -0
- package/scripts/sync-version.js +77 -0
- package/scripts/tauri-prepare.js +328 -0
- package/shared/mcp-registry.json +76 -0
- package/ui/dist/assets/index-DbZ3_HBD.js +3204 -0
- package/ui/dist/assets/index-DjLdm3Mr.css +32 -0
- package/ui/dist/icons/icon-192.svg +16 -0
- package/ui/dist/icons/icon-512.svg +16 -0
- package/ui/dist/index.html +39 -0
- package/ui/dist/manifest.json +25 -0
- package/ui/dist/sw.js +24 -0
- package/ui/dist/tutorial/claude-settings.png +0 -0
- package/ui/dist/tutorial/header.png +0 -0
- package/ui/dist/tutorial/mcp-registry.png +0 -0
- package/ui/dist/tutorial/memory-view.png +0 -0
- package/ui/dist/tutorial/permissions.png +0 -0
- package/ui/dist/tutorial/plugins-view.png +0 -0
- package/ui/dist/tutorial/project-explorer.png +0 -0
- package/ui/dist/tutorial/projects-view.png +0 -0
- package/ui/dist/tutorial/sidebar.png +0 -0
- package/ui/dist/tutorial/tutorial-view.png +0 -0
- package/ui/dist/tutorial/workstreams-view.png +0 -0
- package/ui/routes/activity.js +58 -0
- package/ui/routes/commands.js +74 -0
- package/ui/routes/configs.js +329 -0
- package/ui/routes/env.js +40 -0
- package/ui/routes/file-explorer.js +668 -0
- package/ui/routes/index.js +41 -0
- package/ui/routes/mcp-discovery.js +235 -0
- package/ui/routes/memory.js +385 -0
- package/ui/routes/package.json +3 -0
- package/ui/routes/plugins.js +466 -0
- package/ui/routes/projects.js +198 -0
- package/ui/routes/registry.js +30 -0
- package/ui/routes/rules.js +74 -0
- package/ui/routes/search.js +125 -0
- package/ui/routes/settings.js +381 -0
- package/ui/routes/subprojects.js +208 -0
- package/ui/routes/tool-sync.js +127 -0
- package/ui/routes/updates.js +339 -0
- package/ui/routes/workstreams.js +224 -0
- package/ui/server.cjs +773 -0
- 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
|
+
};
|