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.
Files changed (4) hide show
  1. package/AGENTS.md +0 -1
  2. package/README.md +0 -1
  3. package/index.js +279 -262
  4. 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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-studio-server",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
5
  "main": "index.js",
6
6
  "bin": {