opencode-studio-server 1.3.8 → 1.4.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/index.js +132 -8
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -7,6 +7,9 @@ const os = require('os');
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const { spawn, exec } = require('child_process');
|
|
9
9
|
|
|
10
|
+
const pkg = require('./package.json');
|
|
11
|
+
const SERVER_VERSION = pkg.version;
|
|
12
|
+
|
|
10
13
|
// Atomic file write: write to temp file then rename to prevent corruption
|
|
11
14
|
const atomicWriteFileSync = (filePath, data, options = 'utf8') => {
|
|
12
15
|
const dir = path.dirname(filePath);
|
|
@@ -221,6 +224,7 @@ function processLogLine(line) {
|
|
|
221
224
|
|
|
222
225
|
// Start watcher on server start
|
|
223
226
|
setupLogWatcher();
|
|
227
|
+
importExistingAuth();
|
|
224
228
|
|
|
225
229
|
let pendingActionMemory = null;
|
|
226
230
|
|
|
@@ -435,7 +439,7 @@ const saveConfig = (config) => {
|
|
|
435
439
|
atomicWriteFileSync(configPath, JSON.stringify(config, null, 2));
|
|
436
440
|
};
|
|
437
441
|
|
|
438
|
-
app.get('/api/health', (req, res) => res.json({ status: 'ok' }));
|
|
442
|
+
app.get('/api/health', (req, res) => res.json({ status: 'ok', version: SERVER_VERSION }));
|
|
439
443
|
|
|
440
444
|
app.post('/api/shutdown', (req, res) => {
|
|
441
445
|
res.json({ success: true });
|
|
@@ -444,6 +448,56 @@ app.post('/api/shutdown', (req, res) => {
|
|
|
444
448
|
|
|
445
449
|
app.get('/api/paths', (req, res) => res.json(getPaths()));
|
|
446
450
|
|
|
451
|
+
app.get('/api/debug/auth', (req, res) => {
|
|
452
|
+
const paths = getPaths();
|
|
453
|
+
const studio = loadStudioConfig();
|
|
454
|
+
const activePlugin = studio.activeGooglePlugin;
|
|
455
|
+
|
|
456
|
+
// Check auth.json in all candidate locations
|
|
457
|
+
const authLocations = [];
|
|
458
|
+
paths.candidates.forEach(p => {
|
|
459
|
+
const ap = path.join(path.dirname(p), 'auth.json');
|
|
460
|
+
authLocations.push({
|
|
461
|
+
path: ap,
|
|
462
|
+
exists: fs.existsSync(ap),
|
|
463
|
+
keys: fs.existsSync(ap) ? Object.keys(JSON.parse(fs.readFileSync(ap, 'utf8'))) : []
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Check current auth.json
|
|
468
|
+
if (paths.current) {
|
|
469
|
+
const ap = path.join(path.dirname(paths.current), 'auth.json');
|
|
470
|
+
if (!authLocations.some(l => l.path === ap)) {
|
|
471
|
+
authLocations.push({
|
|
472
|
+
path: ap,
|
|
473
|
+
exists: fs.existsSync(ap),
|
|
474
|
+
keys: fs.existsSync(ap) ? Object.keys(JSON.parse(fs.readFileSync(ap, 'utf8'))) : []
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Check profile directories
|
|
480
|
+
const namespaces = ['google', 'google.gemini', 'google.antigravity', 'openai', 'anthropic'];
|
|
481
|
+
const profileDirs = {};
|
|
482
|
+
namespaces.forEach(ns => {
|
|
483
|
+
const dir = path.join(AUTH_PROFILES_DIR, ns);
|
|
484
|
+
profileDirs[ns] = {
|
|
485
|
+
path: dir,
|
|
486
|
+
exists: fs.existsSync(dir),
|
|
487
|
+
profiles: fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => f.endsWith('.json')) : []
|
|
488
|
+
};
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
res.json({
|
|
492
|
+
configPath: paths.current,
|
|
493
|
+
activeGooglePlugin: activePlugin,
|
|
494
|
+
activeProfiles: studio.activeProfiles || {},
|
|
495
|
+
authLocations,
|
|
496
|
+
profileDirs,
|
|
497
|
+
authProfilesDir: AUTH_PROFILES_DIR
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
447
501
|
app.post('/api/paths', (req, res) => {
|
|
448
502
|
const { configPath } = req.body;
|
|
449
503
|
const studioConfig = loadStudioConfig();
|
|
@@ -475,9 +529,11 @@ const getSkillDir = () => {
|
|
|
475
529
|
app.get('/api/skills', (req, res) => {
|
|
476
530
|
const sd = getSkillDir();
|
|
477
531
|
if (!sd || !fs.existsSync(sd)) return res.json([]);
|
|
532
|
+
const studio = loadStudioConfig();
|
|
533
|
+
const disabledSkills = studio.disabledSkills || [];
|
|
478
534
|
const skills = fs.readdirSync(sd, { withFileTypes: true })
|
|
479
535
|
.filter(e => e.isDirectory() && fs.existsSync(path.join(sd, e.name, 'SKILL.md')))
|
|
480
|
-
.map(e => ({ name: e.name, path: path.join(sd, e.name, 'SKILL.md'), enabled: !e.name
|
|
536
|
+
.map(e => ({ name: e.name, path: path.join(sd, e.name, 'SKILL.md'), enabled: !disabledSkills.includes(e.name) }));
|
|
481
537
|
res.json(skills);
|
|
482
538
|
});
|
|
483
539
|
|
|
@@ -537,9 +593,13 @@ app.get('/api/plugins', (req, res) => {
|
|
|
537
593
|
const pd = getPluginDir();
|
|
538
594
|
const cp = getConfigPath();
|
|
539
595
|
const cr = cp ? path.dirname(cp) : null;
|
|
596
|
+
const studio = loadStudioConfig();
|
|
597
|
+
const disabledPlugins = studio.disabledPlugins || [];
|
|
540
598
|
const plugins = [];
|
|
541
|
-
const add = (name, p,
|
|
542
|
-
if (!plugins.some(pl => pl.name === name))
|
|
599
|
+
const add = (name, p, type = 'file') => {
|
|
600
|
+
if (!plugins.some(pl => pl.name === name)) {
|
|
601
|
+
plugins.push({ name, path: p, type, enabled: !disabledPlugins.includes(name) });
|
|
602
|
+
}
|
|
543
603
|
};
|
|
544
604
|
|
|
545
605
|
if (pd && fs.existsSync(pd)) {
|
|
@@ -548,9 +608,9 @@ app.get('/api/plugins', (req, res) => {
|
|
|
548
608
|
const st = fs.lstatSync(fp);
|
|
549
609
|
if (st.isDirectory()) {
|
|
550
610
|
const j = path.join(fp, 'index.js'), t = path.join(fp, 'index.ts');
|
|
551
|
-
if (fs.existsSync(j) || fs.existsSync(t)) add(e.name, fs.existsSync(j) ? j : t);
|
|
611
|
+
if (fs.existsSync(j) || fs.existsSync(t)) add(e.name, fs.existsSync(j) ? j : t, 'file');
|
|
552
612
|
} else if ((st.isFile() || st.isSymbolicLink()) && /\.(js|ts)$/.test(e.name)) {
|
|
553
|
-
add(e.name.replace(/\.(js|ts)$/, ''), fp);
|
|
613
|
+
add(e.name.replace(/\.(js|ts)$/, ''), fp, 'file');
|
|
554
614
|
}
|
|
555
615
|
});
|
|
556
616
|
}
|
|
@@ -558,14 +618,14 @@ app.get('/api/plugins', (req, res) => {
|
|
|
558
618
|
if (cr && fs.existsSync(cr)) {
|
|
559
619
|
['oh-my-opencode', 'superpowers', 'opencode-gemini-auth'].forEach(n => {
|
|
560
620
|
const fp = path.join(cr, n);
|
|
561
|
-
if (fs.existsSync(fp) && fs.statSync(fp).isDirectory()) add(n, fp);
|
|
621
|
+
if (fs.existsSync(fp) && fs.statSync(fp).isDirectory()) add(n, fp, 'file');
|
|
562
622
|
});
|
|
563
623
|
}
|
|
564
624
|
|
|
565
625
|
const cfg = loadConfig();
|
|
566
626
|
if (cfg && Array.isArray(cfg.plugin)) {
|
|
567
627
|
cfg.plugin.forEach(n => {
|
|
568
|
-
if (!n.includes('/') && !n.includes('\\') && !/\.(js|ts)$/.test(n)) add(n, 'npm');
|
|
628
|
+
if (!n.includes('/') && !n.includes('\\') && !/\.(js|ts)$/.test(n)) add(n, 'npm', 'npm');
|
|
569
629
|
});
|
|
570
630
|
}
|
|
571
631
|
res.json(plugins);
|
|
@@ -1192,6 +1252,70 @@ function syncAntigravityPool() {
|
|
|
1192
1252
|
savePoolMetadata(metadata);
|
|
1193
1253
|
}
|
|
1194
1254
|
|
|
1255
|
+
function importExistingAuth() {
|
|
1256
|
+
const authCfg = loadAuthConfig();
|
|
1257
|
+
if (!authCfg) return;
|
|
1258
|
+
|
|
1259
|
+
const studio = loadStudioConfig();
|
|
1260
|
+
const activeProfiles = studio.activeProfiles || {};
|
|
1261
|
+
let changed = false;
|
|
1262
|
+
|
|
1263
|
+
// Standard providers to check
|
|
1264
|
+
const providers = ['google', 'openai', 'anthropic', 'xai', 'openrouter', 'github-copilot', 'mistral', 'deepseek', 'amazon-bedrock', 'azure'];
|
|
1265
|
+
|
|
1266
|
+
providers.forEach(provider => {
|
|
1267
|
+
if (!authCfg[provider]) return; // No creds for this provider
|
|
1268
|
+
|
|
1269
|
+
// Determine namespace
|
|
1270
|
+
// For auto-import, we target the standard 'google' namespace unless antigravity plugin is active?
|
|
1271
|
+
// Actually, auth.json 'google' key usually means Gemini/Vertex standard auth.
|
|
1272
|
+
const namespace = provider === 'google' && studio.activeGooglePlugin === 'antigravity'
|
|
1273
|
+
? 'google.antigravity'
|
|
1274
|
+
: (provider === 'google' ? 'google.gemini' : provider);
|
|
1275
|
+
|
|
1276
|
+
const profileDir = path.join(AUTH_PROFILES_DIR, namespace);
|
|
1277
|
+
|
|
1278
|
+
// If we already have an active profile for this provider, skip import
|
|
1279
|
+
if (activeProfiles[provider]) return;
|
|
1280
|
+
|
|
1281
|
+
// If directory exists and has files, check if empty
|
|
1282
|
+
if (fs.existsSync(profileDir) && fs.readdirSync(profileDir).filter(f => f.endsWith('.json')).length > 0) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// Import!
|
|
1287
|
+
if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
|
|
1288
|
+
|
|
1289
|
+
const email = authCfg[provider].email || null;
|
|
1290
|
+
const name = email || `imported-${Date.now()}`;
|
|
1291
|
+
const profilePath = path.join(profileDir, `${name}.json`);
|
|
1292
|
+
|
|
1293
|
+
console.log(`[AutoImport] Importing existing ${provider} credentials as ${name}`);
|
|
1294
|
+
atomicWriteFileSync(profilePath, JSON.stringify(authCfg[provider], null, 2));
|
|
1295
|
+
|
|
1296
|
+
// Set as active
|
|
1297
|
+
if (!studio.activeProfiles) studio.activeProfiles = {};
|
|
1298
|
+
studio.activeProfiles[provider] = name;
|
|
1299
|
+
changed = true;
|
|
1300
|
+
|
|
1301
|
+
// Update metadata
|
|
1302
|
+
const metadata = loadPoolMetadata();
|
|
1303
|
+
if (!metadata[namespace]) metadata[namespace] = {};
|
|
1304
|
+
metadata[namespace][name] = {
|
|
1305
|
+
email: email,
|
|
1306
|
+
createdAt: Date.now(),
|
|
1307
|
+
lastUsed: Date.now(),
|
|
1308
|
+
usageCount: 0,
|
|
1309
|
+
imported: true
|
|
1310
|
+
};
|
|
1311
|
+
savePoolMetadata(metadata);
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
if (changed) {
|
|
1315
|
+
saveStudioConfig(studio);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1195
1319
|
function getAccountStatus(meta, now) {
|
|
1196
1320
|
if (!meta) return 'ready';
|
|
1197
1321
|
if (meta.cooldownUntil && meta.cooldownUntil > now) return 'cooldown';
|