opencode-studio-server 1.27.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 +279 -262
- 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 {
|
|
@@ -1175,161 +1192,161 @@ app.get('/api/github/backup/status', async (req, res) => {
|
|
|
1175
1192
|
const user = await getGitHubUser(token);
|
|
1176
1193
|
if (!user) return res.json({ connected: false, config: backupConfig, error: 'Failed to get GitHub user' });
|
|
1177
1194
|
|
|
1178
|
-
if (!backupConfig.repo) {
|
|
1179
|
-
return res.json({ connected: true, user: user.login, config: backupConfig, autoSync: studio.githubAutoSync || false });
|
|
1180
|
-
}
|
|
1195
|
+
if (!backupConfig.repo) {
|
|
1196
|
+
return res.json({ connected: true, user: user.login, config: backupConfig, autoSync: studio.githubAutoSync || false });
|
|
1197
|
+
}
|
|
1181
1198
|
|
|
1182
1199
|
const owner = backupConfig.owner || user.login;
|
|
1183
1200
|
const response = await fetch(`https://api.github.com/repos/${owner}/${backupConfig.repo}`, {
|
|
1184
1201
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
1185
1202
|
});
|
|
1186
1203
|
|
|
1187
|
-
if (!response.ok) {
|
|
1188
|
-
return res.json({ connected: true, user: user.login, config: backupConfig, repoExists: false, autoSync: studio.githubAutoSync || false });
|
|
1189
|
-
}
|
|
1204
|
+
if (!response.ok) {
|
|
1205
|
+
return res.json({ connected: true, user: user.login, config: backupConfig, repoExists: false, autoSync: studio.githubAutoSync || false });
|
|
1206
|
+
}
|
|
1190
1207
|
|
|
1191
|
-
const data = await response.json();
|
|
1192
|
-
res.json({ connected: true, user: user.login, config: backupConfig, repoExists: true, lastUpdated: data.pushed_at, autoSync: studio.githubAutoSync || false });
|
|
1193
|
-
} catch (err) {
|
|
1208
|
+
const data = await response.json();
|
|
1209
|
+
res.json({ connected: true, user: user.login, config: backupConfig, repoExists: true, lastUpdated: data.pushed_at, autoSync: studio.githubAutoSync || false });
|
|
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
|
-
});
|
|
1269
|
-
|
|
1270
|
-
app.post('/api/github/restore', async (req, res) => {
|
|
1271
|
-
let tempDir = null;
|
|
1272
|
-
try {
|
|
1273
|
-
const token = await getGitHubToken();
|
|
1274
|
-
if (!token) return res.status(400).json({ error: 'Not logged in to gh CLI. Run: gh auth login' });
|
|
1275
|
-
|
|
1276
|
-
const user = await getGitHubUser(token);
|
|
1277
|
-
if (!user) return res.status(400).json({ error: 'Failed to get GitHub user' });
|
|
1278
|
-
|
|
1279
|
-
const { owner, repo, branch } = req.body;
|
|
1280
|
-
const studio = loadStudioConfig();
|
|
1281
|
-
|
|
1282
|
-
const finalOwner = owner || studio.githubBackup?.owner || user.login;
|
|
1283
|
-
const finalRepo = repo || studio.githubBackup?.repo;
|
|
1284
|
-
const finalBranch = branch || studio.githubBackup?.branch || 'main';
|
|
1285
|
-
|
|
1286
|
-
if (!finalRepo) return res.status(400).json({ error: 'No repo configured' });
|
|
1287
|
-
|
|
1288
|
-
const repoName = `${finalOwner}/${finalRepo}`;
|
|
1289
|
-
|
|
1290
|
-
const opencodeConfig = getConfigPath();
|
|
1291
|
-
if (!opencodeConfig) return res.status(400).json({ error: 'No opencode config path found' });
|
|
1292
|
-
|
|
1293
|
-
const opencodeDir = path.dirname(opencodeConfig);
|
|
1294
|
-
const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
|
|
1295
|
-
|
|
1296
|
-
tempDir = path.join(os.tmpdir(), `opencode-restore-${Date.now()}`);
|
|
1297
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
1298
|
-
|
|
1299
|
-
await execPromise(`git clone --depth 1 -b ${finalBranch} https://x-access-token:${token}@github.com/${repoName}.git .`, { cwd: tempDir });
|
|
1300
|
-
|
|
1301
|
-
const backupOpencodeDir = path.join(tempDir, 'opencode');
|
|
1302
|
-
const backupStudioDir = path.join(tempDir, 'opencode-studio');
|
|
1303
|
-
|
|
1304
|
-
if (!fs.existsSync(backupOpencodeDir)) {
|
|
1305
|
-
fs.rmSync(tempDir, { recursive: true });
|
|
1306
|
-
return res.status(400).json({ error: 'No opencode backup found in repository' });
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
copyDirContents(backupOpencodeDir, opencodeDir);
|
|
1310
|
-
if (fs.existsSync(backupStudioDir)) {
|
|
1311
|
-
copyDirContents(backupStudioDir, studioDir);
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
fs.rmSync(tempDir, { recursive: true });
|
|
1315
|
-
|
|
1316
|
-
res.json({ success: true, message: 'Config restored from GitHub' });
|
|
1317
|
-
} catch (err) {
|
|
1318
|
-
if (tempDir && fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
|
|
1319
|
-
console.error('GitHub restore error:', err);
|
|
1320
|
-
res.status(500).json({ error: err.message });
|
|
1321
|
-
}
|
|
1322
|
-
});
|
|
1323
|
-
|
|
1324
|
-
app.post('/api/github/autosync', async (req, res) => {
|
|
1325
|
-
const studio = loadStudioConfig();
|
|
1326
|
-
const enabled = req.body.enabled;
|
|
1327
|
-
studio.githubAutoSync = enabled;
|
|
1328
|
-
saveStudioConfig(studio);
|
|
1329
|
-
res.json({ success: true, enabled });
|
|
1330
|
-
});
|
|
1331
|
-
|
|
1332
|
-
const getSkillDir = () => {
|
|
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
|
+
});
|
|
1348
|
+
|
|
1349
|
+
const getSkillDir = () => {
|
|
1333
1350
|
const cp = getConfigPath();
|
|
1334
1351
|
return cp ? path.join(path.dirname(cp), 'skill') : null;
|
|
1335
1352
|
};
|