opencode-studio-server 1.25.0 → 1.26.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 (2) hide show
  1. package/index.js +82 -239
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1103,15 +1103,7 @@ async function ensureGitHubRepo(token, repoName) {
1103
1103
  });
1104
1104
 
1105
1105
  if (response.ok) {
1106
- const data = await response.json();
1107
- const branch = data.default_branch || 'main';
1108
- const refRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
1109
- headers: { 'Authorization': `Bearer ${token}` }
1110
- });
1111
- if (refRes.status === 404 || refRes.status === 409) {
1112
- await bootstrapEmptyRepo(token, owner, repo);
1113
- }
1114
- return data;
1106
+ return await response.json();
1115
1107
  }
1116
1108
 
1117
1109
  if (response.status === 404) {
@@ -1130,6 +1122,7 @@ async function ensureGitHubRepo(token, repoName) {
1130
1122
  });
1131
1123
 
1132
1124
  if (createRes.ok) {
1125
+ await new Promise(r => setTimeout(r, 2000));
1133
1126
  return await createRes.json();
1134
1127
  }
1135
1128
 
@@ -1141,183 +1134,34 @@ async function ensureGitHubRepo(token, repoName) {
1141
1134
  throw new Error(`Failed to check repo: ${err}`);
1142
1135
  }
1143
1136
 
