@frumu/tandem-panel 0.4.42 → 0.4.44
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/.env.example +4 -0
- package/bin/setup.js +354 -0
- package/dist/assets/index-64klqeDg.css +1 -0
- package/dist/assets/{index-CGXrSEHn.js → index-CNKuA4rP.js} +57 -57
- package/dist/assets/{vendor-Dysht3Ll.js → vendor-C_Ph0JAk.js} +57 -57
- package/dist/index.html +3 -3
- package/package.json +3 -3
- package/server/routes/capabilities.js +2 -0
- package/dist/assets/index-0q5GV8Jr.css +0 -1
package/.env.example
CHANGED
|
@@ -23,6 +23,10 @@ TANDEM_CONTROL_PANEL_AUTO_START_ENGINE=1
|
|
|
23
23
|
TANDEM_STATE_DIR=
|
|
24
24
|
TANDEM_CONTROL_PANEL_STATE_DIR=
|
|
25
25
|
|
|
26
|
+
# Optional editable workspace root for the Files page workspace explorer.
|
|
27
|
+
# Hosted/provisioned installs mount this at /workspace/repos.
|
|
28
|
+
TANDEM_CONTROL_PANEL_WORKSPACE_ROOT=
|
|
29
|
+
|
|
26
30
|
# Engine API token used when control panel auto-starts a local engine.
|
|
27
31
|
# If unset, the panel generates one at startup and prints it in logs.
|
|
28
32
|
TANDEM_CONTROL_PANEL_ENGINE_TOKEN=tk_change_me
|
package/bin/setup.js
CHANGED
|
@@ -262,6 +262,7 @@ const SESSION_TTL_MS =
|
|
|
262
262
|
const FILES_ROOT = resolve(
|
|
263
263
|
process.env.TANDEM_CONTROL_PANEL_FILES_ROOT || resolveDefaultChannelUploadsRoot()
|
|
264
264
|
);
|
|
265
|
+
const WORKSPACE_FILES_ROOT_ENV = String(process.env.TANDEM_CONTROL_PANEL_WORKSPACE_ROOT || "").trim();
|
|
265
266
|
const MAX_UPLOAD_BYTES = Math.max(
|
|
266
267
|
1,
|
|
267
268
|
Number.parseInt(
|
|
@@ -1346,6 +1347,7 @@ async function getInstallProfile({ acaAvailable = false, acaReason = "" } = {})
|
|
|
1346
1347
|
acaAvailable,
|
|
1347
1348
|
});
|
|
1348
1349
|
const summary = summarizeControlPanelConfig(config);
|
|
1350
|
+
const workspaceFilesRoot = resolveWorkspaceFilesRoot();
|
|
1349
1351
|
return {
|
|
1350
1352
|
control_panel_mode: mode.mode,
|
|
1351
1353
|
control_panel_mode_source: mode.source,
|
|
@@ -1364,6 +1366,8 @@ async function getInstallProfile({ acaAvailable = false, acaReason = "" } = {})
|
|
|
1364
1366
|
hosted_release_version: String(summary.hosted?.release_version || "").trim(),
|
|
1365
1367
|
hosted_release_channel: String(summary.hosted?.release_channel || "").trim(),
|
|
1366
1368
|
hosted_update_policy: String(summary.hosted?.update_policy || "").trim(),
|
|
1369
|
+
workspace_files_root: workspaceFilesRoot || "",
|
|
1370
|
+
workspace_files_available: !!workspaceFilesRoot,
|
|
1367
1371
|
aca_integration: !!acaAvailable,
|
|
1368
1372
|
aca_reason: acaReason || "",
|
|
1369
1373
|
};
|
|
@@ -1671,6 +1675,46 @@ function toSafeRelPath(raw, allowEmpty = true) {
|
|
|
1671
1675
|
return visible;
|
|
1672
1676
|
}
|
|
1673
1677
|
|
|
1678
|
+
function resolveWorkspaceFilesRoot() {
|
|
1679
|
+
const candidate = WORKSPACE_FILES_ROOT_ENV || (isHostedManagedControlPanel() ? "/workspace/repos" : "");
|
|
1680
|
+
if (!candidate) return null;
|
|
1681
|
+
return resolve(candidate);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function normalizeWorkspaceFilesPath(raw, allowEmpty = true) {
|
|
1685
|
+
const root = resolveWorkspaceFilesRoot();
|
|
1686
|
+
if (!root) return null;
|
|
1687
|
+
const input = String(raw || "")
|
|
1688
|
+
.trim()
|
|
1689
|
+
.replace(/\\/g, "/");
|
|
1690
|
+
if (!input) return allowEmpty ? "" : null;
|
|
1691
|
+
if (input.includes("\0")) return null;
|
|
1692
|
+
const parts = input.split("/").filter(Boolean);
|
|
1693
|
+
if (!parts.length) return allowEmpty ? "" : null;
|
|
1694
|
+
if (parts.some((part) => part === "." || part === "..")) return null;
|
|
1695
|
+
const full = input.startsWith("/") ? resolve(input) : resolve(root, input);
|
|
1696
|
+
if (full !== root && !full.startsWith(`${root}/`)) return null;
|
|
1697
|
+
const rel = relative(root, full).replace(/\\/g, "/");
|
|
1698
|
+
if (!rel) return allowEmpty ? "" : null;
|
|
1699
|
+
return rel;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function workspaceRelToFullPath(relPath) {
|
|
1703
|
+
const root = resolveWorkspaceFilesRoot();
|
|
1704
|
+
if (!root) return null;
|
|
1705
|
+
const full = resolve(root, String(relPath || ""));
|
|
1706
|
+
if (full !== root && !full.startsWith(`${root}/`)) return null;
|
|
1707
|
+
return full;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function parentWorkspacePath(raw) {
|
|
1711
|
+
const normalized = normalizeWorkspaceFilesPath(raw, true);
|
|
1712
|
+
if (!normalized) return null;
|
|
1713
|
+
const idx = normalized.lastIndexOf("/");
|
|
1714
|
+
if (idx < 0) return "";
|
|
1715
|
+
return normalized.slice(0, idx);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1674
1718
|
function toSafeRelFileName(rawName) {
|
|
1675
1719
|
const cleaned = basename(String(rawName || "").trim()).replace(/[\0]/g, "");
|
|
1676
1720
|
if (!cleaned || cleaned === "." || cleaned === "..") return null;
|
|
@@ -1723,6 +1767,60 @@ async function ensureUniqueVisibleRelPath(visiblePath) {
|
|
|
1723
1767
|
}
|
|
1724
1768
|
}
|
|
1725
1769
|
|
|
1770
|
+
async function ensureUniqueWorkspaceRelPath(workspacePath) {
|
|
1771
|
+
const ext = extname(workspacePath);
|
|
1772
|
+
const stem = ext ? workspacePath.slice(0, -ext.length) : workspacePath;
|
|
1773
|
+
let candidate = workspacePath;
|
|
1774
|
+
let counter = 1;
|
|
1775
|
+
while (true) {
|
|
1776
|
+
const full = workspaceRelToFullPath(candidate);
|
|
1777
|
+
if (!full) throw new Error("Invalid workspace path.");
|
|
1778
|
+
try {
|
|
1779
|
+
await stat(full);
|
|
1780
|
+
counter += 1;
|
|
1781
|
+
candidate = `${stem}-${counter}${ext}`;
|
|
1782
|
+
} catch {
|
|
1783
|
+
return candidate;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function decodeHeaderValue(value) {
|
|
1789
|
+
const raw = Array.isArray(value) ? String(value[0] || "") : String(value || "");
|
|
1790
|
+
try {
|
|
1791
|
+
return decodeURIComponent(raw);
|
|
1792
|
+
} catch {
|
|
1793
|
+
return raw;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
async function workspaceFileEntry(relPath, info = null) {
|
|
1798
|
+
const full = workspaceRelToFullPath(relPath);
|
|
1799
|
+
if (!full) return null;
|
|
1800
|
+
const details = info || (await stat(full).catch(() => null));
|
|
1801
|
+
if (!details) return null;
|
|
1802
|
+
const name = basename(full);
|
|
1803
|
+
if (details.isDirectory()) {
|
|
1804
|
+
return {
|
|
1805
|
+
name,
|
|
1806
|
+
path: relPath,
|
|
1807
|
+
updatedAt: Number(details.mtimeMs || 0),
|
|
1808
|
+
previewKind: "directory",
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
if (!details.isFile()) return null;
|
|
1812
|
+
const mime = inferFileMime(relPath || name);
|
|
1813
|
+
return {
|
|
1814
|
+
name,
|
|
1815
|
+
path: relPath,
|
|
1816
|
+
size: Number(details.size || 0),
|
|
1817
|
+
updatedAt: Number(details.mtimeMs || 0),
|
|
1818
|
+
mime,
|
|
1819
|
+
previewKind: inferFilePreviewKind(relPath || name, mime),
|
|
1820
|
+
downloadUrl: `/api/workspace/files/download?path=${encodeURIComponent(relPath)}`,
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1726
1824
|
async function handleFilesApi(req, res, _session) {
|
|
1727
1825
|
const url = new URL(req.url, `http://127.0.0.1:${PORTAL_PORT}`);
|
|
1728
1826
|
const pathname = url.pathname;
|
|
@@ -2022,6 +2120,255 @@ async function handleFilesApi(req, res, _session) {
|
|
|
2022
2120
|
return false;
|
|
2023
2121
|
}
|
|
2024
2122
|
|
|
2123
|
+
async function handleWorkspaceFilesApi(req, res, _session) {
|
|
2124
|
+
const url = new URL(req.url, `http://127.0.0.1:${PORTAL_PORT}`);
|
|
2125
|
+
const pathname = url.pathname;
|
|
2126
|
+
const workspaceRoot = resolveWorkspaceFilesRoot();
|
|
2127
|
+
if (!workspaceRoot) {
|
|
2128
|
+
sendJson(res, 503, {
|
|
2129
|
+
ok: false,
|
|
2130
|
+
error: "Workspace files root is not configured.",
|
|
2131
|
+
});
|
|
2132
|
+
return true;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
if (pathname === "/api/workspace/files/list" && req.method === "GET") {
|
|
2136
|
+
const dirRel = normalizeWorkspaceFilesPath(url.searchParams.get("dir") || "", true);
|
|
2137
|
+
if (dirRel === null) {
|
|
2138
|
+
sendJson(res, 400, { ok: false, error: "Invalid workspace directory path." });
|
|
2139
|
+
return true;
|
|
2140
|
+
}
|
|
2141
|
+
try {
|
|
2142
|
+
await mkdir(workspaceRoot, { recursive: true });
|
|
2143
|
+
const dirFull = workspaceRelToFullPath(dirRel);
|
|
2144
|
+
if (!dirFull) throw new Error("Invalid workspace directory path.");
|
|
2145
|
+
const info = await stat(dirFull);
|
|
2146
|
+
if (!info.isDirectory()) throw new Error("Workspace path is not a directory.");
|
|
2147
|
+
const entries = await readdir(dirFull, { withFileTypes: true });
|
|
2148
|
+
const directories = [];
|
|
2149
|
+
const files = [];
|
|
2150
|
+
for (const entry of entries) {
|
|
2151
|
+
const rel = dirRel ? `${dirRel}/${entry.name}` : entry.name;
|
|
2152
|
+
const childFull = workspaceRelToFullPath(rel);
|
|
2153
|
+
if (!childFull) continue;
|
|
2154
|
+
const childInfo = await stat(childFull).catch(() => null);
|
|
2155
|
+
if (!childInfo) continue;
|
|
2156
|
+
const row = await workspaceFileEntry(rel, childInfo);
|
|
2157
|
+
if (!row) continue;
|
|
2158
|
+
if (entry.isDirectory()) directories.push(row);
|
|
2159
|
+
else if (entry.isFile()) files.push(row);
|
|
2160
|
+
}
|
|
2161
|
+
directories.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
2162
|
+
files.sort(
|
|
2163
|
+
(a, b) => Number(b.updatedAt || 0) - Number(a.updatedAt || 0) || String(a.name).localeCompare(String(b.name))
|
|
2164
|
+
);
|
|
2165
|
+
sendJson(res, 200, {
|
|
2166
|
+
ok: true,
|
|
2167
|
+
root: workspaceRoot,
|
|
2168
|
+
dir: dirRel,
|
|
2169
|
+
parent: parentWorkspacePath(dirRel),
|
|
2170
|
+
directories,
|
|
2171
|
+
files,
|
|
2172
|
+
});
|
|
2173
|
+
} catch (e) {
|
|
2174
|
+
sendJson(res, 404, { ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
2175
|
+
}
|
|
2176
|
+
return true;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
if (pathname === "/api/workspace/files/upload" && req.method === "POST") {
|
|
2180
|
+
const rawName = decodeHeaderValue(req.headers["x-file-name"]);
|
|
2181
|
+
const rawRelativePath = decodeHeaderValue(req.headers["x-relative-path"]);
|
|
2182
|
+
const uploadPathRaw = String(rawRelativePath || rawName || "").trim();
|
|
2183
|
+
if (!uploadPathRaw || uploadPathRaw.startsWith("/") || uploadPathRaw.includes("\0")) {
|
|
2184
|
+
sendJson(res, 400, { ok: false, error: "Missing or invalid upload path." });
|
|
2185
|
+
return true;
|
|
2186
|
+
}
|
|
2187
|
+
const uploadRel = normalizeWorkspaceFilesPath(uploadPathRaw, false);
|
|
2188
|
+
const dirRel = normalizeWorkspaceFilesPath(url.searchParams.get("dir") || "", true);
|
|
2189
|
+
if (uploadRel === null || dirRel === null) {
|
|
2190
|
+
sendJson(res, 400, { ok: false, error: "Invalid upload path." });
|
|
2191
|
+
return true;
|
|
2192
|
+
}
|
|
2193
|
+
const relRaw = dirRel ? `${dirRel}/${uploadRel}` : uploadRel;
|
|
2194
|
+
let relPath = normalizeWorkspaceFilesPath(relRaw, false);
|
|
2195
|
+
if (!relPath) {
|
|
2196
|
+
sendJson(res, 400, { ok: false, error: "Invalid upload path." });
|
|
2197
|
+
return true;
|
|
2198
|
+
}
|
|
2199
|
+
try {
|
|
2200
|
+
await mkdir(workspaceRoot, { recursive: true });
|
|
2201
|
+
relPath = await ensureUniqueWorkspaceRelPath(relPath);
|
|
2202
|
+
const fullPath = workspaceRelToFullPath(relPath);
|
|
2203
|
+
if (!fullPath) throw new Error("Invalid upload path.");
|
|
2204
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
2205
|
+
let bytes = 0;
|
|
2206
|
+
const guard = new Transform({
|
|
2207
|
+
transform(chunk, _enc, cb) {
|
|
2208
|
+
bytes += chunk.length;
|
|
2209
|
+
if (bytes > MAX_UPLOAD_BYTES) {
|
|
2210
|
+
cb(new Error(`Upload exceeds limit of ${MAX_UPLOAD_BYTES} bytes.`));
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
cb(null, chunk);
|
|
2214
|
+
},
|
|
2215
|
+
});
|
|
2216
|
+
await pipeline(req, guard, createWriteStream(fullPath, { flags: "wx" }));
|
|
2217
|
+
const meta = await stat(fullPath);
|
|
2218
|
+
const mime = inferFileMime(relPath);
|
|
2219
|
+
sendJson(res, 200, {
|
|
2220
|
+
ok: true,
|
|
2221
|
+
root: workspaceRoot,
|
|
2222
|
+
name: basename(fullPath),
|
|
2223
|
+
path: relPath,
|
|
2224
|
+
absPath: fullPath,
|
|
2225
|
+
size: meta.size,
|
|
2226
|
+
mime,
|
|
2227
|
+
previewKind: inferFilePreviewKind(relPath, mime),
|
|
2228
|
+
downloadUrl: `/api/workspace/files/download?path=${encodeURIComponent(relPath)}`,
|
|
2229
|
+
});
|
|
2230
|
+
} catch (e) {
|
|
2231
|
+
if (e && typeof e === "object" && "code" in e && e.code === "EEXIST") {
|
|
2232
|
+
sendJson(res, 409, { ok: false, error: "File already exists." });
|
|
2233
|
+
} else {
|
|
2234
|
+
sendJson(res, 500, { ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return true;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
if (pathname === "/api/workspace/files/read" && req.method === "GET") {
|
|
2241
|
+
const rel = normalizeWorkspaceFilesPath(url.searchParams.get("path") || "", false);
|
|
2242
|
+
if (!rel) {
|
|
2243
|
+
sendJson(res, 400, { ok: false, error: "Missing or invalid workspace file path." });
|
|
2244
|
+
return true;
|
|
2245
|
+
}
|
|
2246
|
+
const full = workspaceRelToFullPath(rel);
|
|
2247
|
+
if (!full) {
|
|
2248
|
+
sendJson(res, 400, { ok: false, error: "Invalid workspace file path." });
|
|
2249
|
+
return true;
|
|
2250
|
+
}
|
|
2251
|
+
try {
|
|
2252
|
+
const info = await stat(full);
|
|
2253
|
+
if (!info.isFile()) throw new Error("Not a file");
|
|
2254
|
+
const mime = inferFileMime(rel);
|
|
2255
|
+
const previewKind = inferFilePreviewKind(rel, mime);
|
|
2256
|
+
const previewable = ["text", "markdown", "json", "yaml"].includes(previewKind);
|
|
2257
|
+
if (!previewable || info.size > MAX_PREVIEW_BYTES) {
|
|
2258
|
+
sendJson(res, 200, {
|
|
2259
|
+
ok: true,
|
|
2260
|
+
root: workspaceRoot,
|
|
2261
|
+
path: rel,
|
|
2262
|
+
absPath: full,
|
|
2263
|
+
name: basename(full),
|
|
2264
|
+
size: info.size,
|
|
2265
|
+
mime,
|
|
2266
|
+
previewKind,
|
|
2267
|
+
previewable: false,
|
|
2268
|
+
reason: !previewable ? "not_previewable" : "too_large",
|
|
2269
|
+
downloadUrl: `/api/workspace/files/download?path=${encodeURIComponent(rel)}`,
|
|
2270
|
+
});
|
|
2271
|
+
return true;
|
|
2272
|
+
}
|
|
2273
|
+
const text = await readFile(full, "utf8");
|
|
2274
|
+
sendJson(res, 200, {
|
|
2275
|
+
ok: true,
|
|
2276
|
+
root: workspaceRoot,
|
|
2277
|
+
path: rel,
|
|
2278
|
+
absPath: full,
|
|
2279
|
+
name: basename(full),
|
|
2280
|
+
size: info.size,
|
|
2281
|
+
mime,
|
|
2282
|
+
previewKind,
|
|
2283
|
+
previewable: true,
|
|
2284
|
+
downloadUrl: `/api/workspace/files/download?path=${encodeURIComponent(rel)}`,
|
|
2285
|
+
text,
|
|
2286
|
+
});
|
|
2287
|
+
} catch {
|
|
2288
|
+
sendJson(res, 404, { ok: false, error: "File not found." });
|
|
2289
|
+
}
|
|
2290
|
+
return true;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
if (pathname === "/api/workspace/files/mkdir" && req.method === "POST") {
|
|
2294
|
+
try {
|
|
2295
|
+
const body = await readJsonBody(req);
|
|
2296
|
+
const rel = normalizeWorkspaceFilesPath(body?.path || "", false);
|
|
2297
|
+
if (!rel) {
|
|
2298
|
+
sendJson(res, 400, { ok: false, error: "Missing or invalid workspace directory path." });
|
|
2299
|
+
return true;
|
|
2300
|
+
}
|
|
2301
|
+
const full = workspaceRelToFullPath(rel);
|
|
2302
|
+
if (!full) throw new Error("Invalid workspace directory path.");
|
|
2303
|
+
await mkdir(full, { recursive: true });
|
|
2304
|
+
const info = await stat(full);
|
|
2305
|
+
sendJson(res, 200, {
|
|
2306
|
+
ok: true,
|
|
2307
|
+
root: workspaceRoot,
|
|
2308
|
+
path: rel,
|
|
2309
|
+
absPath: full,
|
|
2310
|
+
updatedAt: info.mtimeMs,
|
|
2311
|
+
previewKind: "directory",
|
|
2312
|
+
});
|
|
2313
|
+
} catch (e) {
|
|
2314
|
+
sendJson(res, 500, { ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
2315
|
+
}
|
|
2316
|
+
return true;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
if (pathname === "/api/workspace/files/download" && req.method === "GET") {
|
|
2320
|
+
const rel = normalizeWorkspaceFilesPath(url.searchParams.get("path") || "", false);
|
|
2321
|
+
if (!rel) {
|
|
2322
|
+
sendJson(res, 400, { ok: false, error: "Missing or invalid workspace file path." });
|
|
2323
|
+
return true;
|
|
2324
|
+
}
|
|
2325
|
+
const full = workspaceRelToFullPath(rel);
|
|
2326
|
+
if (!full) {
|
|
2327
|
+
sendJson(res, 400, { ok: false, error: "Invalid workspace file path." });
|
|
2328
|
+
return true;
|
|
2329
|
+
}
|
|
2330
|
+
try {
|
|
2331
|
+
const info = await stat(full);
|
|
2332
|
+
if (!info.isFile()) throw new Error("Not a file");
|
|
2333
|
+
const mime = inferFileMime(rel);
|
|
2334
|
+
res.writeHead(200, {
|
|
2335
|
+
"content-type": mime,
|
|
2336
|
+
"content-length": String(info.size),
|
|
2337
|
+
"content-disposition": `attachment; filename="${basename(full).replace(/"/g, "")}"`,
|
|
2338
|
+
});
|
|
2339
|
+
createReadStream(full).pipe(res);
|
|
2340
|
+
} catch {
|
|
2341
|
+
sendJson(res, 404, { ok: false, error: "File not found." });
|
|
2342
|
+
}
|
|
2343
|
+
return true;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
if (pathname === "/api/workspace/files/delete" && req.method === "POST") {
|
|
2347
|
+
try {
|
|
2348
|
+
const body = await readJsonBody(req);
|
|
2349
|
+
const rel = normalizeWorkspaceFilesPath(body?.path || "", false);
|
|
2350
|
+
if (!rel) {
|
|
2351
|
+
sendJson(res, 400, { ok: false, error: "Missing or invalid workspace file path." });
|
|
2352
|
+
return true;
|
|
2353
|
+
}
|
|
2354
|
+
const full = workspaceRelToFullPath(rel);
|
|
2355
|
+
if (!full) throw new Error("Invalid workspace file path.");
|
|
2356
|
+
const info = await stat(full);
|
|
2357
|
+
if (!info.isFile()) {
|
|
2358
|
+
sendJson(res, 400, { ok: false, error: "Only files can be deleted from this view." });
|
|
2359
|
+
return true;
|
|
2360
|
+
}
|
|
2361
|
+
await rm(full, { force: true });
|
|
2362
|
+
sendJson(res, 200, { ok: true, root: workspaceRoot, path: rel });
|
|
2363
|
+
} catch (e) {
|
|
2364
|
+
sendJson(res, 500, { ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
2365
|
+
}
|
|
2366
|
+
return true;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
return false;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2025
2372
|
async function handleAuthLogin(req, res) {
|
|
2026
2373
|
try {
|
|
2027
2374
|
const body = await readJsonBody(req);
|
|
@@ -5392,6 +5739,12 @@ async function handleApi(req, res) {
|
|
|
5392
5739
|
return handleFilesApi(req, res, session);
|
|
5393
5740
|
}
|
|
5394
5741
|
|
|
5742
|
+
if (pathname.startsWith("/api/workspace/files")) {
|
|
5743
|
+
const session = requireSession(req, res);
|
|
5744
|
+
if (!session) return true;
|
|
5745
|
+
return handleWorkspaceFilesApi(req, res, session);
|
|
5746
|
+
}
|
|
5747
|
+
|
|
5395
5748
|
if (pathname.startsWith("/api/engine")) {
|
|
5396
5749
|
const session = requireSession(req, res);
|
|
5397
5750
|
if (!session) return true;
|
|
@@ -5499,6 +5852,7 @@ async function main() {
|
|
|
5499
5852
|
log(`Engine mode: ${isLocalEngineUrl(ENGINE_URL) ? "local" : "remote"}`);
|
|
5500
5853
|
log(`Files root: ${FILES_ROOT}`);
|
|
5501
5854
|
log(`Files buckets: uploads, artifacts, exports`);
|
|
5855
|
+
log(`Workspace root:${resolveWorkspaceFilesRoot() || "not configured"}`);
|
|
5502
5856
|
log(`Build: ${CONTROL_PANEL_BUILD_FINGERPRINT}`);
|
|
5503
5857
|
log("=========================================");
|
|
5504
5858
|
});
|