opencode-studio-server 1.0.5 → 1.0.7

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 +386 -395
  2. package/package.json +32 -32
package/index.js CHANGED
@@ -1,92 +1,92 @@
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
-
32
- const ALLOWED_ORIGINS = [
33
- 'http://localhost:3000',
34
- 'http://127.0.0.1:3000',
35
- 'https://opencode-studio.vercel.app',
36
- 'https://opencode.micr.dev',
37
- 'https://opencode-studio.micr.dev',
38
- /\.vercel\.app$/,
39
- /\.micr\.dev$/,
40
- ];
41
-
42
- app.use(cors({
43
- origin: (origin, callback) => {
44
- if (!origin) return callback(null, true);
45
- const allowed = ALLOWED_ORIGINS.some(o =>
46
- o instanceof RegExp ? o.test(origin) : o === origin
47
- );
48
- callback(null, allowed);
49
- },
50
- credentials: true,
51
- }));
52
- app.use(bodyParser.json({ limit: '50mb' }));
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
+
32
+ const ALLOWED_ORIGINS = [
33
+ 'http://localhost:3000',
34
+ 'http://127.0.0.1:3000',
35
+ 'https://opencode-studio.vercel.app',
36
+ 'https://opencode.micr.dev',
37
+ 'https://opencode-studio.micr.dev',
38
+ /\.vercel\.app$/,
39
+ /\.micr\.dev$/,
40
+ ];
41
+
42
+ app.use(cors({
43
+ origin: (origin, callback) => {
44
+ if (!origin) return callback(null, true);
45
+ const allowed = ALLOWED_ORIGINS.some(o =>
46
+ o instanceof RegExp ? o.test(origin) : o === origin
47
+ );
48
+ callback(null, allowed);
49
+ },
50
+ credentials: true,
51
+ }));
52
+ app.use(bodyParser.json({ limit: '50mb' }));
53
53
  app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
54
54
 
