opencode-studio-server 1.0.5 → 1.0.6

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 +375 -381
  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,45 +810,45 @@ 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
- });
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
852
 
853
853
  app.post('/api/auth/login', (req, res) => {
854
854
  const { provider } = req.body;
@@ -862,22 +862,16 @@ app.post('/api/auth/login', (req, res) => {
862
862
  }
863
863
 
864
864
  // Run opencode auth login - this opens browser
865
- // Use detached + shell to ensure browser opens even when server runs headless
866
865
  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
- });
873
-
874
- child.unref(); // Allow server to exit independently
866
+ const command = isWindows
867
+ ? `start "" opencode auth login ${provider}`
868
+ : `opencode auth login ${provider}`;
875
869
 
876
- child.on('error', (err) => {
877
- console.error('Failed to start auth login:', err);
870
+ exec(command, (err) => {
871
+ if (err) console.error('Failed to start auth login:', err);
878
872
  });
879
-
880
- // Return immediately - login happens in browser
873
+
874
+ // Return immediately - login happens in browser
881
875
  res.json({
882
876
  success: true,
883
877
  message: `Opening browser for ${provider} login...`,
@@ -925,203 +919,203 @@ app.get('/api/auth/providers', (req, res) => {
925
919
  res.json(providers);
926
920
  });
927
921
 
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
- });
922
+ const AUTH_PROFILES_DIR = path.join(HOME_DIR, '.config', 'opencode-studio', 'auth-profiles');
923
+
924
+ function ensureAuthProfilesDir() {
925
+ if (!fs.existsSync(AUTH_PROFILES_DIR)) {
926
+ fs.mkdirSync(AUTH_PROFILES_DIR, { recursive: true });
927
+ }
928
+ }
929
+
930
+ function getProviderProfilesDir(provider) {
931
+ return path.join(AUTH_PROFILES_DIR, provider);
932
+ }
933
+
934
+ function listAuthProfiles(provider) {
935
+ const dir = getProviderProfilesDir(provider);
936
+ if (!fs.existsSync(dir)) return [];
937
+
938
+ try {
939
+ return fs.readdirSync(dir)
940
+ .filter(f => f.endsWith('.json'))
941
+ .map(f => f.replace('.json', ''));
942
+ } catch {
943
+ return [];
944
+ }
945
+ }
946
+
947
+ function getNextProfileName(provider) {
948
+ const existing = listAuthProfiles(provider);
949
+ let num = 1;
950
+ while (existing.includes(`account-${num}`)) {
951
+ num++;
952
+ }
953
+ return `account-${num}`;
954
+ }
955
+
956
+ function saveAuthProfile(provider, profileName, data) {
957
+ ensureAuthProfilesDir();
958
+ const dir = getProviderProfilesDir(provider);
959
+ if (!fs.existsSync(dir)) {
960
+ fs.mkdirSync(dir, { recursive: true });
961
+ }
962
+ const filePath = path.join(dir, `${profileName}.json`);
963
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
964
+ return true;
965
+ }
966
+
967
+ function loadAuthProfile(provider, profileName) {
968
+ const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
969
+ if (!fs.existsSync(filePath)) return null;
970
+ try {
971
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
972
+ } catch {
973
+ return null;
974
+ }
975
+ }
976
+
977
+ function deleteAuthProfile(provider, profileName) {
978
+ const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
979
+ if (fs.existsSync(filePath)) {
980
+ fs.unlinkSync(filePath);
981
+ return true;
982
+ }
983
+ return false;
984
+ }
985
+
986
+ function getActiveProfiles() {
987
+ const studioConfig = loadStudioConfig();
988
+ return studioConfig.activeProfiles || {};
989
+ }
990
+
991
+ function setActiveProfile(provider, profileName) {
992
+ const studioConfig = loadStudioConfig();
993
+ studioConfig.activeProfiles = studioConfig.activeProfiles || {};
994
+ studioConfig.activeProfiles[provider] = profileName;
995
+ saveStudioConfig(studioConfig);
996
+ }
997
+
998
+ app.get('/api/auth/profiles', (req, res) => {
999
+ ensureAuthProfilesDir();
1000
+ const activeProfiles = getActiveProfiles();
1001
+ const authConfig = loadAuthConfig() || {};
1002
+
1003
+ const profiles = {};
1004
+
1005
+ Object.keys(PROVIDER_DISPLAY_NAMES).forEach(provider => {
1006
+ const providerProfiles = listAuthProfiles(provider);
1007
+ profiles[provider] = {
1008
+ profiles: providerProfiles,
1009
+ active: activeProfiles[provider] || null,
1010
+ hasCurrentAuth: !!authConfig[provider],
1011
+ };
1012
+ });
1013
+
1014
+ res.json(profiles);
1015
+ });
1016
+
1017
+ app.get('/api/auth/profiles/:provider', (req, res) => {
1018
+ const { provider } = req.params;
1019
+ const providerProfiles = listAuthProfiles(provider);
1020
+ const activeProfiles = getActiveProfiles();
1021
+ const authConfig = loadAuthConfig() || {};
1022
+
1023
+ res.json({
1024
+ profiles: providerProfiles,
1025
+ active: activeProfiles[provider] || null,
1026
+ hasCurrentAuth: !!authConfig[provider],
1027
+ });
1028
+ });
1029
+
1030
+ app.post('/api/auth/profiles/:provider', (req, res) => {
1031
+ const { provider } = req.params;
1032
+ const { name } = req.body;
1033
+
1034
+ const authConfig = loadAuthConfig();
1035
+ if (!authConfig || !authConfig[provider]) {
1036
+ return res.status(400).json({ error: `No active auth for ${provider} to save` });
1037
+ }
1038
+
1039
+ const profileName = name || getNextProfileName(provider);
1040
+ const data = authConfig[provider];
1041
+
1042
+ try {
1043
+ saveAuthProfile(provider, profileName, data);
1044
+ setActiveProfile(provider, profileName);
1045
+ res.json({ success: true, name: profileName });
1046
+ } catch (err) {
1047
+ res.status(500).json({ error: 'Failed to save profile', details: err.message });
1048
+ }
1049
+ });
1050
+
1051
+ app.post('/api/auth/profiles/:provider/:name/activate', (req, res) => {
1052
+ const { provider, name } = req.params;
1053
+
1054
+ const profileData = loadAuthProfile(provider, name);
1055
+ if (!profileData) {
1056
+ return res.status(404).json({ error: 'Profile not found' });
1057
+ }
1058
+
1059
+ const authConfig = loadAuthConfig() || {};
1060
+ authConfig[provider] = profileData;
1061
+
1062
+ if (saveAuthConfig(authConfig)) {
1063
+ setActiveProfile(provider, name);
1064
+ res.json({ success: true });
1065
+ } else {
1066
+ res.status(500).json({ error: 'Failed to activate profile' });
1067
+ }
1068
+ });
1069
+
1070
+ app.delete('/api/auth/profiles/:provider/:name', (req, res) => {
1071
+ const { provider, name } = req.params;
1072
+
1073
+ if (deleteAuthProfile(provider, name)) {
1074
+ const activeProfiles = getActiveProfiles();
1075
+ if (activeProfiles[provider] === name) {
1076
+ const studioConfig = loadStudioConfig();
1077
+ delete studioConfig.activeProfiles[provider];
1078
+ saveStudioConfig(studioConfig);
1079
+ }
1080
+ res.json({ success: true });
1081
+ } else {
1082
+ res.status(404).json({ error: 'Profile not found' });
1083
+ }
1084
+ });
1085
+
1086
+ app.put('/api/auth/profiles/:provider/:name', (req, res) => {
1087
+ const { provider, name } = req.params;
1088
+ const { newName } = req.body;
1089
+
1090
+ if (!newName || newName === name) {
1091
+ return res.status(400).json({ error: 'New name is required and must be different' });
1092
+ }
1093
+
1094
+ const profileData = loadAuthProfile(provider, name);
1095
+ if (!profileData) {
1096
+ return res.status(404).json({ error: 'Profile not found' });
1097
+ }
1098
+
1099
+ const existingProfiles = listAuthProfiles(provider);
1100
+ if (existingProfiles.includes(newName)) {
1101
+ return res.status(400).json({ error: 'Profile name already exists' });
1102
+ }
1103
+
1104
+ try {
1105
+ saveAuthProfile(provider, newName, profileData);
1106
+ deleteAuthProfile(provider, name);
1107
+
1108
+ const activeProfiles = getActiveProfiles();
1109
+ if (activeProfiles[provider] === name) {
1110
+ setActiveProfile(provider, newName);
1111
+ }
1112
+
1113
+ res.json({ success: true, name: newName });
1114
+ } catch (err) {
1115
+ res.status(500).json({ error: 'Failed to rename profile', details: err.message });
1116
+ }
1117
+ });
1118
+
1119
+ app.listen(PORT, () => {
1120
+ console.log(`Server running on http://localhost:${PORT}`);
1121
+ });
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.6",
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
+ }