opencode-studio-server 1.0.0 → 1.0.1

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 CHANGED
@@ -1,14 +1,34 @@
1
- const express = require('express');
2
- const cors = require('cors');
3
- const bodyParser = require('body-parser');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
- const { exec, spawn } = require('child_process');
8
-
9
- const app = express();
10
- const PORT = 3001;
11
-
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const bodyParser = require('body-parser');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { exec, spawn } = require('child_process');
8
+
9
+ const app = express();
10
+ const PORT = 3001;
11
+ const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
12
+
13
+ let lastActivityTime = Date.now();
14
+ let idleTimer = null;
15
+
16
+ function resetIdleTimer() {
17
+ lastActivityTime = Date.now();
18
+ if (idleTimer) clearTimeout(idleTimer);
19
+ idleTimer = setTimeout(() => {
20
+ console.log('Server idle for 30 minutes, shutting down...');
21
+ process.exit(0);
22
+ }, IDLE_TIMEOUT_MS);
23
+ }
24
+
25
+ resetIdleTimer();
26
+
27
+ app.use((req, res, next) => {
28
+ resetIdleTimer();
29
+ next();
30
+ });
31
+
12
32
  const ALLOWED_ORIGINS = [
13
33
  'http://localhost:3000',
14
34
  'http://127.0.0.1:3000',
@@ -169,6 +189,11 @@ app.get('/api/health', (req, res) => {
169
189
  res.json({ status: 'ok', timestamp: Date.now() });
170
190
  });
171
191
 
192
+ app.post('/api/shutdown', (req, res) => {
193
+ res.json({ success: true, message: 'Server shutting down' });
194
+ setTimeout(() => process.exit(0), 100);
195
+ });
196
+
172
197
  app.get('/api/pending-action', (req, res) => {
173
198
  const action = loadPendingAction();
174
199
  res.json({ action });
@@ -764,15 +789,16 @@ function saveAuthConfig(config) {
764
789
  }
765
790
  }
766
791
 
767
- const PROVIDER_DISPLAY_NAMES = {
768
- 'github-copilot': 'GitHub Copilot',
769
- 'google': 'Google',
770
- 'anthropic': 'Anthropic',
771
- 'openai': 'OpenAI',
772
- 'zai': 'Z.AI',
773
- 'xai': 'xAI',
774
- 'groq': 'Groq',
775
- 'together': 'Together AI',
792
+ const PROVIDER_DISPLAY_NAMES = {
793
+ 'github-copilot': 'GitHub Copilot',
794
+ 'google': 'Google',
795
+ 'google-gemini-oauth': 'Google Gemini (OAuth)',
796
+ 'anthropic': 'Anthropic',
797
+ 'openai': 'OpenAI',
798
+ 'zai': 'Z.AI',
799
+ 'xai': 'xAI',
800
+ 'groq': 'Groq',
801
+ 'together': 'Together AI',
776
802
  'mistral': 'Mistral',
777
803
  'deepseek': 'DeepSeek',
778
804
  'openrouter': 'OpenRouter',
@@ -780,31 +806,45 @@ const PROVIDER_DISPLAY_NAMES = {
780
806
  'azure': 'Azure OpenAI',
781
807
  };
782
808
 
783
- app.get('/api/auth', (req, res) => {
784
- const authConfig = loadAuthConfig();
785
- const authFile = getAuthFile();
786
-
787
- if (!authConfig) {
788
- return res.json({
789
- credentials: [],
790
- authFile: null,
791
- message: 'No auth file found'
792
- });
793
- }
794
-
795
- const credentials = Object.entries(authConfig).map(([id, config]) => {
796
- const isExpired = config.expires ? Date.now() > config.expires : false;
797
- return {
798
- id,
799
- name: PROVIDER_DISPLAY_NAMES[id] || id,
800
- type: config.type,
801
- isExpired,
802
- expiresAt: config.expires || null,
803
- };
804
- });
805
-
806
- res.json({ credentials, authFile });
807
- });
809
+ app.get('/api/auth', (req, res) => {
810
+ const authConfig = loadAuthConfig();
811
+ const authFile = getAuthFile();
812
+ const paths = getPaths();
813
+
814
+ let hasGeminiAuthPlugin = false;
815
+ if (paths && fs.existsSync(paths.opencodeJson)) {
816
+ try {
817
+ const config = JSON.parse(fs.readFileSync(paths.opencodeJson, 'utf8'));
818
+ if (Array.isArray(config.plugin)) {
819
+ hasGeminiAuthPlugin = config.plugin.some(p =>
820
+ typeof p === 'string' && p.includes('opencode-gemini-auth')
821
+ );
822
+ }
823
+ } catch {}
824
+ }
825
+
826
+ if (!authConfig) {
827
+ return res.json({
828
+ credentials: [],
829
+ authFile: null,
830
+ message: 'No auth file found',
831
+ hasGeminiAuthPlugin,
832
+ });
833
+ }
834
+
835
+ const credentials = Object.entries(authConfig).map(([id, config]) => {
836
+ const isExpired = config.expires ? Date.now() > config.expires : false;
837
+ return {
838
+ id,
839
+ name: PROVIDER_DISPLAY_NAMES[id] || id,
840
+ type: config.type,
841
+ isExpired,
842
+ expiresAt: config.expires || null,
843
+ };
844
+ });
845
+
846
+ res.json({ credentials, authFile, hasGeminiAuthPlugin });
847
+ });
808
848
 
809
849
  app.post('/api/auth/login', (req, res) => {
810
850
  const { provider } = req.body;
@@ -874,6 +914,203 @@ app.get('/api/auth/providers', (req, res) => {
874
914
  res.json(providers);
875
915
  });
876
916
 
877
- app.listen(PORT, () => {
878
- console.log(`Server running on http://localhost:${PORT}`);
879
- });
917
+ const AUTH_PROFILES_DIR = path.join(HOME_DIR, '.config', 'opencode-studio', 'auth-profiles');
918
+
919
+ function ensureAuthProfilesDir() {
920
+ if (!fs.existsSync(AUTH_PROFILES_DIR)) {
921
+ fs.mkdirSync(AUTH_PROFILES_DIR, { recursive: true });
922
+ }
923
+ }
924
+
925
+ function getProviderProfilesDir(provider) {
926
+ return path.join(AUTH_PROFILES_DIR, provider);
927
+ }
928
+
929
+ function listAuthProfiles(provider) {
930
+ const dir = getProviderProfilesDir(provider);
931
+ if (!fs.existsSync(dir)) return [];
932
+
933
+ try {
934
+ return fs.readdirSync(dir)
935
+ .filter(f => f.endsWith('.json'))
936
+ .map(f => f.replace('.json', ''));
937
+ } catch {
938
+ return [];
939
+ }
940
+ }
941
+
942
+ function getNextProfileName(provider) {
943
+ const existing = listAuthProfiles(provider);
944
+ let num = 1;
945
+ while (existing.includes(`account-${num}`)) {
946
+ num++;
947
+ }
948
+ return `account-${num}`;
949
+ }
950
+
951
+ function saveAuthProfile(provider, profileName, data) {
952
+ ensureAuthProfilesDir();
953
+ const dir = getProviderProfilesDir(provider);
954
+ if (!fs.existsSync(dir)) {
955
+ fs.mkdirSync(dir, { recursive: true });
956
+ }
957
+ const filePath = path.join(dir, `${profileName}.json`);
958
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
959
+ return true;
960
+ }
961
+
962
+ function loadAuthProfile(provider, profileName) {
963
+ const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
964
+ if (!fs.existsSync(filePath)) return null;
965
+ try {
966
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
967
+ } catch {
968
+ return null;
969
+ }
970
+ }
971
+
972
+ function deleteAuthProfile(provider, profileName) {
973
+ const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
974
+ if (fs.existsSync(filePath)) {
975
+ fs.unlinkSync(filePath);
976
+ return true;
977
+ }
978
+ return false;
979
+ }
980
+
981
+ function getActiveProfiles() {
982
+ const studioConfig = loadStudioConfig();
983
+ return studioConfig.activeProfiles || {};
984
+ }
985
+
986
+ function setActiveProfile(provider, profileName) {
987
+ const studioConfig = loadStudioConfig();
988
+ studioConfig.activeProfiles = studioConfig.activeProfiles || {};
989
+ studioConfig.activeProfiles[provider] = profileName;
990
+ saveStudioConfig(studioConfig);
991
+ }
992
+
993
+ app.get('/api/auth/profiles', (req, res) => {
994
+ ensureAuthProfilesDir();
995
+ const activeProfiles = getActiveProfiles();
996
+ const authConfig = loadAuthConfig() || {};
997
+
998
+ const profiles = {};
999
+
1000
+ Object.keys(PROVIDER_DISPLAY_NAMES).forEach(provider => {
1001
+ const providerProfiles = listAuthProfiles(provider);
1002
+ profiles[provider] = {
1003
+ profiles: providerProfiles,
1004
+ active: activeProfiles[provider] || null,
1005
+ hasCurrentAuth: !!authConfig[provider],
1006
+ };
1007
+ });
1008
+
1009
+ res.json(profiles);
1010
+ });
1011
+
1012
+ app.get('/api/auth/profiles/:provider', (req, res) => {
1013
+ const { provider } = req.params;
1014
+ const providerProfiles = listAuthProfiles(provider);
1015
+ const activeProfiles = getActiveProfiles();
1016
+ const authConfig = loadAuthConfig() || {};
1017
+
1018
+ res.json({
1019
+ profiles: providerProfiles,
1020
+ active: activeProfiles[provider] || null,
1021
+ hasCurrentAuth: !!authConfig[provider],
1022
+ });
1023
+ });
1024
+
1025
+ app.post('/api/auth/profiles/:provider', (req, res) => {
1026
+ const { provider } = req.params;
1027
+ const { name } = req.body;
1028
+
1029
+ const authConfig = loadAuthConfig();
1030
+ if (!authConfig || !authConfig[provider]) {
1031
+ return res.status(400).json({ error: `No active auth for ${provider} to save` });
1032
+ }
1033
+
1034
+ const profileName = name || getNextProfileName(provider);
1035
+ const data = authConfig[provider];
1036
+
1037
+ try {
1038
+ saveAuthProfile(provider, profileName, data);
1039
+ setActiveProfile(provider, profileName);
1040
+ res.json({ success: true, name: profileName });
1041
+ } catch (err) {
1042
+ res.status(500).json({ error: 'Failed to save profile', details: err.message });
1043
+ }
1044
+ });
1045
+
1046
+ app.post('/api/auth/profiles/:provider/:name/activate', (req, res) => {
1047
+ const { provider, name } = req.params;
1048
+
1049
+ const profileData = loadAuthProfile(provider, name);
1050
+ if (!profileData) {
1051
+ return res.status(404).json({ error: 'Profile not found' });
1052
+ }
1053
+
1054
+ const authConfig = loadAuthConfig() || {};
1055
+ authConfig[provider] = profileData;
1056
+
1057
+ if (saveAuthConfig(authConfig)) {
1058
+ setActiveProfile(provider, name);
1059
+ res.json({ success: true });
1060
+ } else {
1061
+ res.status(500).json({ error: 'Failed to activate profile' });
1062
+ }
1063
+ });
1064
+
1065
+ app.delete('/api/auth/profiles/:provider/:name', (req, res) => {
1066
+ const { provider, name } = req.params;
1067
+
1068
+ if (deleteAuthProfile(provider, name)) {
1069
+ const activeProfiles = getActiveProfiles();
1070
+ if (activeProfiles[provider] === name) {
1071
+ const studioConfig = loadStudioConfig();
1072
+ delete studioConfig.activeProfiles[provider];
1073
+ saveStudioConfig(studioConfig);
1074
+ }
1075
+ res.json({ success: true });
1076
+ } else {
1077
+ res.status(404).json({ error: 'Profile not found' });
1078
+ }
1079
+ });
1080
+
1081
+ app.put('/api/auth/profiles/:provider/:name', (req, res) => {
1082
+ const { provider, name } = req.params;
1083
+ const { newName } = req.body;
1084
+
1085
+ if (!newName || newName === name) {
1086
+ return res.status(400).json({ error: 'New name is required and must be different' });
1087
+ }
1088
+
1089
+ const profileData = loadAuthProfile(provider, name);
1090
+ if (!profileData) {
1091
+ return res.status(404).json({ error: 'Profile not found' });
1092
+ }
1093
+
1094
+ const existingProfiles = listAuthProfiles(provider);
1095
+ if (existingProfiles.includes(newName)) {
1096
+ return res.status(400).json({ error: 'Profile name already exists' });
1097
+ }
1098
+
1099
+ try {
1100
+ saveAuthProfile(provider, newName, profileData);
1101
+ deleteAuthProfile(provider, name);
1102
+
1103
+ const activeProfiles = getActiveProfiles();
1104
+ if (activeProfiles[provider] === name) {
1105
+ setActiveProfile(provider, newName);
1106
+ }
1107
+
1108
+ res.json({ success: true, name: newName });
1109
+ } catch (err) {
1110
+ res.status(500).json({ error: 'Failed to rename profile', details: err.message });
1111
+ }
1112
+ });
1113
+
1114
+ app.listen(PORT, () => {
1115
+ console.log(`Server running on http://localhost:${PORT}`);
1116
+ });
package/launcher.vbs ADDED
@@ -0,0 +1,6 @@
1
+ Set WshShell = CreateObject("WScript.Shell")
2
+ args = ""
3
+ If WScript.Arguments.Count > 0 Then
4
+ args = " """ & WScript.Arguments(0) & """"
5
+ End If
6
+ WshShell.Run """C:\\node\\node.exe""" & " ""C:\\Users\\Microck\\opencode-studio\\server\\cli.js"" " & args, 0, False
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-studio-server",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,13 +1,32 @@
1
1
  const { exec } = require('child_process');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
+ const fs = require('fs');
4
5
 
5
6
  const PROTOCOL = 'opencodestudio';
6
7
 
7
8
  function registerWindows() {
8
- const nodePath = process.execPath.replace(/\\/g, '\\\\');
9
- const scriptPath = path.join(__dirname, 'cli.js').replace(/\\/g, '\\\\');
10
- const command = `"${nodePath}" "${scriptPath}" "%1"`;
9
+ const nodePath = process.execPath;
10
+ const scriptPath = path.join(__dirname, 'cli.js');
11
+
12
+ // Create a VBScript launcher that runs node hidden
13
+ const vbsPath = path.join(__dirname, 'launcher.vbs');
14
+ const vbsContent = `Set WshShell = CreateObject("WScript.Shell")
15
+ args = ""
16
+ If WScript.Arguments.Count > 0 Then
17
+ args = " """ & WScript.Arguments(0) & """"
18
+ End If
19
+ WshShell.Run """${nodePath.replace(/\\/g, '\\\\')}""" & " ""${scriptPath.replace(/\\/g, '\\\\')}"" " & args, 0, False
20
+ `;
21
+
22
+ try {
23
+ fs.writeFileSync(vbsPath, vbsContent);
24
+ } catch (err) {
25
+ console.error('Failed to create launcher.vbs:', err.message);
26
+ }
27
+
28
+ // Register protocol to use wscript with the VBS launcher
29
+ const command = `wscript.exe "${vbsPath.replace(/\\/g, '\\\\')}" "%1"`;
11
30
 
12
31
  const regCommands = [
13
32
  `reg add "HKCU\\Software\\Classes\\${PROTOCOL}" /ve /d "URL:OpenCode Studio Protocol" /f`,