1144
- async function bootstrapEmptyRepo(token, owner, repo) {
1145
- const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/README.md`, {
1146
- method: 'PUT',
1147
- headers: {
1148
- 'Authorization': `Bearer ${token}`,
1149
- 'Content-Type': 'application/json'
1150
- },
1151
- body: JSON.stringify({
1152
- message: 'Initial commit',
1153
- content: Buffer.from('# OpenCode Studio Backup\n').toString('base64')
1154
- })
1155
- });
1156
-
1157
- if (!res.ok) {
1158
- const err = await res.text();
1159
- throw new Error(`Failed to bootstrap repo: ${err}`);
1160
- }
1161
- }
1162
-
1163
- function collectBlobs(rootDir, basePath = '', blobs = []) {
1164
- const dir = basePath || rootDir;
1165
- if (!fs.existsSync(dir)) return blobs;
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'];
1166
1141
  const SKIP_EXT = ['.log', '.tmp', '.db', '.sqlite', '.cache', '.pack', '.idx'];
1167
1142
 
1168
- for (const name of fs.readdirSync(dir)) {
1169
- const fullPath = path.join(dir, name);
1170
- const stat = fs.statSync(fullPath);
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);
1171
1147
 
1172
1148
  if (stat.isDirectory()) {
1173
- if (name === 'node_modules' || name === '.git' || name === '.next' || name === 'cache') continue;
1174
- collectBlobs(rootDir, fullPath, blobs);
1149
+ if (SKIP_DIRS.includes(name)) continue;
1150
+ copyDirContents(srcPath, destPath);
1175
1151
  } else {
1176
1152
  if (SKIP_EXT.some(ext => name.endsWith(ext))) continue;
1177
- try {
1178
- const content = fs.readFileSync(fullPath, 'utf8');
1179
- blobs.push({
1180
- path: path.relative(rootDir, fullPath).replace(/\\/g, '/'),
1181
- mode: '100644',
1182
- type: 'blob',
1183
- content
1184
- });
1185
- } catch (e) { }
1153
+ fs.copyFileSync(srcPath, destPath);
1186
1154
  }
1187
1155
  }
1188
- return blobs;
1189
1156
  }
1190
-
1191
- async function createGitHubBlob(token, repoName, blob) {
1192
- const [owner, repo] = repoName.split('/');
1193
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/blobs`, {
1194
- method: 'POST',
1195
- headers: {
1196
- 'Authorization': `Bearer ${token}`,
1197
- 'Content-Type': 'application/json'
1198
- },
1199
- body: JSON.stringify({
1200
- content: Buffer.from(blob.content).toString('base64'),
1201
- encoding: 'base64'
1202
- })
1203
- });
1204
-
1205
- if (!response.ok) {
1206
- const err = await response.text();
1207
- throw new Error(`Failed to create blob: ${err}`);
1208
- }
1209
-
1210
- const data = await response.json();
1211
- return data.sha;
1212
- }
1213
-
1214
- async function createGitHubTree(token, repoName, treeItems, baseSha = null) {
1215
- const [owner, repo] = repoName.split('/');
1216
- const body = {
1217
- tree: treeItems.map(item => ({
1218
- path: item.path,
1219
- mode: item.mode,
1220
- type: item.type,
1221
- sha: item.sha
1222
- }))
1223
- };
1224
- if (baseSha) body.base_tree = baseSha;
1225
-
1226
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
1227
- method: 'POST',
1228
- headers: {
1229
- 'Authorization': `Bearer ${token}`,
1230
- 'Content-Type': 'application/json'
1231
- },
1232
- body: JSON.stringify(body)
1233
- });
1234
-
1235
- if (!response.ok) {
1236
- const err = await response.text();
1237
- throw new Error(`Failed to create tree: ${err}`);
1238
- }
1239
-
1240
- const data = await response.json();
1241
- return data.sha;
1242
- }
1243
-
1244
- async function createGitHubCommit(token, repoName, treeSha, message) {
1245
- const [owner, repo] = repoName.split('/');
1246
-
1247
- const headRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main`, {
1248
- headers: { 'Authorization': `Bearer ${token}` }
1249
- });
1250
-
1251
- let parentSha = null;
1252
- if (headRes.ok) {
1253
- const headData = await headRes.json();
1254
- parentSha = headData.object.sha;
1255
- }
1256
-
1257
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
1258
- method: 'POST',
1259
- headers: {
1260
- 'Authorization': `Bearer ${token}`,
1261
- 'Content-Type': 'application/json'
1262
- },
1263
- body: JSON.stringify({
1264
- message,
1265
- tree: treeSha,
1266
- parents: parentSha ? [parentSha] : []
1267
- })
1268
- });
1269
-
1270
- if (!response.ok) {
1271
- const err = await response.text();
1272
- throw new Error(`Failed to create commit: ${err}`);
1273
- }
1274
-
1275
- const data = await response.json();
1276
- return data.sha;
1277
- }
1278
-
1279
- async function updateGitHubRef(token, repoName, commitSha, branch = 'main') {
1280
- const [owner, repo] = repoName.split('/');
1281
-
1282
- const checkRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
1283
- headers: { 'Authorization': `Bearer ${token}` }
1284
- });
1285
-
1286
- if (checkRes.status === 404) {
1287
- const createRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs`, {
1288
- method: 'POST',
1289
- headers: {
1290
- 'Authorization': `Bearer ${token}`,
1291
- 'Content-Type': 'application/json'
1292
- },
1293
- body: JSON.stringify({
1294
- ref: `refs/heads/${branch}`,
1295
- sha: commitSha
1296
- })
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());
1297
1163
  });
1298
-
1299
- if (!createRes.ok) {
1300
- const err = await createRes.text();
1301
- throw new Error(`Failed to create ref: ${err}`);
1302
- }
1303
- return;
1304
- }
1305
-
1306
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
1307
- method: 'PATCH',
1308
- headers: {
1309
- 'Authorization': `Bearer ${token}`,
1310
- 'Content-Type': 'application/json'
1311
- },
1312
- body: JSON.stringify({
1313
- sha: commitSha
1314
- })
1315
1164
  });
1316
-
1317
- if (!response.ok) {
1318
- const err = await response.text();
1319
- throw new Error(`Failed to update ref: ${err}`);
1320
- }
1321
1165
  }
1322
1166
 
