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.
- package/index.js +386 -395
- 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
|
-
|
|
857
|
-
|
|
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 (!
|
|
861
|
-
return res.
|
|
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
|
-
|
|
865
|
-
|
|
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
|
|
868
|
-
detached: true,
|
|
869
|
-
shell: isWindows,
|
|
870
|
-
stdio: 'ignore',
|
|
871
|
-
windowsHide: false,
|
|
872
|
-
});
|
|
857
|
+
const isMac = process.platform === 'darwin';
|
|
873
858
|
|
|
874
|
-
|
|
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
|
-
|
|
877
|
-
console.error('Failed to
|
|
868
|
+
exec(command, (err) => {
|
|
869
|
+
if (err) console.error('Failed to open terminal:', err);
|
|
878
870
|
});
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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.
|
|
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
|
+
}
|