@openchamber/web 1.11.5 → 1.11.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/README.md +6 -0
- package/bin/cli.js +443 -2
- package/dist/assets/{MarkdownRendererImpl-C3-ZpwEx.js → MarkdownRendererImpl-DaF15QNC.js} +1 -1
- package/dist/assets/{MultiRunWindow-BDfPzMDy.js → MultiRunWindow-Cl7wS_CB.js} +1 -1
- package/dist/assets/{OnboardingScreen-DGgh4IXB.js → OnboardingScreen-DTv6YJI1.js} +2 -2
- package/dist/assets/{SettingsWindow-B8QKr5dB.js → SettingsWindow-_c3TTL2z.js} +1 -1
- package/dist/assets/{TerminalView-D7IIkSGJ.js → TerminalView-CuXkDROt.js} +4 -4
- package/dist/assets/es-CYoUf2D-.js +15 -0
- package/dist/assets/{index-DHluop4D.js → index-3WXrN3AX.js} +1 -1
- package/dist/assets/index-BREIbhcb.css +1 -0
- package/dist/assets/ko-2tM0fIna.js +15 -0
- package/dist/assets/main-BF3kWAJ9.js +239 -0
- package/dist/assets/{main-VVcyjpiF.js → main-o8ZERrmU.js} +2 -2
- package/dist/assets/miniChat-BZQjpK23.js +2 -0
- package/dist/assets/{modelPrefsAutoSave-Ctdc3cCY.js → modelPrefsAutoSave-wwnbqBk7.js} +109 -107
- package/dist/assets/pl-Dq8uAotM.js +15 -0
- package/dist/assets/pt-BR-nh9s9DFT.js +15 -0
- package/dist/assets/{renderElectronMiniChatApp-CsddCM3q.js → renderElectronMiniChatApp-C-Ezew9P.js} +2 -2
- package/dist/assets/uk-BZtz0wUV.js +15 -0
- package/dist/assets/{vendor-.bun-Bum-iBXX.js → vendor-.bun-CV3tusA8.js} +1 -1
- package/dist/assets/zh-CN-j_nYMchE.js +15 -0
- package/dist/assets/zh-TW-B11UpkDJ.js +15 -0
- package/dist/index.html +11 -28
- package/dist/mini-chat.html +4 -4
- package/package.json +1 -1
- package/server/index.js +2 -0
- package/server/lib/cloudflare-tunnel.js +3 -5
- package/server/lib/fs/routes.js +5 -0
- package/server/lib/fs/routes.test.js +61 -1
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +82 -1
- package/server/lib/git/service.js +338 -19
- package/server/lib/git/service.test.js +414 -8
- package/server/lib/ngrok-tunnel.js +209 -0
- package/server/lib/opencode/core-routes.js +1 -0
- package/server/lib/opencode/env-runtime.js +52 -4
- package/server/lib/opencode/env-runtime.test.js +82 -6
- package/server/lib/opencode/feature-routes-runtime.js +35 -0
- package/server/lib/opencode/index.js +19 -0
- package/server/lib/opencode/npm-registry.js +157 -0
- package/server/lib/opencode/npm-registry.test.js +179 -0
- package/server/lib/opencode/openchamber-routes.js +9 -7
- package/server/lib/opencode/plugin-routes.js +373 -0
- package/server/lib/opencode/plugin-routes.test.js +384 -0
- package/server/lib/opencode/plugin-spec.js +107 -0
- package/server/lib/opencode/plugin-spec.test.js +154 -0
- package/server/lib/opencode/plugins.js +393 -0
- package/server/lib/opencode/plugins.test.js +176 -0
- package/server/lib/opencode/settings-helpers.js +6 -0
- package/server/lib/opencode/settings-helpers.test.js +11 -0
- package/server/lib/opencode/settings-runtime.js +39 -1
- package/server/lib/opencode/settings-runtime.test.js +39 -0
- package/server/lib/skills-catalog/source.js +1 -1
- package/server/lib/tunnels/DOCUMENTATION.md +1 -0
- package/server/lib/tunnels/providers/ngrok.js +117 -0
- package/server/lib/tunnels/types.js +2 -0
- package/dist/assets/es-dIVpApmS.js +0 -15
- package/dist/assets/index-Bk9IWJe1.css +0 -1
- package/dist/assets/ko-Cqf3E9-d.js +0 -15
- package/dist/assets/main-D45l3Dxw.js +0 -232
- package/dist/assets/miniChat-a9w7WM0c.js +0 -2
- package/dist/assets/pl-C577DpsX.js +0 -15
- package/dist/assets/pt-BR-BeeF6VlK.js +0 -15
- package/dist/assets/uk-CZ7XVz_D.js +0 -15
- package/dist/assets/zh-CN-BMSSqdyO.js +0 -15
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
2
2
|
const {
|
|
3
3
|
fs,
|
|
4
|
-
os,
|
|
5
4
|
path,
|
|
6
5
|
process,
|
|
7
6
|
server,
|
|
@@ -104,14 +103,15 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
const currentPort = server.address()?.port || 3000;
|
|
107
|
-
const
|
|
108
|
-
const instanceFilePath = path.join(tmpDir, `openchamber-${currentPort}.json`);
|
|
106
|
+
const instanceFilePath = path.join(openchamberDataDir, 'run', `openchamber-${currentPort}.json`);
|
|
109
107
|
let storedOptions = { port: currentPort, daemon: true };
|
|
110
108
|
try {
|
|
111
109
|
const content = await fs.promises.readFile(instanceFilePath, 'utf8');
|
|
112
110
|
storedOptions = JSON.parse(content);
|
|
113
111
|
} catch {
|
|
114
112
|
}
|
|
113
|
+
const launchMode = storedOptions.launchMode === 'foreground' ? 'foreground' : 'daemon';
|
|
114
|
+
const isForegroundService = launchMode === 'foreground';
|
|
115
115
|
|
|
116
116
|
const isWindows = process.platform === 'win32';
|
|
117
117
|
const quotePosix = (value) => `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
@@ -152,7 +152,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
152
152
|
restartCmdFallback += ` --ui-password '${escapedPw}'`;
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
-
const restartCmd = `(${restartCmdPrimary}) || (${restartCmdFallback})`;
|
|
155
|
+
const restartCmd = isForegroundService ? '' : `(${restartCmdPrimary}) || (${restartCmdFallback})`;
|
|
156
156
|
const updateLogPath = path.join(openchamberDataDir, 'update-install.log');
|
|
157
157
|
const logPreamble = [
|
|
158
158
|
'',
|
|
@@ -165,8 +165,9 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
165
165
|
`packagePath=${pmDetails.packagePath || 'unknown'}`,
|
|
166
166
|
`globalNodeModulesRoot=${pmDetails.globalNodeModulesRoot || 'unknown'}`,
|
|
167
167
|
`mode=${isContainer ? 'container' : 'restart'}`,
|
|
168
|
+
`launchMode=${launchMode}`,
|
|
168
169
|
`updateCommand=${updateCmd}`,
|
|
169
|
-
`restartCommand=${restartCmd}`,
|
|
170
|
+
`restartCommand=${restartCmd || 'service-manager'}`,
|
|
170
171
|
`logPath=${updateLogPath}`,
|
|
171
172
|
].join('\n');
|
|
172
173
|
|
|
@@ -176,6 +177,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
176
177
|
version: updateInfo.version,
|
|
177
178
|
packageManager: pm,
|
|
178
179
|
autoRestart: true,
|
|
180
|
+
restartManager: isForegroundService ? 'service' : 'cli',
|
|
179
181
|
});
|
|
180
182
|
|
|
181
183
|
setTimeout(() => {
|
|
@@ -192,7 +194,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
192
194
|
${updateCmd}
|
|
193
195
|
if %ERRORLEVEL% EQU 0 (
|
|
194
196
|
echo Update successful, restarting OpenChamber...
|
|
195
|
-
${restartCmd}
|
|
197
|
+
${restartCmd || 'echo Service manager will restart OpenChamber.'}
|
|
196
198
|
) else (
|
|
197
199
|
echo Update failed
|
|
198
200
|
exit /b 1
|
|
@@ -204,7 +206,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
204
206
|
${updateCmd}
|
|
205
207
|
if [ $? -eq 0 ]; then
|
|
206
208
|
echo "Update successful, restarting OpenChamber..."
|
|
207
|
-
${restartCmd}
|
|
209
|
+
${restartCmd || 'echo "Service manager will restart OpenChamber."'}
|
|
208
210
|
else
|
|
209
211
|
echo "Update failed"
|
|
210
212
|
exit 1
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
import { getNpmInfo as defaultGetNpmInfo } from './npm-registry.js';
|
|
5
|
+
import { isExactSemver as defaultIsExactSemver, isPathSpec as defaultIsPathSpec, parseNpmSpec as defaultParseNpmSpec, parsePathSpec as defaultParsePathSpec } from './plugin-spec.js';
|
|
6
|
+
|
|
7
|
+
const ENTRY_EXISTS_CODES = new Set(['ENTRY_EXISTS', 'EEXIST']);
|
|
8
|
+
const FILE_EXISTS_CODES = new Set(['FILE_EXISTS', 'EEXIST']);
|
|
9
|
+
const NOT_FOUND_CODES = new Set(['NOT_FOUND', 'ENOENT']);
|
|
10
|
+
const BAD_REQUEST_CODES = new Set(['INVALID_FILENAME', 'INVALID_SCOPE', 'INVALID_SPEC', 'EINVAL']);
|
|
11
|
+
|
|
12
|
+
export const registerPluginRoutes = (app, dependencies) => {
|
|
13
|
+
const {
|
|
14
|
+
resolveOptionalProjectDirectory,
|
|
15
|
+
refreshOpenCodeAfterConfigChange,
|
|
16
|
+
clientReloadDelayMs,
|
|
17
|
+
listPluginEntries,
|
|
18
|
+
getPluginEntry,
|
|
19
|
+
createPluginEntry,
|
|
20
|
+
updatePluginEntry,
|
|
21
|
+
deletePluginEntry,
|
|
22
|
+
listPluginDirFiles,
|
|
23
|
+
readPluginDirFile,
|
|
24
|
+
writePluginDirFile,
|
|
25
|
+
deletePluginDirFile,
|
|
26
|
+
encodePluginId,
|
|
27
|
+
decodePluginId,
|
|
28
|
+
getNpmInfo = defaultGetNpmInfo,
|
|
29
|
+
parseNpmSpec = defaultParseNpmSpec,
|
|
30
|
+
parsePathSpec = defaultParsePathSpec,
|
|
31
|
+
isExactSemver = defaultIsExactSemver,
|
|
32
|
+
isPathSpec = defaultIsPathSpec,
|
|
33
|
+
} = dependencies;
|
|
34
|
+
|
|
35
|
+
const parsedKindForSpec = (spec) => (isPathSpec(spec) ? 'path' : 'npm');
|
|
36
|
+
|
|
37
|
+
const resolveDirectory = async (req, res) => {
|
|
38
|
+
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
39
|
+
if (error) {
|
|
40
|
+
res.status(400).json({ error });
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return directory || null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const successPayload = (message) => ({
|
|
47
|
+
success: true,
|
|
48
|
+
requiresReload: true,
|
|
49
|
+
message,
|
|
50
|
+
reloadDelayMs: clientReloadDelayMs,
|
|
51
|
+
reloadFailed: false,
|
|
52
|
+
warning: undefined,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const completePluginMutation = async (res, operation, _noun, applyChange) => {
|
|
56
|
+
applyChange();
|
|
57
|
+
|
|
58
|
+
const pastTense = operation.replace(/ion$/, 'ed').replace(/update$/, 'updated');
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await refreshOpenCodeAfterConfigChange(`plugin ${operation}`);
|
|
62
|
+
return res.json(successPayload(`Plugin ${pastTense}. Reloading interface…`));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(`[API:plugin ${operation}] Reload failed after config write:`, error);
|
|
65
|
+
return res.json({
|
|
66
|
+
success: true,
|
|
67
|
+
requiresReload: false,
|
|
68
|
+
message: `Plugin ${pastTense}, but OpenCode reload failed.`,
|
|
69
|
+
reloadDelayMs: clientReloadDelayMs,
|
|
70
|
+
reloadFailed: true,
|
|
71
|
+
warning: error.message || 'OpenCode reload failed after plugin config changed',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const validateEntryId = (id) => {
|
|
77
|
+
const decoded = decodePluginId(id);
|
|
78
|
+
if (decoded.prefix !== 'config') {
|
|
79
|
+
const error = new Error('Plugin entry not found');
|
|
80
|
+
error.code = 'NOT_FOUND';
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const validateFileId = (id) => {
|
|
86
|
+
const decoded = decodePluginId(id);
|
|
87
|
+
if (decoded.prefix !== 'file') {
|
|
88
|
+
const error = new Error('Plugin file not found');
|
|
89
|
+
error.code = 'NOT_FOUND';
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handlePluginError = (res, error, fallbackMessage, context, existsKind = null) => {
|
|
95
|
+
const code = error?.code;
|
|
96
|
+
if ((existsKind === 'entry' && ENTRY_EXISTS_CODES.has(code)) || (existsKind === 'file' && FILE_EXISTS_CODES.has(code))) {
|
|
97
|
+
return res.status(409).json({ error: error.message });
|
|
98
|
+
}
|
|
99
|
+
if (NOT_FOUND_CODES.has(code)) {
|
|
100
|
+
return res.status(404).json({ error: error.message });
|
|
101
|
+
}
|
|
102
|
+
if (BAD_REQUEST_CODES.has(code)) {
|
|
103
|
+
return res.status(400).json({ error: error.message });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.error(context, error);
|
|
107
|
+
return res.status(500).json({ error: fallbackMessage });
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
app.get('/api/config/plugins', async (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
const directory = await resolveDirectory(req, res);
|
|
113
|
+
if (directory === null && res.headersSent) return;
|
|
114
|
+
|
|
115
|
+
res.json({
|
|
116
|
+
entries: listPluginEntries(directory),
|
|
117
|
+
files: listPluginDirFiles(directory),
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('[API:GET /api/config/plugins] Failed:', error);
|
|
121
|
+
res.status(500).json({ error: 'Failed to list plugins' });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
app.get('/api/config/plugins/registry', async (req, res) => {
|
|
126
|
+
try {
|
|
127
|
+
const { directory, error: directoryError } = await resolveOptionalProjectDirectory(req);
|
|
128
|
+
if (directoryError) {
|
|
129
|
+
return res.status(400).json({ error: directoryError });
|
|
130
|
+
}
|
|
131
|
+
const rawSpecs = (req.query.specs || '').toString();
|
|
132
|
+
const specs = rawSpecs
|
|
133
|
+
? rawSpecs.split(',').map((spec) => {
|
|
134
|
+
try {
|
|
135
|
+
return decodeURIComponent(spec);
|
|
136
|
+
} catch {
|
|
137
|
+
return spec;
|
|
138
|
+
}
|
|
139
|
+
}).filter((spec) => spec.length > 0)
|
|
140
|
+
: [];
|
|
141
|
+
const uniqueSpecs = Array.from(new Set(specs));
|
|
142
|
+
if (uniqueSpecs.length > 100) {
|
|
143
|
+
return res.status(400).json({ error: 'too many specs' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const refresh = req.query.refresh === 'true';
|
|
147
|
+
const npmJobs = new Map();
|
|
148
|
+
const malformedSpecs = new Set();
|
|
149
|
+
|
|
150
|
+
for (const spec of uniqueSpecs) {
|
|
151
|
+
if (parsedKindForSpec(spec) !== 'npm') continue;
|
|
152
|
+
|
|
153
|
+
const parsed = parseNpmSpec(spec);
|
|
154
|
+
if (parsed.malformed) {
|
|
155
|
+
malformedSpecs.add(spec);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const job = npmJobs.get(parsed.name) || { specs: [], parsedBySpec: new Map() };
|
|
160
|
+
job.specs.push(spec);
|
|
161
|
+
job.parsedBySpec.set(spec, parsed);
|
|
162
|
+
npmJobs.set(parsed.name, job);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const npmInfoByName = new Map();
|
|
166
|
+
await Promise.all(Array.from(npmJobs.keys()).map(async (name) => {
|
|
167
|
+
npmInfoByName.set(name, await getNpmInfo(name, { forceRefresh: refresh }));
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
const results = [];
|
|
171
|
+
for (const spec of uniqueSpecs) {
|
|
172
|
+
if (malformedSpecs.has(spec)) {
|
|
173
|
+
results.push({ kind: 'npm-malformed', spec, error: 'Spec syntax is malformed' });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (parsedKindForSpec(spec) === 'path') {
|
|
178
|
+
const { absolutePath } = parsePathSpec(spec, { homedir: os.homedir(), cwd: directory || os.homedir() });
|
|
179
|
+
try {
|
|
180
|
+
fs.statSync(absolutePath);
|
|
181
|
+
} catch {
|
|
182
|
+
results.push({ kind: 'path-missing', spec, absolutePath });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
fs.accessSync(absolutePath, fs.constants.R_OK);
|
|
188
|
+
results.push({ kind: 'path-ok', spec, absolutePath });
|
|
189
|
+
} catch {
|
|
190
|
+
results.push({ kind: 'path-unreadable', spec, absolutePath });
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const parsed = parseNpmSpec(spec);
|
|
196
|
+
const info = npmInfoByName.get(parsed.name);
|
|
197
|
+
if (!info.ok) {
|
|
198
|
+
if (info.status === 404) {
|
|
199
|
+
results.push({ kind: 'npm-missing-package', spec, name: parsed.name, error: info.error });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
results.push({ kind: 'npm-network', spec, error: info.status === 'network' ? info.error : `Registry returned ${info.status}` });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const currentVersion = parsed.version;
|
|
208
|
+
if (currentVersion !== null && isExactSemver(currentVersion) && !info.versions.includes(currentVersion)) {
|
|
209
|
+
results.push({
|
|
210
|
+
kind: 'npm-missing-version',
|
|
211
|
+
spec,
|
|
212
|
+
name: parsed.name,
|
|
213
|
+
currentVersion,
|
|
214
|
+
latestVersion: info.latest,
|
|
215
|
+
versions: info.versions,
|
|
216
|
+
});
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
results.push({
|
|
221
|
+
kind: 'npm-ok',
|
|
222
|
+
spec,
|
|
223
|
+
name: parsed.name,
|
|
224
|
+
currentVersion,
|
|
225
|
+
latestVersion: info.latest,
|
|
226
|
+
versions: info.versions,
|
|
227
|
+
hasUpdate: currentVersion !== null && isExactSemver(currentVersion) && currentVersion !== info.latest,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return res.json({ results });
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error('[API:GET /api/config/plugins/registry]', error);
|
|
234
|
+
return res.status(500).json({ error: 'Failed to query npm registry' });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
app.get('/api/config/plugins/entry/:id', async (req, res) => {
|
|
239
|
+
try {
|
|
240
|
+
const directory = await resolveDirectory(req, res);
|
|
241
|
+
if (directory === null && res.headersSent) return;
|
|
242
|
+
validateEntryId(req.params.id);
|
|
243
|
+
|
|
244
|
+
const entry = getPluginEntry(req.params.id, directory);
|
|
245
|
+
if (!entry) {
|
|
246
|
+
return res.status(404).json({ error: 'Plugin entry not found' });
|
|
247
|
+
}
|
|
248
|
+
return res.json(entry);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return handlePluginError(res, error, 'Failed to get plugin entry', '[API:GET /api/config/plugins/entry/:id] Failed:');
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
app.post('/api/config/plugins/entry', async (req, res) => {
|
|
255
|
+
try {
|
|
256
|
+
const directory = await resolveDirectory(req, res);
|
|
257
|
+
if (directory === null && res.headersSent) return;
|
|
258
|
+
|
|
259
|
+
await completePluginMutation(res, 'entry creation', 'entry', () => {
|
|
260
|
+
createPluginEntry({
|
|
261
|
+
spec: req.body?.spec,
|
|
262
|
+
options: req.body?.options,
|
|
263
|
+
scope: req.body?.scope,
|
|
264
|
+
}, directory);
|
|
265
|
+
});
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return handlePluginError(res, error, 'Failed to create plugin entry', '[API:POST /api/config/plugins/entry] Failed:', 'entry');
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
app.patch('/api/config/plugins/entry/:id', async (req, res) => {
|
|
272
|
+
try {
|
|
273
|
+
const directory = await resolveDirectory(req, res);
|
|
274
|
+
if (directory === null && res.headersSent) return;
|
|
275
|
+
validateEntryId(req.params.id);
|
|
276
|
+
|
|
277
|
+
await completePluginMutation(res, 'entry update', 'entry', () => {
|
|
278
|
+
updatePluginEntry(req.params.id, {
|
|
279
|
+
spec: req.body?.spec,
|
|
280
|
+
options: req.body?.options,
|
|
281
|
+
}, directory);
|
|
282
|
+
});
|
|
283
|
+
} catch (error) {
|
|
284
|
+
return handlePluginError(res, error, 'Failed to update plugin entry', '[API:PATCH /api/config/plugins/entry/:id] Failed:', 'entry');
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
app.delete('/api/config/plugins/entry/:id', async (req, res) => {
|
|
289
|
+
try {
|
|
290
|
+
const directory = await resolveDirectory(req, res);
|
|
291
|
+
if (directory === null && res.headersSent) return;
|
|
292
|
+
validateEntryId(req.params.id);
|
|
293
|
+
|
|
294
|
+
await completePluginMutation(res, 'entry deletion', 'entry', () => {
|
|
295
|
+
deletePluginEntry(req.params.id, directory);
|
|
296
|
+
});
|
|
297
|
+
} catch (error) {
|
|
298
|
+
return handlePluginError(res, error, 'Failed to delete plugin entry', '[API:DELETE /api/config/plugins/entry/:id] Failed:', 'entry');
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
app.get('/api/config/plugins/file/:id', async (req, res) => {
|
|
303
|
+
try {
|
|
304
|
+
const directory = await resolveDirectory(req, res);
|
|
305
|
+
if (directory === null && res.headersSent) return;
|
|
306
|
+
validateFileId(req.params.id);
|
|
307
|
+
|
|
308
|
+
const file = readPluginDirFile(req.params.id, directory);
|
|
309
|
+
if (!file) {
|
|
310
|
+
return res.status(404).json({ error: 'Plugin file not found' });
|
|
311
|
+
}
|
|
312
|
+
return res.json(file);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return handlePluginError(res, error, 'Failed to read plugin file', '[API:GET /api/config/plugins/file/:id] Failed:');
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
app.post('/api/config/plugins/file', async (req, res) => {
|
|
319
|
+
try {
|
|
320
|
+
const directory = await resolveDirectory(req, res);
|
|
321
|
+
if (directory === null && res.headersSent) return;
|
|
322
|
+
const id = encodePluginId('file', `${req.body?.scope || 'user'}:${req.body?.fileName || ''}`);
|
|
323
|
+
|
|
324
|
+
await completePluginMutation(res, 'file creation', 'file', () => {
|
|
325
|
+
validateFileId(id);
|
|
326
|
+
writePluginDirFile({
|
|
327
|
+
fileName: req.body?.fileName,
|
|
328
|
+
content: req.body?.content,
|
|
329
|
+
scope: req.body?.scope,
|
|
330
|
+
}, directory);
|
|
331
|
+
});
|
|
332
|
+
} catch (error) {
|
|
333
|
+
return handlePluginError(res, error, 'Failed to create plugin file', '[API:POST /api/config/plugins/file] Failed:', 'file');
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
app.put('/api/config/plugins/file/:id', async (req, res) => {
|
|
338
|
+
try {
|
|
339
|
+
const directory = await resolveDirectory(req, res);
|
|
340
|
+
if (directory === null && res.headersSent) return;
|
|
341
|
+
validateFileId(req.params.id);
|
|
342
|
+
|
|
343
|
+
const existing = readPluginDirFile(req.params.id, directory);
|
|
344
|
+
if (!existing) {
|
|
345
|
+
return res.status(404).json({ error: 'Plugin file not found' });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await completePluginMutation(res, 'file update', 'file', () => {
|
|
349
|
+
writePluginDirFile({
|
|
350
|
+
fileName: existing.fileName,
|
|
351
|
+
content: req.body?.content,
|
|
352
|
+
scope: existing.scope,
|
|
353
|
+
}, directory, { overwrite: true });
|
|
354
|
+
});
|
|
355
|
+
} catch (error) {
|
|
356
|
+
return handlePluginError(res, error, 'Failed to update plugin file', '[API:PUT /api/config/plugins/file/:id] Failed:', 'file');
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
app.delete('/api/config/plugins/file/:id', async (req, res) => {
|
|
361
|
+
try {
|
|
362
|
+
const directory = await resolveDirectory(req, res);
|
|
363
|
+
if (directory === null && res.headersSent) return;
|
|
364
|
+
validateFileId(req.params.id);
|
|
365
|
+
|
|
366
|
+
await completePluginMutation(res, 'file deletion', 'file', () => {
|
|
367
|
+
deletePluginDirFile(req.params.id, directory);
|
|
368
|
+
});
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return handlePluginError(res, error, 'Failed to delete plugin file', '[API:DELETE /api/config/plugins/file/:id] Failed:', 'file');
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
};
|