1323
1167
  app.get('/api/github/backup/status', async (req, res) => {
@@ -1351,78 +1195,77 @@ app.get('/api/github/backup/status', async (req, res) => {
1351
1195
  }
1352
1196
  });
1353
1197
 
1354
- app.post('/api/github/backup', async (req, res) => {
1355
- try {
1356
- const token = await getGitHubToken();
1357
- if (!token) return res.status(400).json({ error: 'Not logged in to gh CLI. Run: gh auth login' });
1358
-
1359
- const user = await getGitHubUser(token);
1360
- if (!user) return res.status(400).json({ error: 'Failed to get GitHub user' });
1361
-
1362
- const { owner, repo, branch } = req.body;
1363
- const studio = loadStudioConfig();
1364
-
1365
- const finalOwner = owner || studio.githubBackup?.owner || user.login;
1366
- const finalRepo = repo || studio.githubBackup?.repo;
1367
- const finalBranch = branch || studio.githubBackup?.branch || 'main';
1368
-
1369
- if (!finalRepo) return res.status(400).json({ error: 'No repo name provided' });
1370
-
1371
- const repoName = `${finalOwner}/${finalRepo}`;
1372
-
1373
- await ensureGitHubRepo(token, repoName);
1374
-
1375
- const opencodeConfig = getConfigPath();
1376
- if (!opencodeConfig) return res.status(400).json({ error: 'No opencode config path found' });
1377
-
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
+
1378
1223
  const opencodeDir = path.dirname(opencodeConfig);
1379
1224
  const studioDir = path.join(HOME_DIR, '.config', 'opencode-studio');
1380
1225
 
1381
- const opencodeBlobs = collectBlobs(opencodeDir);
1382
- const studioBlobs = collectBlobs(studioDir);
1383
- const skipped = [];
1226
+ tempDir = path.join(os.tmpdir(), `opencode-backup-${Date.now()}`);
1227
+ fs.mkdirSync(tempDir, { recursive: true });
1384
1228
 
1385
- for (const blob of opencodeBlobs) {
1386
- try {
1387
- blob.sha = await createGitHubBlob(token, repoName, blob);
1388
- blob.path = `opencode/${blob.path}`;
1389
- delete blob.content;
1390
- } catch (e) {
1391
- skipped.push({ path: `opencode/${blob.path}`, reason: e.message, size: Buffer.byteLength(blob.content) });
1392
- blob.skip = true;
1393
- }
1394
- }
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);
1395
1239
 
1396
- for (const blob of studioBlobs) {
1397
- try {
1398
- blob.sha = await createGitHubBlob(token, repoName, blob);
1399
- blob.path = `opencode-studio/${blob.path}`;
1400
- delete blob.content;
1401
- } catch (e) {
1402
- skipped.push({ path: `opencode-studio/${blob.path}`, reason: e.message, size: Buffer.byteLength(blob.content) });
1403
- blob.skip = true;
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}` });
1404
1252
  }
1253
+ throw e;
1405
1254
  }
1406
1255
 
1407
- const allBlobs = [...opencodeBlobs, ...studioBlobs].filter(b => !b.skip);
1408
- const rootTreeSha = await createGitHubTree(token, repoName, allBlobs);
1409
-
1410
- const timestamp = new Date().toISOString();
1411
- const commitMessage = `OpenCode Studio backup ${timestamp}`;
1412
- const commitSha = await createGitHubCommit(token, repoName, rootTreeSha, commitMessage);
1413
-
1414
- await updateGitHubRef(token, repoName, commitSha, finalBranch);
1415
-
1416
- studio.githubBackup = { owner: finalOwner, repo: finalRepo, branch: finalBranch };
1417
- studio.lastGithubBackup = timestamp;
1418
- saveStudioConfig(studio);
1419
-
1420
- res.json({ success: true, timestamp, commit: commitSha, url: `https://github.com/${repoName}`, skipped });
1421
- } catch (err) {
1422
- console.error('GitHub backup error:', err);
1423
- res.status(500).json({ error: err.message });
1424
- }
1425
- });
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
+ });
1426
1269
 
1427
1270
  const getSkillDir = () => {
1428
1271
  const cp = getConfigPath();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-studio-server",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
5
  "main": "index.js",
6
6
  "bin": {