opencode-studio-server 1.26.0 → 1.28.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/AGENTS.md +0 -1
- package/README.md +0 -1
- package/index.js +271 -192
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -7,7 +7,6 @@ Express API backend (port 3001). Single-file architecture.
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
|------|---------|
|
|
9
9
|
| `index.js` | All routes, config IO, auth, skills, plugins, usage stats |
|
|
10
|
-
| `proxy-manager.js` | Manages CLIProxyAPI process and config |
|
|
11
10
|
| `cli.js` | npm bin entry, protocol URL parser, pending action queue |
|
|
12
11
|
| `register-protocol.js` | OS-specific `opencodestudio://` handler registration |
|
|
13
12
|
|
package/README.md
CHANGED
|
@@ -31,7 +31,6 @@ The server runs on port **3001** and provides an API for managing your local Ope
|
|
|
31
31
|
- **Protocol Handler**: `opencodestudio://` support for one-click actions
|
|
32
32
|
- **Config Management**: Reads/writes `~/.config/opencode/opencode.json`
|
|
33
33
|
- **MCP Management**: Add/remove/toggle MCP servers
|
|
34
|
-
- **Proxy Manager**: Manage CLIProxyAPI instance
|
|
35
34
|
- **Auth**: Manage authentication profiles
|
|
36
35
|
|
|
37
36
|
## License
|
package/index.js
CHANGED
|
@@ -461,6 +461,16 @@ function saveStudioConfig(config) {
|
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
const getWslDistributions = () => {
|
|
465
|
+
try {
|
|
466
|
+
const { execSync } = require('child_process');
|
|
467
|
+
const stdout = execSync('wsl.exe -l -q', { encoding: 'utf16le', stdio: ['ignore', 'pipe', 'pipe'], timeout: 1000 });
|
|
468
|
+
return stdout.trim().split('\n').filter(d => d.length > 0);
|
|
469
|
+
} catch {
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
464
474
|
const getPaths = () => {
|
|
465
475
|
const platform = process.platform;
|
|
466
476
|
const home = os.homedir();
|
|
@@ -473,6 +483,13 @@ const getPaths = () => {
|
|
|
473
483
|
candidates.push(path.join(process.env.APPDATA, 'opencode', 'opencode.json'));
|
|
474
484
|
}
|
|
475
485
|
|
|
486
|
+
if (platform === 'win32') {
|
|
487
|
+
const distros = getWslDistributions();
|
|
488
|
+
for (const distro of distros) {
|
|
489
|
+
candidates.push(`\\\\wsl$\\${distro}\\home\\${os.userInfo().username}\\.config\\opencode\\opencode.json`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
476
493
|
const studioConfig = loadStudioConfig();
|
|
477
494
|
const manualPath = studioConfig.configPath;
|
|
478
495
|
|
|
@@ -1020,56 +1037,56 @@ app.get('/api/ohmyopencode', (req, res) => {
|
|
|
1020
1037
|
res.json({ path: ohMyPath, exists, config, preferences });
|
|
1021
1038
|
});
|
|
1022
1039
|
|
|
1023
|
-
app.post('/api/ohmyopencode', (req, res) => {
|
|
1024
|
-
try {
|
|
1025
|
-
const { preferences } = req.body;
|
|
1026
|
-
if (!preferences || !preferences.agents) {
|
|
1027
|
-
return res.status(400).json({ error: 'Missing preferences.agents' });
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const studio = loadStudioConfig();
|
|
1031
|
-
studio.ohmy = preferences;
|
|
1032
|
-
saveStudioConfig(studio);
|
|
1033
|
-
|
|
1034
|
-
const currentConfig = loadOhMyOpenCodeConfig() || {};
|
|
1035
|
-
const warnings = [];
|
|
1036
|
-
|
|
1037
|
-
for (const [agentName, agentPrefs] of Object.entries(preferences.agents)) {
|
|
1038
|
-
const choices = agentPrefs.choices || [];
|
|
1039
|
-
const available = choices.find(c => c.available);
|
|
1040
|
-
if (available) {
|
|
1041
|
-
if (!currentConfig.agents) currentConfig.agents = {};
|
|
1042
|
-
const agentConfig = { model: available.model };
|
|
1043
|
-
|
|
1044
|
-
if (available.thinking && available.thinking.type === 'enabled') {
|
|
1045
|
-
agentConfig.thinking = { type: 'enabled' };
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
if (available.reasoning && available.reasoning.effort) {
|
|
1049
|
-
agentConfig.reasoning = { effort: available.reasoning.effort };
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
currentConfig.agents[agentName] = agentConfig;
|
|
1053
|
-
} else if (choices.length > 0) {
|
|
1054
|
-
warnings.push(`No available model for agent "${agentName}"`);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
saveOhMyOpenCodeConfig(currentConfig);
|
|
1059
|
-
|
|
1060
|
-
const ohMyPath = getOhMyOpenCodeConfigPath();
|
|
1061
|
-
res.json({
|
|
1062
|
-
success: true,
|
|
1063
|
-
path: ohMyPath,
|
|
1064
|
-
exists: true,
|
|
1065
|
-
config: currentConfig,
|
|
1066
|
-
preferences,
|
|
1067
|
-
warnings: warnings.length > 0 ? warnings : undefined
|
|
1068
|
-
});
|
|
1069
|
-
} catch (err) {
|
|
1070
|
-
res.status(500).json({ error: err.message });
|
|
1071
|
-
}
|
|
1072
|
-
});
|
|
1040
|
+
app.post('/api/ohmyopencode', (req, res) => {
|
|
1041
|
+
try {
|
|
1042
|
+
const { preferences } = req.body;
|
|
1043
|
+
if (!preferences || !preferences.agents) {
|
|
1044
|
+
return res.status(400).json({ error: 'Missing preferences.agents' });
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const studio = loadStudioConfig();
|
|
1048
|
+
studio.ohmy = preferences;
|
|
1049
|
+
saveStudioConfig(studio);
|
|
1050
|
+
|
|
1051
|
+
const currentConfig = loadOhMyOpenCodeConfig() || {};
|
|
1052
|
+
const warnings = [];
|
|
1053
|
+
|
|
1054
|
+
for (const [agentName, agentPrefs] of Object.entries(preferences.agents)) {
|
|
1055
|
+
const choices = agentPrefs.choices || [];
|
|
1056
|
+
const available = choices.find(c => c.available);
|
|
1057
|
+
if (available) {
|
|
1058
|
+
if (!currentConfig.agents) currentConfig.agents = {};
|
|
1059
|
+
const agentConfig = { model: available.model };
|
|
1060
|
+
|
|
1061
|
+
if (available.thinking && available.thinking.type === 'enabled') {
|
|
1062
|
+
agentConfig.thinking = { type: 'enabled' };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (available.reasoning && available.reasoning.effort) {
|
|
1066
|
+
agentConfig.reasoning = { effort: available.reasoning.effort };
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
currentConfig.agents[agentName] = agentConfig;
|
|
1070
|
+
} else if (choices.length > 0) {
|
|
1071
|
+
warnings.push(`No available model for agent "${agentName}"`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
saveOhMyOpenCodeConfig(currentConfig);
|
|
1076
|
+
|
|
1077
|
+
const ohMyPath = getOhMyOpenCodeConfigPath();
|
|
1078
|
+
res.json({
|
|
1079
|
+
success: true,
|
|
1080
|
+
path: ohMyPath,
|
|
1081
|
+
exists: true,
|
|
1082
|
+
config: currentConfig,
|
|
1083
|
+
preferences,
|
|
1084
|
+
warnings: warnings.length > 0 ? warnings : undefined
|
|
1085
|
+
});
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
res.status(500).json({ error: err.message });
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1073
1090
|
|
|
1074
1091
|
// ============================================
|
|
1075
1092
|
// GITHUB BACKUP
|
|
@@ -1095,74 +1112,74 @@ async function getGitHubUser(token) {
|
|
|
1095
1112
|
return await response.json();
|
|
1096
1113
|
}
|
|
1097
1114
|
|
|
1098
|
-
async function ensureGitHubRepo(token, repoName) {
|
|
1099
|
-
const [owner, repo] = repoName.split('/');
|
|
1100
|
-
|
|
1101
|
-
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
1102
|
-
headers: { 'Authorization': `Bearer ${token}` }
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
if (response.ok) {
|
|
1106
|
-
return await response.json();
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
if (response.status === 404) {
|
|
1110
|
-
const createRes = await fetch(`https://api.github.com/user/repos`, {
|
|
1111
|
-
method: 'POST',
|
|
1112
|
-
headers: {
|
|
1113
|
-
'Authorization': `Bearer ${token}`,
|
|
1114
|
-
'Content-Type': 'application/json'
|
|
1115
|
-
},
|
|
1116
|
-
body: JSON.stringify({
|
|
1117
|
-
name: repo,
|
|
1118
|
-
private: true,
|
|
1119
|
-
description: 'OpenCode Studio backup',
|
|
1120
|
-
auto_init: true
|
|
1121
|
-
})
|
|
1122
|
-
});
|
|
1123
|
-
|
|
1124
|
-
if (createRes.ok) {
|
|
1125
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
1126
|
-
return await createRes.json();
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
const err = await createRes.text();
|
|
1130
|
-
throw new Error(`Failed to create repo: ${err}`);
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
const err = await response.text();
|
|
1134
|
-
throw new Error(`Failed to check repo: ${err}`);
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
function copyDirContents(src, dest) {
|
|
1138
|
-
if (!fs.existsSync(src)) return;
|
|
1139
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
1140
|
-
const SKIP_DIRS = ['node_modules', '.git', '.next', 'cache'];
|
|
1141
|
-
const SKIP_EXT = ['.log', '.tmp', '.db', '.sqlite', '.cache', '.pack', '.idx'];
|
|
1142
|
-
|
|
1143
|
-
for (const name of fs.readdirSync(src)) {
|
|
1144
|
-
const srcPath = path.join(src, name);
|
|
1145
|
-
const destPath = path.join(dest, name);
|
|
1146
|
-
const stat = fs.statSync(srcPath);
|
|
1147
|
-
|
|
1148
|
-
if (stat.isDirectory()) {
|
|
1149
|
-
if (SKIP_DIRS.includes(name)) continue;
|
|
1150
|
-
copyDirContents(srcPath, destPath);
|
|
1151
|
-
} else {
|
|
1152
|
-
if (SKIP_EXT.some(ext => name.endsWith(ext))) continue;
|
|
1153
|
-
fs.copyFileSync(srcPath, destPath);
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
function execPromise(cmd, opts = {}) {
|
|
1159
|
-
return new Promise((resolve, reject) => {
|
|
1160
|
-
exec(cmd, opts, (err, stdout, stderr) => {
|
|
1161
|
-
if (err) reject(new Error(stderr || err.message));
|
|
1162
|
-
else resolve(stdout.trim());
|
|
1163
|
-
});
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1115
|
+
async function ensureGitHubRepo(token, repoName) {
|
|
1116
|
+
const [owner, repo] = repoName.split('/');
|
|
1117
|
+
|
|
1118
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
1119
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
if (response.ok) {
|
|
1123
|
+
return await response.json();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (response.status === 404) {
|
|
1127
|
+
const createRes = await fetch(`https://api.github.com/user/repos`, {
|
|
1128
|
+
method: 'POST',
|
|
1129
|
+
headers: {
|
|
1130
|
+
'Authorization': `Bearer ${token}`,
|
|
1131
|
+
'Content-Type': 'application/json'
|
|
1132
|
+
},
|
|
1133
|
+
body: JSON.stringify({
|
|
1134
|
+
name: repo,
|
|
1135
|
+
private: true,
|
|
1136
|
+
description: 'OpenCode Studio backup',
|
|
1137
|
+
auto_init: true
|
|
1138
|
+
})
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
if (createRes.ok) {
|
|
1142
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1143
|
+
return await createRes.json();
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const err = await createRes.text();
|
|
1147
|
+
throw new Error(`Failed to create repo: ${err}`);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const err = await response.text();
|
|
1151
|
+
throw new Error(`Failed to check repo: ${err}`);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function copyDirContents(src, dest) {
|
|
1155
|
+
if (!fs.existsSync(src)) return;
|
|
1156
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
1157
|
+
const SKIP_DIRS = ['node_modules', '.git', '.next', 'cache'];
|
|
1158
|
+
const SKIP_EXT = ['.log', '.tmp', '.db', '.sqlite', '.cache', '.pack', '.idx'];
|
|
1159
|
+
|
|
1160
|
+
for (const name of fs.readdirSync(src)) {
|
|
1161
|
+
const srcPath = path.join(src, name);
|
|
1162
|
+
const destPath = path.join(dest, name);
|
|
1163
|
+
const stat = fs.statSync(srcPath);
|
|
1164
|
+
|
|
1165
|
+
if (stat.isDirectory()) {
|
|
1166
|
+
if (SKIP_DIRS.includes(name)) continue;
|
|
1167
|
+
copyDirContents(srcPath, destPath);
|
|
1168
|
+
} else {
|
|
1169
|
+
if (SKIP_EXT.some(ext => name.endsWith(ext))) continue;
|
|
1170
|
+
fs.copyFileSync(srcPath, destPath);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function execPromise(cmd, opts = {}) {
|
|
1176
|
+
return new Promise((resolve, reject) => {
|
|
1177
|
+
exec(cmd, opts, (err, stdout, stderr) => {
|
|
1178
|
+
if (err) reject(new Error(stderr || err.message));
|
|
1179
|
+
else resolve(stdout.trim());
|
|
1180
|
+
});
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1166
1183
|
|
|
1167
1184
|
app.get('/api/github/backup/status', async (req, res) => {
|
|
1168
1185
|
try {
|
|
@@ -1176,7 +1193,7 @@ app.get('/api/github/backup/status', async (req, res) => {
|
|
|
1176
1193
|
if (!user) return res.json({ connected: false, config: backupConfig, error: 'Failed to get GitHub user' });
|
|
1177
1194
|
|
|
1178
1195
|
if (!backupConfig.repo) {
|
|
1179
|
-
return res.json({ connected: true, user: user.login, config: backupConfig });
|
|
1196
|
+
return res.json({ connected: true, user: user.login, config: backupConfig, autoSync: studio.githubAutoSync || false });
|
|
1180
1197
|
}
|
|
1181
1198
|
|
|
1182
1199
|
const owner = backupConfig.owner || user.login;
|
|
@@ -1185,87 +1202,149 @@ app.get('/api/github/backup/status', async (req, res) => {
|
|
|
1185
1202
|
});
|
|
1186
1203
|
|
|
1187
1204
|
if (!response.ok) {
|
|
1188
|
-
return res.json({ connected: true, user: user.login, config: backupConfig, repoExists: false });
|
|
1205
|
+
return res.json({ connected: true, user: user.login, config: backupConfig, repoExists: false, autoSync: studio.githubAutoSync || false });
|
|
1189
1206
|
}
|
|
1190
1207
|
|
|
1191
1208
|
const data = await response.json();
|
|
1192
|
-
res.json({ connected: true, user: user.login, config: backupConfig, repoExists: true, lastUpdated: data.pushed_at });
|
|
1209
|
+
res.json({ connected: true, user: user.login, config: backupConfig, repoExists: true, lastUpdated: data.pushed_at, autoSync: studio.githubAutoSync || false });
|
|
1193
1210
|
} catch (err) {
|
|
1194
1211
|
res.json({ connected: false, error: err.message });
|
|
1195
1212
|
}
|
|
1196
1213
|
});
|
|
1197
1214
|
|
|
1198
|
-
app.post('/api/github/backup', async (req, res) => {
|
|
1199
|
-
let tempDir = null;
|
|
1200
|
-
try {
|
|
1201
|
-
const token = await getGitHubToken();
|
|
1202
|
-
if (!token) return res.status(400).json({ error: 'Not logged in to gh CLI. Run: gh auth login' });
|
|
1203
|
-
|
|
1204
|
-
const user = await getGitHubUser(token);
|
|
1205
|
-
if (!user) return res.status(400).json({ error: 'Failed to get GitHub user' });
|
|
1206
|
-
|
|
1207
|
-
const { owner, repo, branch } = req.body;
|
|
1208
|
-
const studio = loadStudioConfig();
|
|
1209
|
-
|
|
1210
|
-
const finalOwner = owner || studio.githubBackup?.owner || user.login;
|
|
1211
|
-
const finalRepo = repo || studio.githubBackup?.repo;
|
|
1212
|
-
const finalBranch = branch || studio.githubBackup?.branch || 'main';
|
|
1213
|
-
|
|
1214
|
-
if (!finalRepo) return res.status(400).json({ error: 'No repo name provided' });
|
|
1215
|
-
|
|
1216
|
-
const repoName = `${finalOwner}/${finalRepo}`;
|
|
1217
|
-
|
|
1218
|
-
await ensureGitHubRepo(token, repoName);
|
|
1219
|
-
|
|
1220
|
-
const opencodeConfig = getConfigPath();
|
|
1221
|
-
if (!opencodeConfig) return res.status(400).json({ error: 'No opencode config path found' });
|
|
1222
|
-
|
|
1223
|
-
const opencodeDir = path.dirname(opencodeConfig);
|
|
1224
|
-
const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
|
|
1225
|
-
|
|
1226
|
-
tempDir = path.join(os.tmpdir(), `opencode-backup-${Date.now()}`);
|
|
1227
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
1228
|
-
|
|
1229
|
-
await execPromise(`git clone --depth 1 https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
|
|
1230
|
-
|
|
1231
|
-
const backupOpencodeDir = path.join(tempDir, 'opencode');
|
|
1232
|
-
const backupStudioDir = path.join(tempDir, 'opencode-studio');
|
|
1233
|
-
|
|
1234
|
-
if (fs.existsSync(backupOpencodeDir)) fs.rmSync(backupOpencodeDir, { recursive: true });
|
|
1235
|
-
if (fs.existsSync(backupStudioDir)) fs.rmSync(backupStudioDir, { recursive: true });
|
|
1236
|
-
|
|
1237
|
-
copyDirContents(opencodeDir, backupOpencodeDir);
|
|
1238
|
-
copyDirContents(studioDir, backupStudioDir);
|
|
1239
|
-
|
|
1240
|
-
await execPromise('git add -A', { cwd: tempDir });
|
|
1241
|
-
|
|
1242
|
-
const timestamp = new Date().toISOString();
|
|
1243
|
-
const commitMessage = `OpenCode Studio backup ${timestamp}`;
|
|
1244
|
-
|
|
1245
|
-
try {
|
|
1246
|
-
await execPromise(`git commit -m "${commitMessage}"`, { cwd: tempDir });
|
|
1247
|
-
await execPromise(`git push origin ${finalBranch}`, { cwd: tempDir });
|
|
1248
|
-
} catch (e) {
|
|
1249
|
-
if (e.message.includes('nothing to commit')) {
|
|
1250
|
-
fs.rmSync(tempDir, { recursive: true });
|
|
1251
|
-
return res.json({ success: true, timestamp, message: 'No changes to backup', url: `https://github.com/${repoName}` });
|
|
1252
|
-
}
|
|
1253
|
-
throw e;
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
fs.rmSync(tempDir, { recursive: true });
|
|
1257
|
-
|
|
1258
|
-
studio.githubBackup = { owner: finalOwner, repo: finalRepo, branch: finalBranch };
|
|
1259
|
-
studio.lastGithubBackup = timestamp;
|
|
1260
|
-
saveStudioConfig(studio);
|
|
1261
|
-
|
|
1262
|
-
res.json({ success: true, timestamp, url: `https://github.com/${repoName}` });
|
|
1263
|
-
} catch (err) {
|
|
1264
|
-
if (tempDir && fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
|
|
1265
|
-
console.error('GitHub backup error:', err);
|
|
1266
|
-
res.status(500).json({ error: err.message });
|
|
1267
|
-
}
|
|
1268
|
-
});
|
|
1215
|
+
app.post('/api/github/backup', async (req, res) => {
|
|
1216
|
+
let tempDir = null;
|
|
1217
|
+
try {
|
|
1218
|
+
const token = await getGitHubToken();
|
|
1219
|
+
if (!token) return res.status(400).json({ error: 'Not logged in to gh CLI. Run: gh auth login' });
|
|
1220
|
+
|
|
1221
|
+
const user = await getGitHubUser(token);
|
|
1222
|
+
if (!user) return res.status(400).json({ error: 'Failed to get GitHub user' });
|
|
1223
|
+
|
|
1224
|
+
const { owner, repo, branch } = req.body;
|
|
1225
|
+
const studio = loadStudioConfig();
|
|
1226
|
+
|
|
1227
|
+
const finalOwner = owner || studio.githubBackup?.owner || user.login;
|
|
1228
|
+
const finalRepo = repo || studio.githubBackup?.repo;
|
|
1229
|
+
const finalBranch = branch || studio.githubBackup?.branch || 'main';
|
|
1230
|
+
|
|
1231
|
+
if (!finalRepo) return res.status(400).json({ error: 'No repo name provided' });
|
|
1232
|
+
|
|
1233
|
+
const repoName = `${finalOwner}/${finalRepo}`;
|
|
1234
|
+
|
|
1235
|
+
await ensureGitHubRepo(token, repoName);
|
|
1236
|
+
|
|
1237
|
+
const opencodeConfig = getConfigPath();
|
|
1238
|
+
if (!opencodeConfig) return res.status(400).json({ error: 'No opencode config path found' });
|
|
1239
|
+
|
|
1240
|
+
const opencodeDir = path.dirname(opencodeConfig);
|
|
1241
|
+
const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
|
|
1242
|
+
|
|
1243
|
+
tempDir = path.join(os.tmpdir(), `opencode-backup-${Date.now()}`);
|
|
1244
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1245
|
+
|
|
1246
|
+
await execPromise(`git clone --depth 1 https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
|
|
1247
|
+
|
|
1248
|
+
const backupOpencodeDir = path.join(tempDir, 'opencode');
|
|
1249
|
+
const backupStudioDir = path.join(tempDir, 'opencode-studio');
|
|
1250
|
+
|
|
1251
|
+
if (fs.existsSync(backupOpencodeDir)) fs.rmSync(backupOpencodeDir, { recursive: true });
|
|
1252
|
+
if (fs.existsSync(backupStudioDir)) fs.rmSync(backupStudioDir, { recursive: true });
|
|
1253
|
+
|
|
1254
|
+
copyDirContents(opencodeDir, backupOpencodeDir);
|
|
1255
|
+
copyDirContents(studioDir, backupStudioDir);
|
|
1256
|
+
|
|
1257
|
+
await execPromise('git add -A', { cwd: tempDir });
|
|
1258
|
+
|
|
1259
|
+
const timestamp = new Date().toISOString();
|
|
1260
|
+
const commitMessage = `OpenCode Studio backup ${timestamp}`;
|
|
1261
|
+
|
|
1262
|
+
try {
|
|
1263
|
+
await execPromise(`git commit -m "${commitMessage}"`, { cwd: tempDir });
|
|
1264
|
+
await execPromise(`git push origin ${finalBranch}`, { cwd: tempDir });
|
|
1265
|
+
} catch (e) {
|
|
1266
|
+
if (e.message.includes('nothing to commit')) {
|
|
1267
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
1268
|
+
return res.json({ success: true, timestamp, message: 'No changes to backup', url: `https://github.com/${repoName}` });
|
|
1269
|
+
}
|
|
1270
|
+
throw e;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
1274
|
+
|
|
1275
|
+
studio.githubBackup = { owner: finalOwner, repo: finalRepo, branch: finalBranch };
|
|
1276
|
+
studio.lastGithubBackup = timestamp;
|
|
1277
|
+
saveStudioConfig(studio);
|
|
1278
|
+
|
|
1279
|
+
res.json({ success: true, timestamp, url: `https://github.com/${repoName}` });
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
if (tempDir && fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
|
|
1282
|
+
console.error('GitHub backup error:', err);
|
|
1283
|
+
res.status(500).json({ error: err.message });
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
app.post('/api/github/restore', async (req, res) => {
|
|
1288
|
+
let tempDir = null;
|
|
1289
|
+
try {
|
|
1290
|
+
const token = await getGitHubToken();
|
|
1291
|
+
if (!token) return res.status(400).json({ error: 'Not logged in to gh CLI. Run: gh auth login' });
|
|
1292
|
+
|
|
1293
|
+
const user = await getGitHubUser(token);
|
|
1294
|
+
if (!user) return res.status(400).json({ error: 'Failed to get GitHub user' });
|
|
1295
|
+
|
|
1296
|
+
const { owner, repo, branch } = req.body;
|
|
1297
|
+
const studio = loadStudioConfig();
|
|
1298
|
+
|
|
1299
|
+
const finalOwner = owner || studio.githubBackup?.owner || user.login;
|
|
1300
|
+
const finalRepo = repo || studio.githubBackup?.repo;
|
|
1301
|
+
const finalBranch = branch || studio.githubBackup?.branch || 'main';
|
|
1302
|
+
|
|
1303
|
+
if (!finalRepo) return res.status(400).json({ error: 'No repo configured' });
|
|
1304
|
+
|
|
1305
|
+
const repoName = `${finalOwner}/${finalRepo}`;
|
|
1306
|
+
|
|
1307
|
+
const opencodeConfig = getConfigPath();
|
|
1308
|
+
if (!opencodeConfig) return res.status(400).json({ error: 'No opencode config path found' });
|
|
1309
|
+
|
|
1310
|
+
const opencodeDir = path.dirname(opencodeConfig);
|
|
1311
|
+
const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
|
|
1312
|
+
|
|
1313
|
+
tempDir = path.join(os.tmpdir(), `opencode-restore-${Date.now()}`);
|
|
1314
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1315
|
+
|
|
1316
|
+
await execPromise(`git clone --depth 1 -b ${finalBranch} https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
|
|
1317
|
+
|
|
1318
|
+
const backupOpencodeDir = path.join(tempDir, 'opencode');
|
|
1319
|
+
const backupStudioDir = path.join(tempDir, 'opencode-studio');
|
|
1320
|
+
|
|
1321
|
+
if (!fs.existsSync(backupOpencodeDir)) {
|
|
1322
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
1323
|
+
return res.status(400).json({ error: 'No opencode backup found in repository' });
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
copyDirContents(backupOpencodeDir, opencodeDir);
|
|
1327
|
+
if (fs.existsSync(backupStudioDir)) {
|
|
1328
|
+
copyDirContents(backupStudioDir, studioDir);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
1332
|
+
|
|
1333
|
+
res.json({ success: true, message: 'Config restored from GitHub' });
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
if (tempDir && fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
|
|
1336
|
+
console.error('GitHub restore error:', err);
|
|
1337
|
+
res.status(500).json({ error: err.message });
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
app.post('/api/github/autosync', async (req, res) => {
|
|
1342
|
+
const studio = loadStudioConfig();
|
|
1343
|
+
const enabled = req.body.enabled;
|
|
1344
|
+
studio.githubAutoSync = enabled;
|
|
1345
|
+
saveStudioConfig(studio);
|
|
1346
|
+
res.json({ success: true, enabled });
|
|
1347
|
+
});
|
|
1269
1348
|
|
|
1270
1349
|
const getSkillDir = () => {
|
|
1271
1350
|
const cp = getConfigPath();
|