opencode-studio-server 1.4.1 → 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.
Files changed (2) hide show
  1. package/index.js +289 -7
  2. 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
- const listAuthProfiles = (p, activePlugin) => {
766
- let ns = p;
767
- if (p === 'google') {
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
- const d = path.join(AUTH_PROFILES_DIR, ns);
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 = path.join(AUTH_PROFILES_DIR, namespace);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-studio-server",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
5
  "main": "index.js",
6
6
  "bin": {