opencode-studio-server 1.5.0 → 1.6.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 +289 -7
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -519,6 +519,278 @@ app.post('/api/config', (req, res) => {
|
|
|
519
519
|
}
|
|
520
520
|
});
|
|
521
521
|
|
|
522
|
+
app.get('/api/backup', (req, res) => {
|
|
523
|
+
try {
|
|
524
|
+
const studioConfig = loadStudioConfig();
|
|
525
|
+
const opencodeConfig = loadConfig();
|
|
526
|
+
const skills = [];
|
|
527
|
+
const plugins = [];
|
|
528
|
+
|
|
529
|
+
const sd = getSkillDir();
|
|
530
|
+
if (sd && fs.existsSync(sd)) {
|
|
531
|
+
fs.readdirSync(sd, { withFileTypes: true })
|
|
532
|
+
.filter(e => e.isDirectory() && fs.existsSync(path.join(sd, e.name, 'SKILL.md')))
|
|
533
|
+
.forEach(e => {
|
|
534
|
+
const content = fs.readFileSync(path.join(sd, e.name, 'SKILL.md'), 'utf8');
|
|
535
|
+
skills.push({ name: e.name, content });
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const pd = getPluginDir();
|
|
540
|
+
if (pd && fs.existsSync(pd)) {
|
|
541
|
+
fs.readdirSync(pd, { withFileTypes: true }).forEach(e => {
|
|
542
|
+
const fp = path.join(pd, e.name);
|
|
543
|
+
if (e.isFile() && /\.(js|ts)$/.test(e.name)) {
|
|
544
|
+
plugins.push({ name: e.name.replace(/\.(js|ts)$/, ''), content: fs.readFileSync(fp, 'utf8') });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
res.json({
|
|
550
|
+
version: 1,
|
|
551
|
+
timestamp: new Date().toISOString(),
|
|
552
|
+
studioConfig,
|
|
553
|
+
opencodeConfig,
|
|
554
|
+
skills,
|
|
555
|
+
plugins
|
|
556
|
+
});
|
|
557
|
+
} catch (err) {
|
|
558
|
+
res.status(500).json({ error: err.message });
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
app.post('/api/restore', (req, res) => {
|
|
563
|
+
try {
|
|
564
|
+
const { studioConfig, opencodeConfig, skills, plugins } = req.body;
|
|
565
|
+
|
|
566
|
+
if (studioConfig) saveStudioConfig(studioConfig);
|
|
567
|
+
if (opencodeConfig) saveConfig(opencodeConfig);
|
|
568
|
+
|
|
569
|
+
const sd = getSkillDir();
|
|
570
|
+
if (sd && skills && Array.isArray(skills)) {
|
|
571
|
+
if (!fs.existsSync(sd)) fs.mkdirSync(sd, { recursive: true });
|
|
572
|
+
skills.forEach(s => {
|
|
573
|
+
const skillDir = path.join(sd, s.name);
|
|
574
|
+
if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
|
|
575
|
+
atomicWriteFileSync(path.join(skillDir, 'SKILL.md'), s.content);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const pd = getPluginDir();
|
|
580
|
+
if (pd && plugins && Array.isArray(plugins)) {
|
|
581
|
+
if (!fs.existsSync(pd)) fs.mkdirSync(pd, { recursive: true });
|
|
582
|
+
plugins.forEach(p => {
|
|
583
|
+
atomicWriteFileSync(path.join(pd, `${p.name}.js`), p.content);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
res.json({ success: true });
|
|
588
|
+
} catch (err) {
|
|
589
|
+
res.status(500).json({ error: err.message });
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
app.post('/api/sync/push', (req, res) => {
|
|
594
|
+
try {
|
|
595
|
+
const studio = loadStudioConfig();
|
|
596
|
+
const syncFolder = studio.syncFolder;
|
|
597
|
+
if (!syncFolder) return res.status(400).json({ error: 'Sync folder not configured' });
|
|
598
|
+
if (!fs.existsSync(syncFolder)) return res.status(400).json({ error: 'Sync folder does not exist' });
|
|
599
|
+
|
|
600
|
+
const opencodeConfig = loadConfig();
|
|
601
|
+
const skills = [];
|
|
602
|
+
const plugins = [];
|
|
603
|
+
|
|
604
|
+
const sd = getSkillDir();
|
|
605
|
+
if (sd && fs.existsSync(sd)) {
|
|
606
|
+
fs.readdirSync(sd, { withFileTypes: true })
|
|
607
|
+
.filter(e => e.isDirectory() && fs.existsSync(path.join(sd, e.name, 'SKILL.md')))
|
|
608
|
+
.forEach(e => {
|
|
609
|
+
skills.push({ name: e.name, content: fs.readFileSync(path.join(sd, e.name, 'SKILL.md'), 'utf8') });
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const pd = getPluginDir();
|
|
614
|
+
if (pd && fs.existsSync(pd)) {
|
|
615
|
+
fs.readdirSync(pd, { withFileTypes: true }).forEach(e => {
|
|
616
|
+
if (e.isFile() && /\.(js|ts)$/.test(e.name)) {
|
|
617
|
+
plugins.push({ name: e.name.replace(/\.(js|ts)$/, ''), content: fs.readFileSync(path.join(pd, e.name), 'utf8') });
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const backup = {
|
|
623
|
+
version: 1,
|
|
624
|
+
timestamp: new Date().toISOString(),
|
|
625
|
+
studioConfig: { ...studio, syncFolder: undefined },
|
|
626
|
+
opencodeConfig,
|
|
627
|
+
skills,
|
|
628
|
+
plugins
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const backupPath = path.join(syncFolder, 'opencode-studio-sync.json');
|
|
632
|
+
atomicWriteFileSync(backupPath, JSON.stringify(backup, null, 2));
|
|
633
|
+
|
|
634
|
+
studio.lastSyncAt = backup.timestamp;
|
|
635
|
+
saveStudioConfig(studio);
|
|
636
|
+
|
|
637
|
+
res.json({ success: true, path: backupPath, timestamp: backup.timestamp });
|
|
638
|
+
} catch (err) {
|
|
639
|
+
res.status(500).json({ error: err.message });
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
app.post('/api/sync/pull', (req, res) => {
|
|
644
|
+
try {
|
|
645
|
+
const studio = loadStudioConfig();
|
|
646
|
+
const syncFolder = studio.syncFolder;
|
|
647
|
+
if (!syncFolder) return res.status(400).json({ error: 'Sync folder not configured' });
|
|
648
|
+
|
|
649
|
+
const backupPath = path.join(syncFolder, 'opencode-studio-sync.json');
|
|
650
|
+
if (!fs.existsSync(backupPath)) return res.status(404).json({ error: 'No sync file found in folder' });
|
|
651
|
+
|
|
652
|
+
const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
|
|
653
|
+
|
|
654
|
+
if (backup.studioConfig) {
|
|
655
|
+
const merged = { ...backup.studioConfig, syncFolder: studio.syncFolder };
|
|
656
|
+
saveStudioConfig(merged);
|
|
657
|
+
}
|
|
658
|
+
if (backup.opencodeConfig) saveConfig(backup.opencodeConfig);
|
|
659
|
+
|
|
660
|
+
const sd = getSkillDir();
|
|
661
|
+
if (sd && backup.skills && Array.isArray(backup.skills)) {
|
|
662
|
+
if (!fs.existsSync(sd)) fs.mkdirSync(sd, { recursive: true });
|
|
663
|
+
backup.skills.forEach(s => {
|
|
664
|
+
const skillDir = path.join(sd, s.name);
|
|
665
|
+
if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
|
|
666
|
+
atomicWriteFileSync(path.join(skillDir, 'SKILL.md'), s.content);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const pd = getPluginDir();
|
|
671
|
+
if (pd && backup.plugins && Array.isArray(backup.plugins)) {
|
|
672
|
+
if (!fs.existsSync(pd)) fs.mkdirSync(pd, { recursive: true });
|
|
673
|
+
backup.plugins.forEach(p => {
|
|
674
|
+
atomicWriteFileSync(path.join(pd, `${p.name}.js`), p.content);
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const updated = loadStudioConfig();
|
|
679
|
+
updated.lastSyncAt = new Date().toISOString();
|
|
680
|
+
saveStudioConfig(updated);
|
|
681
|
+
|
|
682
|
+
res.json({ success: true, timestamp: backup.timestamp, skills: (backup.skills || []).length, plugins: (backup.plugins || []).length });
|
|
683
|
+
} catch (err) {
|
|
684
|
+
res.status(500).json({ error: err.message });
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
app.get('/api/sync/status', (req, res) => {
|
|
689
|
+
const studio = loadStudioConfig();
|
|
690
|
+
const syncFolder = studio.syncFolder;
|
|
691
|
+
let fileExists = false;
|
|
692
|
+
let fileTimestamp = null;
|
|
693
|
+
|
|
694
|
+
if (syncFolder) {
|
|
695
|
+
const backupPath = path.join(syncFolder, 'opencode-studio-sync.json');
|
|
696
|
+
if (fs.existsSync(backupPath)) {
|
|
697
|
+
fileExists = true;
|
|
698
|
+
try {
|
|
699
|
+
const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
|
|
700
|
+
fileTimestamp = backup.timestamp;
|
|
701
|
+
} catch {}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
res.json({
|
|
706
|
+
configured: !!syncFolder,
|
|
707
|
+
folder: syncFolder || null,
|
|
708
|
+
lastSync: studio.lastSyncAt || null,
|
|
709
|
+
autoSync: !!studio.autoSync,
|
|
710
|
+
fileExists,
|
|
711
|
+
fileTimestamp
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
app.post('/api/sync/config', (req, res) => {
|
|
716
|
+
const { folder, autoSync } = req.body;
|
|
717
|
+
const studio = loadStudioConfig();
|
|
718
|
+
|
|
719
|
+
if (folder !== undefined) {
|
|
720
|
+
if (folder) {
|
|
721
|
+
if (!fs.existsSync(folder)) {
|
|
722
|
+
return res.status(400).json({ error: 'Folder does not exist' });
|
|
723
|
+
}
|
|
724
|
+
studio.syncFolder = folder;
|
|
725
|
+
} else {
|
|
726
|
+
delete studio.syncFolder;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (autoSync !== undefined) {
|
|
731
|
+
studio.autoSync = !!autoSync;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
saveStudioConfig(studio);
|
|
735
|
+
res.json({ success: true, folder: studio.syncFolder || null, autoSync: !!studio.autoSync });
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
app.post('/api/sync/auto', (req, res) => {
|
|
739
|
+
const studio = loadStudioConfig();
|
|
740
|
+
if (!studio.syncFolder || !studio.autoSync) {
|
|
741
|
+
return res.json({ action: 'none', reason: 'auto-sync not configured' });
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const syncFolder = studio.syncFolder;
|
|
745
|
+
const backupPath = path.join(syncFolder, 'opencode-studio-sync.json');
|
|
746
|
+
|
|
747
|
+
if (!fs.existsSync(backupPath)) {
|
|
748
|
+
return res.json({ action: 'none', reason: 'no remote file' });
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
try {
|
|
752
|
+
const remote = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
|
|
753
|
+
const remoteTime = new Date(remote.timestamp).getTime();
|
|
754
|
+
const localTime = studio.lastSyncAt ? new Date(studio.lastSyncAt).getTime() : 0;
|
|
755
|
+
|
|
756
|
+
if (remoteTime > localTime) {
|
|
757
|
+
if (remote.studioConfig) {
|
|
758
|
+
const merged = { ...remote.studioConfig, syncFolder: studio.syncFolder, autoSync: studio.autoSync, lastSyncAt: studio.lastSyncAt };
|
|
759
|
+
saveStudioConfig(merged);
|
|
760
|
+
}
|
|
761
|
+
if (remote.opencodeConfig) saveConfig(remote.opencodeConfig);
|
|
762
|
+
|
|
763
|
+
const sd = getSkillDir();
|
|
764
|
+
if (sd && remote.skills && Array.isArray(remote.skills)) {
|
|
765
|
+
if (!fs.existsSync(sd)) fs.mkdirSync(sd, { recursive: true });
|
|
766
|
+
remote.skills.forEach(s => {
|
|
767
|
+
const skillDir = path.join(sd, s.name);
|
|
768
|
+
if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
|
|
769
|
+
atomicWriteFileSync(path.join(skillDir, 'SKILL.md'), s.content);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const pd = getPluginDir();
|
|
774
|
+
if (pd && remote.plugins && Array.isArray(remote.plugins)) {
|
|
775
|
+
if (!fs.existsSync(pd)) fs.mkdirSync(pd, { recursive: true });
|
|
776
|
+
remote.plugins.forEach(p => {
|
|
777
|
+
atomicWriteFileSync(path.join(pd, `${p.name}.js`), p.content);
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const updated = loadStudioConfig();
|
|
782
|
+
updated.lastSyncAt = new Date().toISOString();
|
|
783
|
+
saveStudioConfig(updated);
|
|
784
|
+
|
|
785
|
+
return res.json({ action: 'pulled', timestamp: remote.timestamp });
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
res.json({ action: 'none', reason: 'local is current' });
|
|
789
|
+
} catch (err) {
|
|
790
|
+
res.status(500).json({ error: err.message });
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
522
794
|
const getSkillDir = () => {
|
|
523
795
|
const cp = getConfigPath();
|
|
524
796
|
return cp ? path.join(path.dirname(cp), 'skill') : null;
|
|
@@ -762,13 +1034,22 @@ function loadAuthConfig() {
|
|
|
762
1034
|
}
|
|
763
1035
|
|
|
764
1036
|
const AUTH_PROFILES_DIR = path.join(HOME_DIR, '.config', 'opencode-studio', 'auth-profiles');
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1037
|
+
|
|
1038
|
+
const getProfileDir = (provider, activePlugin) => {
|
|
1039
|
+
let ns = provider;
|
|
1040
|
+
if (provider === 'google') {
|
|
768
1041
|
ns = activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini';
|
|
1042
|
+
const nsDir = path.join(AUTH_PROFILES_DIR, ns);
|
|
1043
|
+
const plainDir = path.join(AUTH_PROFILES_DIR, 'google');
|
|
1044
|
+
const nsHas = fs.existsSync(nsDir) && fs.readdirSync(nsDir).filter(f => f.endsWith('.json')).length > 0;
|
|
1045
|
+
const plainHas = fs.existsSync(plainDir) && fs.readdirSync(plainDir).filter(f => f.endsWith('.json')).length > 0;
|
|
1046
|
+
if (!nsHas && plainHas) return plainDir;
|
|
769
1047
|
}
|
|
770
|
-
|
|
771
|
-
|
|
1048
|
+
return path.join(AUTH_PROFILES_DIR, ns);
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
const listAuthProfiles = (p, activePlugin) => {
|
|
1052
|
+
const d = getProfileDir(p, activePlugin);
|
|
772
1053
|
if (!fs.existsSync(d)) return [];
|
|
773
1054
|
try { return fs.readdirSync(d).filter(f => f.endsWith('.json')).map(f => f.replace('.json', '')); } catch { return []; }
|
|
774
1055
|
};
|
|
@@ -1327,11 +1608,12 @@ function buildAccountPool(provider) {
|
|
|
1327
1608
|
? (activePlugin === 'antigravity' ? 'google.antigravity' : 'google.gemini')
|
|
1328
1609
|
: provider;
|
|
1329
1610
|
|
|
1330
|
-
const profileDir =
|
|
1611
|
+
const profileDir = getProfileDir(provider, activePlugin);
|
|
1612
|
+
|
|
1331
1613
|
const profiles = [];
|
|
1332
1614
|
const now = Date.now();
|
|
1333
1615
|
const metadata = loadPoolMetadata();
|
|
1334
|
-
const providerMeta = metadata[namespace] || {};
|
|
1616
|
+
const providerMeta = metadata[namespace] || metadata[provider] || {};
|
|
1335
1617
|
|
|
1336
1618
|
// Get current active profile from studio config
|
|
1337
1619
|
const studio = loadStudioConfig();
|