opencode-studio-server 1.6.0 → 1.8.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 +365 -140
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -590,91 +590,373 @@ app.post('/api/restore', (req, res) => {
|
|
|
590
590
|
}
|
|
591
591
|
});
|
|
592
592
|
|
|
593
|
-
|
|
593
|
+
const DROPBOX_CLIENT_ID = 'your-dropbox-app-key';
|
|
594
|
+
const GDRIVE_CLIENT_ID = 'your-google-client-id';
|
|
595
|
+
|
|
596
|
+
function buildBackupData() {
|
|
597
|
+
const studio = loadStudioConfig();
|
|
598
|
+
const opencodeConfig = loadConfig();
|
|
599
|
+
const skills = [];
|
|
600
|
+
const plugins = [];
|
|
601
|
+
|
|
602
|
+
const sd = getSkillDir();
|
|
603
|
+
if (sd && fs.existsSync(sd)) {
|
|
604
|
+
fs.readdirSync(sd, { withFileTypes: true })
|
|
605
|
+
.filter(e => e.isDirectory() && fs.existsSync(path.join(sd, e.name, 'SKILL.md')))
|
|
606
|
+
.forEach(e => {
|
|
607
|
+
skills.push({ name: e.name, content: fs.readFileSync(path.join(sd, e.name, 'SKILL.md'), 'utf8') });
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const pd = getPluginDir();
|
|
612
|
+
if (pd && fs.existsSync(pd)) {
|
|
613
|
+
fs.readdirSync(pd, { withFileTypes: true }).forEach(e => {
|
|
614
|
+
if (e.isFile() && /\.(js|ts)$/.test(e.name)) {
|
|
615
|
+
plugins.push({ name: e.name.replace(/\.(js|ts)$/, ''), content: fs.readFileSync(path.join(pd, e.name), 'utf8') });
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const cloudSettings = studio.cloudProvider ? { provider: studio.cloudProvider } : {};
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
version: 1,
|
|
624
|
+
timestamp: new Date().toISOString(),
|
|
625
|
+
studioConfig: { ...studio, cloudToken: undefined, cloudProvider: undefined },
|
|
626
|
+
opencodeConfig,
|
|
627
|
+
skills,
|
|
628
|
+
plugins
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function restoreFromBackup(backup, studio) {
|
|
633
|
+
if (backup.studioConfig) {
|
|
634
|
+
const merged = {
|
|
635
|
+
...backup.studioConfig,
|
|
636
|
+
cloudProvider: studio.cloudProvider,
|
|
637
|
+
cloudToken: studio.cloudToken,
|
|
638
|
+
autoSync: studio.autoSync,
|
|
639
|
+
lastSyncAt: studio.lastSyncAt
|
|
640
|
+
};
|
|
641
|
+
saveStudioConfig(merged);
|
|
642
|
+
}
|
|
643
|
+
if (backup.opencodeConfig) saveConfig(backup.opencodeConfig);
|
|
644
|
+
|
|
645
|
+
const sd = getSkillDir();
|
|
646
|
+
if (sd && backup.skills && Array.isArray(backup.skills)) {
|
|
647
|
+
if (!fs.existsSync(sd)) fs.mkdirSync(sd, { recursive: true });
|
|
648
|
+
backup.skills.forEach(s => {
|
|
649
|
+
const skillDir = path.join(sd, s.name);
|
|
650
|
+
if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
|
|
651
|
+
atomicWriteFileSync(path.join(skillDir, 'SKILL.md'), s.content);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const pd = getPluginDir();
|
|
656
|
+
if (pd && backup.plugins && Array.isArray(backup.plugins)) {
|
|
657
|
+
if (!fs.existsSync(pd)) fs.mkdirSync(pd, { recursive: true });
|
|
658
|
+
backup.plugins.forEach(p => {
|
|
659
|
+
atomicWriteFileSync(path.join(pd, `${p.name}.js`), p.content);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
app.get('/api/sync/status', (req, res) => {
|
|
665
|
+
const studio = loadStudioConfig();
|
|
666
|
+
res.json({
|
|
667
|
+
provider: studio.cloudProvider || null,
|
|
668
|
+
connected: !!(studio.cloudProvider && studio.cloudToken),
|
|
669
|
+
lastSync: studio.lastSyncAt || null,
|
|
670
|
+
autoSync: !!studio.autoSync
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
app.post('/api/sync/config', (req, res) => {
|
|
675
|
+
const { autoSync } = req.body;
|
|
676
|
+
const studio = loadStudioConfig();
|
|
677
|
+
if (autoSync !== undefined) studio.autoSync = !!autoSync;
|
|
678
|
+
saveStudioConfig(studio);
|
|
679
|
+
res.json({ success: true, autoSync: !!studio.autoSync });
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
app.post('/api/sync/disconnect', (req, res) => {
|
|
683
|
+
const studio = loadStudioConfig();
|
|
684
|
+
delete studio.cloudProvider;
|
|
685
|
+
delete studio.cloudToken;
|
|
686
|
+
delete studio.cloudRefreshToken;
|
|
687
|
+
saveStudioConfig(studio);
|
|
688
|
+
res.json({ success: true });
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
app.get('/api/sync/dropbox/auth-url', (req, res) => {
|
|
692
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
693
|
+
const studio = loadStudioConfig();
|
|
694
|
+
studio.oauthState = state;
|
|
695
|
+
saveStudioConfig(studio);
|
|
696
|
+
|
|
697
|
+
const redirectUri = req.query.redirect_uri || 'http://localhost:3000/settings';
|
|
698
|
+
studio.oauthRedirectUri = redirectUri;
|
|
699
|
+
saveStudioConfig(studio);
|
|
700
|
+
|
|
701
|
+
const params = new URLSearchParams({
|
|
702
|
+
client_id: DROPBOX_CLIENT_ID,
|
|
703
|
+
response_type: 'code',
|
|
704
|
+
token_access_type: 'offline',
|
|
705
|
+
redirect_uri: redirectUri,
|
|
706
|
+
state: state
|
|
707
|
+
});
|
|
708
|
+
res.json({ url: `https://www.dropbox.com/oauth2/authorize?${params}` });
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
app.post('/api/sync/dropbox/callback', async (req, res) => {
|
|
594
712
|
try {
|
|
713
|
+
const { code, state } = req.body;
|
|
595
714
|
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
715
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
716
|
+
if (state !== studio.oauthState) {
|
|
717
|
+
return res.status(400).json({ error: 'Invalid state' });
|
|
718
|
+
}
|
|
719
|
+
const redirectUri = studio.oauthRedirectUri || 'http://localhost:3000/settings';
|
|
720
|
+
delete studio.oauthState;
|
|
721
|
+
delete studio.oauthRedirectUri;
|
|
603
722
|
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
723
|
+
const response = await fetch('https://api.dropboxapi.com/oauth2/token', {
|
|
724
|
+
method: 'POST',
|
|
725
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
726
|
+
body: new URLSearchParams({
|
|
727
|
+
code,
|
|
728
|
+
grant_type: 'authorization_code',
|
|
729
|
+
client_id: DROPBOX_CLIENT_ID,
|
|
730
|
+
redirect_uri: redirectUri
|
|
731
|
+
})
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
if (!response.ok) {
|
|
735
|
+
const err = await response.text();
|
|
736
|
+
return res.status(400).json({ error: `Dropbox auth failed: ${err}` });
|
|
611
737
|
}
|
|
612
738
|
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
739
|
+
const tokens = await response.json();
|
|
740
|
+
studio.cloudProvider = 'dropbox';
|
|
741
|
+
studio.cloudToken = tokens.access_token;
|
|
742
|
+
if (tokens.refresh_token) studio.cloudRefreshToken = tokens.refresh_token;
|
|
743
|
+
saveStudioConfig(studio);
|
|
744
|
+
|
|
745
|
+
res.json({ success: true, provider: 'dropbox' });
|
|
746
|
+
} catch (err) {
|
|
747
|
+
res.status(500).json({ error: err.message });
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
app.get('/api/sync/gdrive/auth-url', (req, res) => {
|
|
752
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
753
|
+
const studio = loadStudioConfig();
|
|
754
|
+
studio.oauthState = state;
|
|
755
|
+
|
|
756
|
+
const redirectUri = req.query.redirect_uri || 'http://localhost:3000/settings';
|
|
757
|
+
studio.oauthRedirectUri = redirectUri;
|
|
758
|
+
saveStudioConfig(studio);
|
|
759
|
+
|
|
760
|
+
const params = new URLSearchParams({
|
|
761
|
+
client_id: GDRIVE_CLIENT_ID,
|
|
762
|
+
response_type: 'code',
|
|
763
|
+
scope: 'https://www.googleapis.com/auth/drive.file',
|
|
764
|
+
access_type: 'offline',
|
|
765
|
+
prompt: 'consent',
|
|
766
|
+
redirect_uri: redirectUri,
|
|
767
|
+
state: state
|
|
768
|
+
});
|
|
769
|
+
res.json({ url: `https://accounts.google.com/o/oauth2/v2/auth?${params}` });
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
app.post('/api/sync/gdrive/callback', async (req, res) => {
|
|
773
|
+
try {
|
|
774
|
+
const { code, state } = req.body;
|
|
775
|
+
const studio = loadStudioConfig();
|
|
776
|
+
|
|
777
|
+
if (state !== studio.oauthState) {
|
|
778
|
+
return res.status(400).json({ error: 'Invalid state' });
|
|
620
779
|
}
|
|
780
|
+
const redirectUri = studio.oauthRedirectUri || 'http://localhost:3000/settings';
|
|
781
|
+
delete studio.oauthState;
|
|
782
|
+
delete studio.oauthRedirectUri;
|
|
621
783
|
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
784
|
+
const response = await fetch('https://oauth2.googleapis.com/token', {
|
|
785
|
+
method: 'POST',
|
|
786
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
787
|
+
body: new URLSearchParams({
|
|
788
|
+
code,
|
|
789
|
+
client_id: GDRIVE_CLIENT_ID,
|
|
790
|
+
// client_secret: 'GDRIVE_CLIENT_SECRET', // NOTE: OAuth flow for installed apps usually requires client secret.
|
|
791
|
+
// For simplicity in this demo, we assume PKCE or a public client flow if supported,
|
|
792
|
+
// OR the user will need to provide the secret in environment variables.
|
|
793
|
+
// Since this is a local server, we might need the secret.
|
|
794
|
+
// Let's assume for now we'll put a placeholder or rely on env vars.
|
|
795
|
+
// Google "Installed App" flow doesn't always need secret if type is "Desktop".
|
|
796
|
+
// However, for web flow it does.
|
|
797
|
+
// Let's assume we need a client secret env var for now.
|
|
798
|
+
client_secret: process.env.GDRIVE_CLIENT_SECRET || 'your-google-client-secret',
|
|
799
|
+
redirect_uri: redirectUri,
|
|
800
|
+
grant_type: 'authorization_code'
|
|
801
|
+
})
|
|
802
|
+
});
|
|
630
803
|
|
|
631
|
-
|
|
632
|
-
|
|
804
|
+
if (!response.ok) {
|
|
805
|
+
const err = await response.text();
|
|
806
|
+
return res.status(400).json({ error: `Google auth failed: ${err}` });
|
|
807
|
+
}
|
|
633
808
|
|
|
634
|
-
|
|
809
|
+
const tokens = await response.json();
|
|
810
|
+
studio.cloudProvider = 'gdrive';
|
|
811
|
+
studio.cloudToken = tokens.access_token;
|
|
812
|
+
if (tokens.refresh_token) studio.cloudRefreshToken = tokens.refresh_token;
|
|
635
813
|
saveStudioConfig(studio);
|
|
636
814
|
|
|
637
|
-
res.json({ success: true,
|
|
815
|
+
res.json({ success: true, provider: 'gdrive' });
|
|
638
816
|
} catch (err) {
|
|
639
817
|
res.status(500).json({ error: err.message });
|
|
640
818
|
}
|
|
641
819
|
});
|
|
642
820
|
|
|
643
|
-
app.post('/api/sync/
|
|
821
|
+
app.post('/api/sync/push', async (req, res) => {
|
|
644
822
|
try {
|
|
645
823
|
const studio = loadStudioConfig();
|
|
646
|
-
|
|
647
|
-
|
|
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' });
|
|
824
|
+
if (!studio.cloudProvider || !studio.cloudToken) {
|
|
825
|
+
return res.status(400).json({ error: 'No cloud provider connected' });
|
|
826
|
+
}
|
|
651
827
|
|
|
652
|
-
const backup =
|
|
828
|
+
const backup = buildBackupData();
|
|
829
|
+
const content = JSON.stringify(backup, null, 2);
|
|
653
830
|
|
|
654
|
-
if (
|
|
655
|
-
const
|
|
656
|
-
|
|
831
|
+
if (studio.cloudProvider === 'dropbox') {
|
|
832
|
+
const response = await fetch('https://content.dropboxapi.com/2/files/upload', {
|
|
833
|
+
method: 'POST',
|
|
834
|
+
headers: {
|
|
835
|
+
'Authorization': `Bearer ${studio.cloudToken}`,
|
|
836
|
+
'Content-Type': 'application/octet-stream',
|
|
837
|
+
'Dropbox-API-Arg': JSON.stringify({
|
|
838
|
+
path: '/opencode-studio-sync.json',
|
|
839
|
+
mode: 'overwrite',
|
|
840
|
+
autorename: false
|
|
841
|
+
})
|
|
842
|
+
},
|
|
843
|
+
body: content
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
if (!response.ok) {
|
|
847
|
+
const err = await response.text();
|
|
848
|
+
return res.status(400).json({ error: `Dropbox upload failed: ${err}` });
|
|
849
|
+
}
|
|
850
|
+
} else if (studio.cloudProvider === 'gdrive') {
|
|
851
|
+
// Find existing file
|
|
852
|
+
const listRes = await fetch('https://www.googleapis.com/drive/v3/files?q=name=\'opencode-studio-sync.json\' and trashed=false', {
|
|
853
|
+
headers: { 'Authorization': `Bearer ${studio.cloudToken}` }
|
|
854
|
+
});
|
|
855
|
+
const listData = await listRes.json();
|
|
856
|
+
const existingFile = listData.files?.[0];
|
|
857
|
+
|
|
858
|
+
if (existingFile) {
|
|
859
|
+
// Update content
|
|
860
|
+
const updateRes = await fetch(`https://www.googleapis.com/upload/drive/v3/files/${existingFile.id}?uploadType=media`, {
|
|
861
|
+
method: 'PATCH',
|
|
862
|
+
headers: {
|
|
863
|
+
'Authorization': `Bearer ${studio.cloudToken}`,
|
|
864
|
+
'Content-Type': 'application/json'
|
|
865
|
+
},
|
|
866
|
+
body: content
|
|
867
|
+
});
|
|
868
|
+
if (!updateRes.ok) {
|
|
869
|
+
const err = await updateRes.text();
|
|
870
|
+
return res.status(400).json({ error: `Google Drive update failed: ${err}` });
|
|
871
|
+
}
|
|
872
|
+
} else {
|
|
873
|
+
// Create new file (multipart)
|
|
874
|
+
const metadata = { name: 'opencode-studio-sync.json', mimeType: 'application/json' };
|
|
875
|
+
const boundary = '-------314159265358979323846';
|
|
876
|
+
const delimiter = "\r\n--" + boundary + "\r\n";
|
|
877
|
+
const close_delim = "\r\n--" + boundary + "--";
|
|
878
|
+
|
|
879
|
+
const multipartBody =
|
|
880
|
+
delimiter +
|
|
881
|
+
'Content-Type: application/json\r\n\r\n' +
|
|
882
|
+
JSON.stringify(metadata) +
|
|
883
|
+
delimiter +
|
|
884
|
+
'Content-Type: application/json\r\n\r\n' +
|
|
885
|
+
content +
|
|
886
|
+
close_delim;
|
|
887
|
+
|
|
888
|
+
const createRes = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart', {
|
|
889
|
+
method: 'POST',
|
|
890
|
+
headers: {
|
|
891
|
+
'Authorization': `Bearer ${studio.cloudToken}`,
|
|
892
|
+
'Content-Type': `multipart/related; boundary="${boundary}"`
|
|
893
|
+
},
|
|
894
|
+
body: multipartBody
|
|
895
|
+
});
|
|
896
|
+
if (!createRes.ok) {
|
|
897
|
+
const err = await createRes.text();
|
|
898
|
+
return res.status(400).json({ error: `Google Drive create failed: ${err}` });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
657
901
|
}
|
|
658
|
-
if (backup.opencodeConfig) saveConfig(backup.opencodeConfig);
|
|
659
902
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
903
|
+
studio.lastSyncAt = backup.timestamp;
|
|
904
|
+
saveStudioConfig(studio);
|
|
905
|
+
res.json({ success: true, timestamp: backup.timestamp });
|
|
906
|
+
} catch (err) {
|
|
907
|
+
res.status(500).json({ error: err.message });
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
app.post('/api/sync/pull', async (req, res) => {
|
|
912
|
+
try {
|
|
913
|
+
const studio = loadStudioConfig();
|
|
914
|
+
if (!studio.cloudProvider || !studio.cloudToken) {
|
|
915
|
+
return res.status(400).json({ error: 'No cloud provider connected' });
|
|
668
916
|
}
|
|
669
917
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
918
|
+
let content;
|
|
919
|
+
|
|
920
|
+
if (studio.cloudProvider === 'dropbox') {
|
|
921
|
+
const response = await fetch('https://content.dropboxapi.com/2/files/download', {
|
|
922
|
+
method: 'POST',
|
|
923
|
+
headers: {
|
|
924
|
+
'Authorization': `Bearer ${studio.cloudToken}`,
|
|
925
|
+
'Dropbox-API-Arg': JSON.stringify({ path: '/opencode-studio-sync.json' })
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
if (!response.ok) {
|
|
930
|
+
if (response.status === 409) {
|
|
931
|
+
return res.status(404).json({ error: 'No sync file found in cloud' });
|
|
932
|
+
}
|
|
933
|
+
const err = await response.text();
|
|
934
|
+
return res.status(400).json({ error: `Dropbox download failed: ${err}` });
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
content = await response.text();
|
|
938
|
+
} else if (studio.cloudProvider === 'gdrive') {
|
|
939
|
+
const listRes = await fetch('https://www.googleapis.com/drive/v3/files?q=name=\'opencode-studio-sync.json\' and trashed=false', {
|
|
940
|
+
headers: { 'Authorization': `Bearer ${studio.cloudToken}` }
|
|
941
|
+
});
|
|
942
|
+
const listData = await listRes.json();
|
|
943
|
+
const existingFile = listData.files?.[0];
|
|
944
|
+
|
|
945
|
+
if (!existingFile) return res.status(404).json({ error: 'No sync file found in cloud' });
|
|
946
|
+
|
|
947
|
+
const downloadRes = await fetch(`https://www.googleapis.com/drive/v3/files/${existingFile.id}?alt=media`, {
|
|
948
|
+
headers: { 'Authorization': `Bearer ${studio.cloudToken}` }
|
|
675
949
|
});
|
|
950
|
+
if (!downloadRes.ok) {
|
|
951
|
+
const err = await downloadRes.text();
|
|
952
|
+
return res.status(400).json({ error: `Google Drive download failed: ${err}` });
|
|
953
|
+
}
|
|
954
|
+
content = await downloadRes.text();
|
|
676
955
|
}
|
|
677
956
|
|
|
957
|
+
const backup = JSON.parse(content);
|
|
958
|
+
restoreFromBackup(backup, studio);
|
|
959
|
+
|
|
678
960
|
const updated = loadStudioConfig();
|
|
679
961
|
updated.lastSyncAt = new Date().toISOString();
|
|
680
962
|
saveStudioConfig(updated);
|
|
@@ -685,98 +967,41 @@ app.post('/api/sync/pull', (req, res) => {
|
|
|
685
967
|
}
|
|
686
968
|
});
|
|
687
969
|
|
|
688
|
-
app.
|
|
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) => {
|
|
970
|
+
app.post('/api/sync/auto', async (req, res) => {
|
|
739
971
|
const studio = loadStudioConfig();
|
|
740
|
-
if (!studio.
|
|
972
|
+
if (!studio.cloudProvider || !studio.cloudToken || !studio.autoSync) {
|
|
741
973
|
return res.json({ action: 'none', reason: 'auto-sync not configured' });
|
|
742
974
|
}
|
|
743
975
|
|
|
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
976
|
try {
|
|
752
|
-
|
|
753
|
-
const remoteTime = new Date(remote.timestamp).getTime();
|
|
754
|
-
const localTime = studio.lastSyncAt ? new Date(studio.lastSyncAt).getTime() : 0;
|
|
977
|
+
let content;
|
|
755
978
|
|
|
756
|
-
if (
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
979
|
+
if (studio.cloudProvider === 'dropbox') {
|
|
980
|
+
const response = await fetch('https://content.dropboxapi.com/2/files/download', {
|
|
981
|
+
method: 'POST',
|
|
982
|
+
headers: {
|
|
983
|
+
'Authorization': `Bearer ${studio.cloudToken}`,
|
|
984
|
+
'Dropbox-API-Arg': JSON.stringify({ path: '/opencode-studio-sync.json' })
|
|
985
|
+
}
|
|
986
|
+
});
|
|
762
987
|
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
});
|
|
988
|
+
if (!response.ok) {
|
|
989
|
+
return res.json({ action: 'none', reason: 'no remote file' });
|
|
771
990
|
}
|
|
772
991
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
992
|
+
content = await response.text();
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (!content) {
|
|
996
|
+
return res.json({ action: 'none', reason: 'no content' });
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const remote = JSON.parse(content);
|
|
1000
|
+
const remoteTime = new Date(remote.timestamp).getTime();
|
|
1001
|
+
const localTime = studio.lastSyncAt ? new Date(studio.lastSyncAt).getTime() : 0;
|
|
1002
|
+
|
|
1003
|
+
if (remoteTime > localTime) {
|
|
1004
|
+
restoreFromBackup(remote, studio);
|
|
780
1005
|
|
|
781
1006
|
const updated = loadStudioConfig();
|
|
782
1007
|
updated.lastSyncAt = new Date().toISOString();
|