55
- const HOME_DIR = os.homedir();
56
- const STUDIO_CONFIG_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'studio.json');
57
- const PENDING_ACTION_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'pending-action.json');
58
-
59
- let pendingActionMemory = null;
60
-
61
- function loadPendingAction() {
62
- if (pendingActionMemory) return pendingActionMemory;
63
-
64
- if (fs.existsSync(PENDING_ACTION_PATH)) {
65
- try {
66
- const action = JSON.parse(fs.readFileSync(PENDING_ACTION_PATH, 'utf8'));
67
- if (action.timestamp && Date.now() - action.timestamp < 60000) {
68
- pendingActionMemory = action;
69
- fs.unlinkSync(PENDING_ACTION_PATH);
70
- return action;
71
- }
72
- fs.unlinkSync(PENDING_ACTION_PATH);
73
- } catch {
74
- try { fs.unlinkSync(PENDING_ACTION_PATH); } catch {}
75
- }
76
- }
77
- return null;
78
- }
79
-
80
- function clearPendingAction() {
81
- pendingActionMemory = null;
82
- if (fs.existsSync(PENDING_ACTION_PATH)) {
83
- try { fs.unlinkSync(PENDING_ACTION_PATH); } catch {}
84
- }
85
- }
86
-
87
- function setPendingAction(action) {
88
- pendingActionMemory = { ...action, timestamp: Date.now() };
89
- }
55
+ const HOME_DIR = os.homedir();
56
+ const STUDIO_CONFIG_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'studio.json');
57
+ const PENDING_ACTION_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'pending-action.json');
58
+
59
+ let pendingActionMemory = null;
60
+
61
+ function loadPendingAction() {
62
+ if (pendingActionMemory) return pendingActionMemory;
63
+
64
+ if (fs.existsSync(PENDING_ACTION_PATH)) {
65
+ try {
66
+ const action = JSON.parse(fs.readFileSync(PENDING_ACTION_PATH, 'utf8'));
67
+ if (action.timestamp && Date.now() - action.timestamp < 60000) {
68
+ pendingActionMemory = action;
69
+ fs.unlinkSync(PENDING_ACTION_PATH);
70
+ return action;
71
+ }
72
+ fs.unlinkSync(PENDING_ACTION_PATH);
73
+ } catch {
74
+ try { fs.unlinkSync(PENDING_ACTION_PATH); } catch {}
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+
80
+ function clearPendingAction() {
81
+ pendingActionMemory = null;
82
+ if (fs.existsSync(PENDING_ACTION_PATH)) {
83
+ try { fs.unlinkSync(PENDING_ACTION_PATH); } catch {}
84
+ }
85
+ }
86
+
87
+ function setPendingAction(action) {
88
+ pendingActionMemory = { ...action, timestamp: Date.now() };
89
+ }
90
90
 
91
91
  const AUTH_CANDIDATE_PATHS = [
92
92
  path.join(HOME_DIR, '.local', 'share', 'opencode', 'auth.json'),
@@ -187,38 +187,38 @@ description: ${description}
187
187
  ${body}`;
188
188
  }
189
189
 
190
- console.log(`Detected config at: ${getConfigDir() || 'NOT FOUND'}`);
191
-
192
- app.get('/api/health', (req, res) => {
193
- res.json({ status: 'ok', timestamp: Date.now() });
194
- });
195
-
196
- app.post('/api/shutdown', (req, res) => {
197
- res.json({ success: true, message: 'Server shutting down' });
198
- setTimeout(() => process.exit(0), 100);
199
- });
200
-
201
- app.get('/api/pending-action', (req, res) => {
202
- const action = loadPendingAction();
203
- res.json({ action });
204
- });
205
-
206
- app.delete('/api/pending-action', (req, res) => {
207
- clearPendingAction();
208
- res.json({ success: true });
209
- });
210
-
211
- app.post('/api/pending-action', (req, res) => {
212
- const { action } = req.body;
213
- if (action && action.type) {
214
- setPendingAction(action);
215
- res.json({ success: true });
216
- } else {
217
- res.status(400).json({ error: 'Invalid action' });
218
- }
219
- });
220
-
221
- app.get('/api/paths', (req, res) => {
190
+ console.log(`Detected config at: ${getConfigDir() || 'NOT FOUND'}`);
191
+
192
+ app.get('/api/health', (req, res) => {
193
+ res.json({ status: 'ok', timestamp: Date.now() });
194
+ });
195
+
196
+ app.post('/api/shutdown', (req, res) => {
197
+ res.json({ success: true, message: 'Server shutting down' });
198
+ setTimeout(() => process.exit(0), 100);
199
+ });
200
+
201
+ app.get('/api/pending-action', (req, res) => {
202
+ const action = loadPendingAction();
203
+ res.json({ action });
204
+ });
205
+
206
+ app.delete('/api/pending-action', (req, res) => {
207
+ clearPendingAction();
208
+ res.json({ success: true });
209
+ });
210
+
211
+ app.post('/api/pending-action', (req, res) => {
212
+ const { action } = req.body;
213
+ if (action && action.type) {
214
+ setPendingAction(action);
215
+ res.json({ success: true });
216
+ } else {
217
+ res.status(400).json({ error: 'Invalid action' });
218
+ }
219
+ });
220
+
221
+ app.get('/api/paths', (req, res) => {
222
222
  const detected = detectConfigDir();
223
223
  const studioConfig = loadStudioConfig();
224
224
  const current = getConfigDir();
@@ -793,16 +793,16 @@ function saveAuthConfig(config) {
793
793
  }
794
794
  }
795
795
 
796
- const PROVIDER_DISPLAY_NAMES = {
797
- 'github-copilot': 'GitHub Copilot',
798
- 'google': 'Google',
799
- 'google-gemini-oauth': 'Google Gemini (OAuth)',
800
- 'anthropic': 'Anthropic',
801
- 'openai': 'OpenAI',
802
- 'zai': 'Z.AI',
803
- 'xai': 'xAI',
804
- 'groq': 'Groq',
805
- 'together': 'Together AI',
796
+ const PROVIDER_DISPLAY_NAMES = {
797
+ 'github-copilot': 'GitHub Copilot',
798
+ 'google': 'Google',
799
+ 'google-gemini-oauth': 'Google Gemini (OAuth)',
800
+ 'anthropic': 'Anthropic',
801
+ 'openai': 'OpenAI',
802
+ 'zai': 'Z.AI',
803
+ 'xai': 'xAI',
804
+ 'groq': 'Groq',
805
+ 'together': 'Together AI',
806
806
  'mistral': 'Mistral',
807
807
  'deepseek': 'DeepSeek',
808
808
  'openrouter': 'OpenRouter',
@@ -810,80 +810,71 @@ const PROVIDER_DISPLAY_NAMES = {
810
810
  'azure': 'Azure OpenAI',
811
811
  };
812
812
 
813
- app.get('/api/auth', (req, res) => {
814
- const authConfig = loadAuthConfig();
815
- const authFile = getAuthFile();
816
- const paths = getPaths();
817
-
818
- let hasGeminiAuthPlugin = false;
819
- if (paths && fs.existsSync(paths.opencodeJson)) {
820
- try {
821
- const config = JSON.parse(fs.readFileSync(paths.opencodeJson, 'utf8'));
822
- if (Array.isArray(config.plugin)) {
823
- hasGeminiAuthPlugin = config.plugin.some(p =>
824
- typeof p === 'string' && p.includes('opencode-gemini-auth')
825
- );
826
- }
827
- } catch {}
828
- }
829
-
830
- if (!authConfig) {
831
- return res.json({
832
- credentials: [],
833
- authFile: null,
834
- message: 'No auth file found',
835
- hasGeminiAuthPlugin,
836
- });
837
- }
838
-
839
- const credentials = Object.entries(authConfig).map(([id, config]) => {
840
- const isExpired = config.expires ? Date.now() > config.expires : false;
841
- return {
842
- id,
843
- name: PROVIDER_DISPLAY_NAMES[id] || id,
844
- type: config.type,
845
- isExpired,
846
- expiresAt: config.expires || null,
847
- };
848
- });
849
-
850
- res.json({ credentials, authFile, hasGeminiAuthPlugin });
851
- });
852
-
853
- app.post('/api/auth/login', (req, res) => {
854
- const { provider } = req.body;
813
+ app.get('/api/auth', (req, res) => {
814
+ const authConfig = loadAuthConfig();
815
+ const authFile = getAuthFile();
816
+ const paths = getPaths();
855
817
 
856
- if (!provider) {
857
- return res.status(400).json({ error: 'Provider is required' });
818
+ let hasGeminiAuthPlugin = false;
819
+ if (paths && fs.existsSync(paths.opencodeJson)) {
820
+ try {
821
+ const config = JSON.parse(fs.readFileSync(paths.opencodeJson, 'utf8'));
822
+ if (Array.isArray(config.plugin)) {
823
+ hasGeminiAuthPlugin = config.plugin.some(p =>
824
+ typeof p === 'string' && p.includes('opencode-gemini-auth')
825
+ );
826
+ }
827
+ } catch {}
858
828
  }
859
-
860
- if (!PROVIDER_DISPLAY_NAMES[provider]) {
861
- return res.status(400).json({ error: 'Invalid provider' });
829
+
830
+ if (!authConfig) {
831
+ return res.json({
832
+ credentials: [],
833
+ authFile: null,
834
+ message: 'No auth file found',
835
+ hasGeminiAuthPlugin,
836
+ });
862
837
  }
863
838
 
864
- // Run opencode auth login - this opens browser
865
- // Use detached + shell to ensure browser opens even when server runs headless
839
+ const credentials = Object.entries(authConfig).map(([id, config]) => {
840
+ const isExpired = config.expires ? Date.now() > config.expires : false;
841
+ return {
842
+ id,
843
+ name: PROVIDER_DISPLAY_NAMES[id] || id,
844
+ type: config.type,
845
+ isExpired,
846
+ expiresAt: config.expires || null,
847
+ };
848
+ });
849
+
850
+ res.json({ credentials, authFile, hasGeminiAuthPlugin });
851
+ });
852
+
853
+ app.post('/api/auth/login', (req, res) => {
854
+ // opencode auth login is interactive and requires a terminal
855
+ // Open a new terminal window with the command
866
856
  const isWindows = process.platform === 'win32';
867
- const child = spawn('opencode', ['auth', 'login', provider], {
868
- detached: true,
869
- shell: isWindows,
870
- stdio: 'ignore',
871
- windowsHide: false,
872
- });
857
+ const isMac = process.platform === 'darwin';
873
858
 
874
- child.unref(); // Allow server to exit independently
859
+ let command;
860
+ if (isWindows) {
861
+ command = 'start cmd /k "opencode auth login"';
862
+ } else if (isMac) {
863
+ command = 'osascript -e \'tell app "Terminal" to do script "opencode auth login"\'';
864
+ } else {
865
+ command = 'x-terminal-emulator -e "opencode auth login" || gnome-terminal -- opencode auth login || xterm -e "opencode auth login"';
866
+ }
875
867
 
876
- child.on('error', (err) => {
877
- console.error('Failed to start auth login:', err);
868
+ exec(command, (err) => {
869
+ if (err) console.error('Failed to open terminal:', err);
878
870
  });
879
-
880
- // Return immediately - login happens in browser
881
- res.json({
882
- success: true,
883
- message: `Opening browser for ${provider} login...`,
884
- note: 'Complete authentication in your browser, then refresh this page.'
885
- });
886
- });
871
+
872
+ res.json({
873
+ success: true,
874
+ message: 'Opening terminal for authentication...',
875
+ note: 'Complete authentication in the terminal window, then refresh this page.'
876
+ });
877
+ });
887
878
 
888
879
  app.delete('/api/auth/:provider', (req, res) => {
889
880
  const provider = req.params.provider;
@@ -925,203 +916,203 @@ app.get('/api/auth/providers', (req, res) => {
925
916
  res.json(providers);
926
917
  });
927
918
 
928
- const AUTH_PROFILES_DIR = path.join(HOME_DIR, '.config', 'opencode-studio', 'auth-profiles');
929
-
930
- function ensureAuthProfilesDir() {
931
- if (!fs.existsSync(AUTH_PROFILES_DIR)) {
932
- fs.mkdirSync(AUTH_PROFILES_DIR, { recursive: true });
933
- }
934
- }
935
-
936
- function getProviderProfilesDir(provider) {
937
- return path.join(AUTH_PROFILES_DIR, provider);
938
- }
939
-
940
- function listAuthProfiles(provider) {
941
- const dir = getProviderProfilesDir(provider);
942
- if (!fs.existsSync(dir)) return [];
943
-
944
- try {
945
- return fs.readdirSync(dir)
946
- .filter(f => f.endsWith('.json'))
947
- .map(f => f.replace('.json', ''));
948
- } catch {
949
- return [];
950
- }
951
- }
952
-
953
- function getNextProfileName(provider) {
954
- const existing = listAuthProfiles(provider);
955
- let num = 1;
956
- while (existing.includes(`account-${num}`)) {
957
- num++;
958
- }
959
- return `account-${num}`;
960
- }
961
-
962
- function saveAuthProfile(provider, profileName, data) {
963
- ensureAuthProfilesDir();
964
- const dir = getProviderProfilesDir(provider);
965
- if (!fs.existsSync(dir)) {
966
- fs.mkdirSync(dir, { recursive: true });
967
- }
968
- const filePath = path.join(dir, `${profileName}.json`);
969
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
970
- return true;
971
- }
972
-
973
- function loadAuthProfile(provider, profileName) {
974
- const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
975
- if (!fs.existsSync(filePath)) return null;
976
- try {
977
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
978
- } catch {
979
- return null;
980
- }
981
- }
982
-
983
- function deleteAuthProfile(provider, profileName) {
984
- const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
985
- if (fs.existsSync(filePath)) {
986
- fs.unlinkSync(filePath);
987
- return true;
988
- }
989
- return false;
990
- }
991
-
992
- function getActiveProfiles() {
993
- const studioConfig = loadStudioConfig();
994
- return studioConfig.activeProfiles || {};
995
- }
996
-
997
- function setActiveProfile(provider, profileName) {
998
- const studioConfig = loadStudioConfig();
999
- studioConfig.activeProfiles = studioConfig.activeProfiles || {};
1000
- studioConfig.activeProfiles[provider] = profileName;
1001
- saveStudioConfig(studioConfig);
1002
- }
1003
-
1004
- app.get('/api/auth/profiles', (req, res) => {
1005
- ensureAuthProfilesDir();
1006
- const activeProfiles = getActiveProfiles();
1007
- const authConfig = loadAuthConfig() || {};
1008
-
1009
- const profiles = {};
1010
-
1011
- Object.keys(PROVIDER_DISPLAY_NAMES).forEach(provider => {
1012
- const providerProfiles = listAuthProfiles(provider);
1013
- profiles[provider] = {
1014
- profiles: providerProfiles,
1015
- active: activeProfiles[provider] || null,
1016
- hasCurrentAuth: !!authConfig[provider],
1017
- };
1018
- });
1019
-
1020
- res.json(profiles);
1021
- });
1022
-
1023
- app.get('/api/auth/profiles/:provider', (req, res) => {
1024
- const { provider } = req.params;
1025
- const providerProfiles = listAuthProfiles(provider);
1026
- const activeProfiles = getActiveProfiles();
1027
- const authConfig = loadAuthConfig() || {};
1028
-
1029
- res.json({
1030
- profiles: providerProfiles,
1031
- active: activeProfiles[provider] || null,
1032
- hasCurrentAuth: !!authConfig[provider],
1033
- });
1034
- });
1035
-
1036
- app.post('/api/auth/profiles/:provider', (req, res) => {
1037
- const { provider } = req.params;
1038
- const { name } = req.body;
1039
-
1040
- const authConfig = loadAuthConfig();
1041
- if (!authConfig || !authConfig[provider]) {
1042
- return res.status(400).json({ error: `No active auth for ${provider} to save` });
1043
- }
1044
-
1045
- const profileName = name || getNextProfileName(provider);
1046
- const data = authConfig[provider];
1047
-
1048
- try {
1049
- saveAuthProfile(provider, profileName, data);
1050
- setActiveProfile(provider, profileName);
1051
- res.json({ success: true, name: profileName });
1052
- } catch (err) {
1053
- res.status(500).json({ error: 'Failed to save profile', details: err.message });
1054
- }
1055
- });
1056
-
1057
- app.post('/api/auth/profiles/:provider/:name/activate', (req, res) => {
1058
- const { provider, name } = req.params;
1059
-
1060
- const profileData = loadAuthProfile(provider, name);
1061
- if (!profileData) {
1062
- return res.status(404).json({ error: 'Profile not found' });
1063
- }
1064
-
1065
- const authConfig = loadAuthConfig() || {};
1066
- authConfig[provider] = profileData;
1067
-
1068
- if (saveAuthConfig(authConfig)) {
1069
- setActiveProfile(provider, name);
1070
- res.json({ success: true });
1071
- } else {
1072
- res.status(500).json({ error: 'Failed to activate profile' });
1073
- }
1074
- });
1075
-
1076
- app.delete('/api/auth/profiles/:provider/:name', (req, res) => {
1077
- const { provider, name } = req.params;
1078
-
1079
- if (deleteAuthProfile(provider, name)) {
1080
- const activeProfiles = getActiveProfiles();
1081
- if (activeProfiles[provider] === name) {
1082
- const studioConfig = loadStudioConfig();
1083
- delete studioConfig.activeProfiles[provider];
1084
- saveStudioConfig(studioConfig);
1085
- }
1086
- res.json({ success: true });
1087
- } else {
1088
- res.status(404).json({ error: 'Profile not found' });
1089
- }
1090
- });
1091
-
1092
- app.put('/api/auth/profiles/:provider/:name', (req, res) => {
1093
- const { provider, name } = req.params;
1094
- const { newName } = req.body;
1095
-
1096
- if (!newName || newName === name) {
1097
- return res.status(400).json({ error: 'New name is required and must be different' });
1098
- }
1099
-
1100
- const profileData = loadAuthProfile(provider, name);
1101
- if (!profileData) {
1102
- return res.status(404).json({ error: 'Profile not found' });
1103
- }
1104
-
1105
- const existingProfiles = listAuthProfiles(provider);
1106
- if (existingProfiles.includes(newName)) {
1107
- return res.status(400).json({ error: 'Profile name already exists' });
1108
- }
1109
-
1110
- try {
1111
- saveAuthProfile(provider, newName, profileData);
1112
- deleteAuthProfile(provider, name);
1113
-
1114
- const activeProfiles = getActiveProfiles();
1115
- if (activeProfiles[provider] === name) {
1116
- setActiveProfile(provider, newName);
1117
- }
1118
-
1119
- res.json({ success: true, name: newName });
1120
- } catch (err) {
1121
- res.status(500).json({ error: 'Failed to rename profile', details: err.message });
1122
- }
1123
- });
1124
-
1125
- app.listen(PORT, () => {
1126
- console.log(`Server running on http://localhost:${PORT}`);
1127
- });
919
+ const AUTH_PROFILES_DIR = path.join(HOME_DIR, '.config', 'opencode-studio', 'auth-profiles');
920
+
921
+ function ensureAuthProfilesDir() {
922
+ if (!fs.existsSync(AUTH_PROFILES_DIR)) {
923
+ fs.mkdirSync(AUTH_PROFILES_DIR, { recursive: true });
924
+ }
925
+ }
926
+
927
+ function getProviderProfilesDir(provider) {
928
+ return path.join(AUTH_PROFILES_DIR, provider);
929
+ }
930
+
931
+ function listAuthProfiles(provider) {
932
+ const dir = getProviderProfilesDir(provider);
933
+ if (!fs.existsSync(dir)) return [];
934
+
935
+ try {
936
+ return fs.readdirSync(dir)
937
+ .filter(f => f.endsWith('.json'))
938
+ .map(f => f.replace('.json', ''));
939
+ } catch {
940
+ return [];
941
+ }
942
+ }
943
+
944
+ function getNextProfileName(provider) {
945
+ const existing = listAuthProfiles(provider);
946
+ let num = 1;
947
+ while (existing.includes(`account-${num}`)) {
948
+ num++;
949
+ }
950
+ return `account-${num}`;
951
+ }
952
+
953
+ function saveAuthProfile(provider, profileName, data) {
954
+ ensureAuthProfilesDir();
955
+ const dir = getProviderProfilesDir(provider);
956
+ if (!fs.existsSync(dir)) {
957
+ fs.mkdirSync(dir, { recursive: true });
958
+ }
959
+ const filePath = path.join(dir, `${profileName}.json`);
960
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
961
+ return true;
962
+ }
963
+
964
+ function loadAuthProfile(provider, profileName) {
965
+ const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
966
+ if (!fs.existsSync(filePath)) return null;
967
+ try {
968
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
969
+ } catch {
970
+ return null;
971
+ }
972
+ }
973
+
974
+ function deleteAuthProfile(provider, profileName) {
975
+ const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
976
+ if (fs.existsSync(filePath)) {
977
+ fs.unlinkSync(filePath);
978
+ return true;
979
+ }
980
+ return false;
981
+ }
982
+
983
+ function getActiveProfiles() {
984
+ const studioConfig = loadStudioConfig();
985
+ return studioConfig.activeProfiles || {};
986
+ }
987
+
988
+ function setActiveProfile(provider, profileName) {
989
+ const studioConfig = loadStudioConfig();
990
+ studioConfig.activeProfiles = studioConfig.activeProfiles || {};
991
+ studioConfig.activeProfiles[provider] = profileName;
992
+ saveStudioConfig(studioConfig);
993
+ }
994
+
995
+ app.get('/api/auth/profiles', (req, res) => {
996
+ ensureAuthProfilesDir();
997
+ const activeProfiles = getActiveProfiles();
998
+ const authConfig = loadAuthConfig() || {};
999
+
1000
+ const profiles = {};
1001
+
1002
+ Object.keys(PROVIDER_DISPLAY_NAMES).forEach(provider => {
1003
+ const providerProfiles = listAuthProfiles(provider);
1004
+ profiles[provider] = {
1005
+ profiles: providerProfiles,
1006
+ active: activeProfiles[provider] || null,
1007
+ hasCurrentAuth: !!authConfig[provider],
1008
+ };
1009
+ });
1010
+
1011
+ res.json(profiles);
1012
+ });
1013
+
1014
+ app.get('/api/auth/profiles/:provider', (req, res) => {
1015
+ const { provider } = req.params;
1016
+ const providerProfiles = listAuthProfiles(provider);
1017
+ const activeProfiles = getActiveProfiles();
1018
+ const authConfig = loadAuthConfig() || {};
1019
+
1020
+ res.json({
1021
+ profiles: providerProfiles,
1022
+ active: activeProfiles[provider] || null,
1023
+ hasCurrentAuth: !!authConfig[provider],
1024
+ });
1025
+ });
1026
+
1027
+ app.post('/api/auth/profiles/:provider', (req, res) => {
1028
+ const { provider } = req.params;
1029
+ const { name } = req.body;
1030
+
1031
+ const authConfig = loadAuthConfig();
1032
+ if (!authConfig || !authConfig[provider]) {
1033
+ return res.status(400).json({ error: `No active auth for ${provider} to save` });
1034
+ }
1035
+
1036
+ const profileName = name || getNextProfileName(provider);
1037
+ const data = authConfig[provider];
1038
+
1039
+ try {
1040
+ saveAuthProfile(provider, profileName, data);
1041
+ setActiveProfile(provider, profileName);
1042
+ res.json({ success: true, name: profileName });
1043
+ } catch (err) {
1044
+ res.status(500).json({ error: 'Failed to save profile', details: err.message });
1045
+ }
1046
+ });
1047
+
1048
+ app.post('/api/auth/profiles/:provider/:name/activate', (req, res) => {
1049
+ const { provider, name } = req.params;
1050
+
1051
+ const profileData = loadAuthProfile(provider, name);
1052
+ if (!profileData) {
1053
+ return res.status(404).json({ error: 'Profile not found' });
1054
+ }
1055
+
1056
+ const authConfig = loadAuthConfig() || {};
1057
+ authConfig[provider] = profileData;
1058
+
1059
+ if (saveAuthConfig(authConfig)) {
1060
+ setActiveProfile(provider, name);
1061
+ res.json({ success: true });
1062
+ } else {
1063
+ res.status(500).json({ error: 'Failed to activate profile' });
1064
+ }
1065
+ });
1066
+
1067
+ app.delete('/api/auth/profiles/:provider/:name', (req, res) => {
1068
+ const { provider, name } = req.params;
1069
+
1070
+ if (deleteAuthProfile(provider, name)) {
1071
+ const activeProfiles = getActiveProfiles();
1072
+ if (activeProfiles[provider] === name) {
1073
+ const studioConfig = loadStudioConfig();
1074
+ delete studioConfig.activeProfiles[provider];
1075
+ saveStudioConfig(studioConfig);
1076
+ }
1077
+ res.json({ success: true });
1078
+ } else {
1079
+ res.status(404).json({ error: 'Profile not found' });
1080
+ }
1081
+ });
1082
+
1083
+ app.put('/api/auth/profiles/:provider/:name', (req, res) => {
1084
+ const { provider, name } = req.params;
1085
+ const { newName } = req.body;
1086
+
1087
+ if (!newName || newName === name) {
1088
+ return res.status(400).json({ error: 'New name is required and must be different' });
1089
+ }
1090
+
1091
+ const profileData = loadAuthProfile(provider, name);
1092
+ if (!profileData) {
1093
+ return res.status(404).json({ error: 'Profile not found' });
1094
+ }
1095
+
1096
+ const existingProfiles = listAuthProfiles(provider);
1097
+ if (existingProfiles.includes(newName)) {
1098
+ return res.status(400).json({ error: 'Profile name already exists' });
1099
+ }
1100
+
1101
+ try {
1102
+ saveAuthProfile(provider, newName, profileData);
1103
+ deleteAuthProfile(provider, name);
1104
+
1105
+ const activeProfiles = getActiveProfiles();
1106
+ if (activeProfiles[provider] === name) {
1107
+ setActiveProfile(provider, newName);
1108
+ }
1109
+
1110
+ res.json({ success: true, name: newName });
1111
+ } catch (err) {
1112
+ res.status(500).json({ error: 'Failed to rename profile', details: err.message });
1113
+ }
1114
+ });
1115
+
1116
+ app.listen(PORT, () => {
1117
+ console.log(`Server running on http://localhost:${PORT}`);
1118
+ });
package/package.json CHANGED
@@ -1,32 +1,32 @@
1
- {
2
- "name": "opencode-studio-server",
3
- "version": "1.0.5",
4
- "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
- "main": "index.js",
6
- "bin": {
7
- "opencode-studio-server": "cli.js"
8
- },
9
- "scripts": {
10
- "start": "node index.js",
11
- "register": "node register-protocol.js",
12
- "postinstall": "node register-protocol.js"
13
- },
14
- "keywords": [
15
- "opencode",
16
- "studio",
17
- "config",
18
- "manager"
19
- ],
20
- "author": "Microck",
21
- "license": "MIT",
22
- "repository": {
23
- "type": "git",
24
- "url": "git+https://github.com/Microck/opencode-studio.git"
25
- },
26
- "type": "commonjs",
27
- "dependencies": {
28
- "body-parser": "^2.2.1",
29
- "cors": "^2.8.5",
30
- "express": "^5.2.1"
31
- }
32
- }
1
+ {
2
+ "name": "opencode-studio-server",
3
+ "version": "1.0.7",
4
+ "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "opencode-studio-server": "cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js",
11
+ "register": "node register-protocol.js",
12
+ "postinstall": "node register-protocol.js"
13
+ },
14
+ "keywords": [
15
+ "opencode",
16
+ "studio",
17
+ "config",
18
+ "manager"
19
+ ],
20
+ "author": "Microck",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/Microck/opencode-studio.git"
25
+ },
26
+ "type": "commonjs",
27
+ "dependencies": {
28
+ "body-parser": "^2.2.1",
29
+ "cors": "^2.8.5",
30
+ "express": "^5.2.1"
31
+ }
32
+ }