svamp-cli 0.2.257 → 0.2.258

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 (26) hide show
  1. package/dist/{agentCommands-fCsOp7ko.mjs → agentCommands-CQ89BZon.mjs} +5 -5
  2. package/dist/{auth-BH6Awl3O.mjs → auth-CaFsKYbx.mjs} +1 -1
  3. package/dist/cli.mjs +61 -61
  4. package/dist/{commands-BpNDUqvv.mjs → commands--X8f4eSr.mjs} +24 -13
  5. package/dist/{commands-B5veY89L.mjs → commands-B6eGMF5H.mjs} +1 -1
  6. package/dist/{commands-BzF6GzdR.mjs → commands-BgX0W0uK.mjs} +1 -1
  7. package/dist/{commands-CE1PdEJJ.mjs → commands-D8l74hsL.mjs} +1 -1
  8. package/dist/{commands-BCmpKBjg.mjs → commands-Dd8xEIf0.mjs} +1 -1
  9. package/dist/{commands-DNIxtTXe.mjs → commands-DrfYv705.mjs} +2 -2
  10. package/dist/{commands-B6BkPRui.mjs → commands-DutfM3U7.mjs} +2 -2
  11. package/dist/{fleet-XYsLuf-3.mjs → fleet-UvQnIWT1.mjs} +1 -1
  12. package/dist/{frpc-QESvQN-s.mjs → frpc-IXxyfgPn.mjs} +1 -1
  13. package/dist/{headlessCli-DuCmcrAL.mjs → headlessCli-BK7WSS3D.mjs} +2 -2
  14. package/dist/index.mjs +1 -1
  15. package/dist/{package-BA3c2JLu.mjs → package-BdZX5mDv.mjs} +2 -2
  16. package/dist/{rpc-DCeDG4c7.mjs → rpc-CyACkgY1.mjs} +1 -1
  17. package/dist/{rpc-Bscxex4z.mjs → rpc-D7DB3XCH.mjs} +1 -1
  18. package/dist/{run-BtGnnEMI.mjs → run-C1Unk4Zx.mjs} +11 -11
  19. package/dist/{run-BmOCcZRG.mjs → run-C1ufZjIW.mjs} +1 -1
  20. package/dist/{scheduler-Dbqmnd_o.mjs → scheduler-7wve0Fy3.mjs} +1 -1
  21. package/dist/{serveCommands-DueTsJSe.mjs → serveCommands-iQJJLsMJ.mjs} +5 -5
  22. package/dist/{serveManager-BclzoPIb.mjs → serveManager-BszV0oIl.mjs} +62 -74
  23. package/dist/{sideband-C_ifGgde.mjs → sideband-DMPZ-ZoR.mjs} +1 -1
  24. package/dist/staticFileServer-FCEOLIYH.mjs +239 -0
  25. package/package.json +2 -2
  26. package/dist/caddy-CuTbE3NY.mjs +0 -322
@@ -0,0 +1,239 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ const MIME = {
5
+ ".html": "text/html; charset=utf-8",
6
+ ".htm": "text/html; charset=utf-8",
7
+ ".js": "text/javascript; charset=utf-8",
8
+ ".mjs": "text/javascript; charset=utf-8",
9
+ ".css": "text/css; charset=utf-8",
10
+ ".json": "application/json; charset=utf-8",
11
+ ".map": "application/json; charset=utf-8",
12
+ ".txt": "text/plain; charset=utf-8",
13
+ ".md": "text/plain; charset=utf-8",
14
+ ".xml": "application/xml; charset=utf-8",
15
+ ".csv": "text/csv; charset=utf-8",
16
+ ".svg": "image/svg+xml",
17
+ ".png": "image/png",
18
+ ".jpg": "image/jpeg",
19
+ ".jpeg": "image/jpeg",
20
+ ".gif": "image/gif",
21
+ ".webp": "image/webp",
22
+ ".avif": "image/avif",
23
+ ".ico": "image/x-icon",
24
+ ".bmp": "image/bmp",
25
+ ".mp4": "video/mp4",
26
+ ".webm": "video/webm",
27
+ ".mov": "video/quicktime",
28
+ ".m4v": "video/mp4",
29
+ ".mp3": "audio/mpeg",
30
+ ".wav": "audio/wav",
31
+ ".ogg": "audio/ogg",
32
+ ".m4a": "audio/mp4",
33
+ ".flac": "audio/flac",
34
+ ".pdf": "application/pdf",
35
+ ".wasm": "application/wasm",
36
+ ".zip": "application/zip",
37
+ ".gz": "application/gzip",
38
+ ".tar": "application/x-tar",
39
+ ".woff": "font/woff",
40
+ ".woff2": "font/woff2",
41
+ ".ttf": "font/ttf",
42
+ ".otf": "font/otf",
43
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
44
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
45
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
46
+ };
47
+ function contentTypeFor(filePath) {
48
+ return MIME[path.extname(filePath).toLowerCase()] || "application/octet-stream";
49
+ }
50
+ function containedPath(rootDir, relPath) {
51
+ const base = path.resolve(rootDir);
52
+ const decoded = safeDecode(relPath);
53
+ const fp = path.resolve(path.join(base, decoded));
54
+ if (fp !== base && !fp.startsWith(base + path.sep)) return null;
55
+ return fp;
56
+ }
57
+ function safeDecode(p) {
58
+ try {
59
+ return decodeURIComponent(p);
60
+ } catch {
61
+ return p;
62
+ }
63
+ }
64
+ function parseRange(header, size) {
65
+ if (!header) return null;
66
+ const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
67
+ if (!m) return null;
68
+ const [, s, e] = m;
69
+ if (s === "" && e === "") return null;
70
+ let start, end;
71
+ if (s === "") {
72
+ const n = parseInt(e, 10);
73
+ if (n <= 0) return "unsatisfiable";
74
+ start = Math.max(0, size - n);
75
+ end = size - 1;
76
+ } else {
77
+ start = parseInt(s, 10);
78
+ end = e === "" ? size - 1 : parseInt(e, 10);
79
+ }
80
+ if (Number.isNaN(start) || Number.isNaN(end) || start > end || start >= size) return "unsatisfiable";
81
+ if (end >= size) end = size - 1;
82
+ return { start, end };
83
+ }
84
+ function directoryListingHtml(mountRelUrl, entries) {
85
+ const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
86
+ const base = mountRelUrl.endsWith("/") ? mountRelUrl : mountRelUrl + "/";
87
+ const rows = entries.sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1).map((e) => {
88
+ const href = base + encodeURIComponent(e.name) + (e.isDir ? "/" : "");
89
+ return `<li><a href="${esc(href)}">${esc(e.name)}${e.isDir ? "/" : ""}</a></li>`;
90
+ }).join("\n");
91
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${esc(base)}</title>
92
+ <style>body{font-family:system-ui,sans-serif;max-width:760px;margin:40px auto;padding:0 20px;color:#333}
93
+ h1{font-size:1.15rem;word-break:break-all}ul{list-style:none;padding:0}li{padding:6px 0;border-bottom:1px solid #eee}
94
+ a{color:#0969da;text-decoration:none}a:hover{text-decoration:underline}</style></head>
95
+ <body><h1>${esc(base)}</h1><ul>${rows || "<li>(empty)</li>"}</ul></body></html>`;
96
+ }
97
+ const CORS = {
98
+ "Access-Control-Allow-Origin": "*",
99
+ "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
100
+ "Access-Control-Allow-Headers": "*"
101
+ };
102
+ function serveStaticMount(req, res, opts) {
103
+ const { rootDir, relPath, mountUrlPrefix } = opts;
104
+ const browse = opts.browse !== false;
105
+ const method = (req.method || "GET").toUpperCase();
106
+ if (method === "OPTIONS") {
107
+ res.writeHead(204, CORS);
108
+ res.end();
109
+ return;
110
+ }
111
+ if (method !== "GET" && method !== "HEAD") {
112
+ res.writeHead(405, { ...CORS, Allow: "GET, HEAD, OPTIONS" });
113
+ res.end("Method Not Allowed");
114
+ return;
115
+ }
116
+ let rootStat;
117
+ try {
118
+ rootStat = fs.statSync(rootDir);
119
+ } catch {
120
+ res.writeHead(404, CORS);
121
+ res.end("Not Found");
122
+ return;
123
+ }
124
+ if (rootStat.isFile()) {
125
+ sendFile(req, res, rootDir, rootStat);
126
+ return;
127
+ }
128
+ const target = containedPath(rootDir, relPath.replace(/^\/+/, ""));
129
+ if (!target) {
130
+ res.writeHead(403, { ...CORS, "Content-Type": "text/plain" });
131
+ res.end("Forbidden");
132
+ return;
133
+ }
134
+ let st;
135
+ try {
136
+ st = fs.statSync(target);
137
+ } catch {
138
+ res.writeHead(404, { ...CORS, "Content-Type": "text/plain" });
139
+ res.end("Not Found");
140
+ return;
141
+ }
142
+ if (st.isDirectory()) {
143
+ const reqPath = (req.url || "/").split("?")[0];
144
+ if (!reqPath.endsWith("/")) {
145
+ res.writeHead(302, { ...CORS, Location: reqPath + "/" });
146
+ res.end();
147
+ return;
148
+ }
149
+ const indexPath = path.join(target, "index.html");
150
+ if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
151
+ sendFile(req, res, indexPath, fs.statSync(indexPath));
152
+ return;
153
+ }
154
+ if (!browse) {
155
+ res.writeHead(403, { ...CORS, "Content-Type": "text/plain" });
156
+ res.end("Directory listing disabled");
157
+ return;
158
+ }
159
+ let entries;
160
+ try {
161
+ entries = fs.readdirSync(target, { withFileTypes: true }).filter((d) => !d.name.startsWith(".")).map((d) => ({ name: d.name, isDir: d.isDirectory() }));
162
+ } catch {
163
+ res.writeHead(500, CORS);
164
+ res.end("Listing failed");
165
+ return;
166
+ }
167
+ const listUrl = mountUrlPrefix + (relPath === "/" ? "/" : relPath);
168
+ const html = directoryListingHtml(listUrl, entries);
169
+ res.writeHead(200, { ...CORS, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
170
+ res.end(method === "HEAD" ? void 0 : html);
171
+ return;
172
+ }
173
+ if (st.isFile()) {
174
+ sendFile(req, res, target, st);
175
+ return;
176
+ }
177
+ res.writeHead(404, CORS);
178
+ res.end("Not Found");
179
+ }
180
+ function etagFor(st) {
181
+ return `"${st.size.toString(16)}-${Math.floor(st.mtimeMs).toString(16)}"`;
182
+ }
183
+ function sendFile(req, res, filePath, st) {
184
+ const method = (req.method || "GET").toUpperCase();
185
+ const etag = etagFor(st);
186
+ const lastMod = new Date(st.mtimeMs).toUTCString();
187
+ const baseHeaders = {
188
+ ...CORS,
189
+ "Content-Type": contentTypeFor(filePath),
190
+ "Accept-Ranges": "bytes",
191
+ ETag: etag,
192
+ "Last-Modified": lastMod,
193
+ "Cache-Control": "no-cache"
194
+ // revalidate via ETag; content can change under a mount
195
+ };
196
+ const inm = req.headers["if-none-match"];
197
+ const ims = req.headers["if-modified-since"];
198
+ if (inm && inm === etag || !inm && ims && new Date(ims).getTime() >= Math.floor(st.mtimeMs / 1e3) * 1e3) {
199
+ res.writeHead(304, baseHeaders);
200
+ res.end();
201
+ return;
202
+ }
203
+ const range = parseRange(req.headers.range, st.size);
204
+ if (range === "unsatisfiable") {
205
+ res.writeHead(416, { ...baseHeaders, "Content-Range": `bytes */${st.size}` });
206
+ res.end();
207
+ return;
208
+ }
209
+ if (range) {
210
+ const len = range.end - range.start + 1;
211
+ res.writeHead(206, { ...baseHeaders, "Content-Range": `bytes ${range.start}-${range.end}/${st.size}`, "Content-Length": String(len) });
212
+ if (method === "HEAD") {
213
+ res.end();
214
+ return;
215
+ }
216
+ pipeStream(fs.createReadStream(filePath, { start: range.start, end: range.end }), res);
217
+ return;
218
+ }
219
+ res.writeHead(200, { ...baseHeaders, "Content-Length": String(st.size) });
220
+ if (method === "HEAD") {
221
+ res.end();
222
+ return;
223
+ }
224
+ pipeStream(fs.createReadStream(filePath), res);
225
+ }
226
+ function pipeStream(stream, res) {
227
+ stream.on("error", () => {
228
+ try {
229
+ res.destroy();
230
+ } catch {
231
+ }
232
+ });
233
+ res.on("close", () => {
234
+ stream.destroy();
235
+ });
236
+ stream.pipe(res);
237
+ }
238
+
239
+ export { containedPath, contentTypeFor, directoryListingHtml, parseRange, serveStaticMount };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.257",
3
+ "version": "0.2.258",
4
4
  "description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
5
5
  "author": "Amun AI AB",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "rm -rf dist bin/skills bin/commands && mkdir -p bin/skills && cp -r ../../skills/artifact bin/skills/artifact && cp -r ../../skills/crew bin/skills/crew && cp -r ../../commands bin/commands && tsc --noEmit && pkgroll",
22
22
  "typecheck": "tsc --noEmit",
23
- "test": "npx tsx test/test-context-window.mjs && npx tsx test/test-ratelimit-retry.mjs && npx tsx test/test-instance-config.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-sharing-notify-sync.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-btw-proxy-env.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-inbox-guard.mjs && npx tsx test/test-auto-topic.mjs && npx tsx test/test-project-info.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-session-send-query.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && node test/test-supervisor-restart.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox-store.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-inbox-cross-machine.mjs && npx tsx test/test-issue-store.mjs && npx tsx test/test-issue-close-gate.mjs && npx tsx test/test-loop-verify.mjs && npx tsx test/test-issue-rpc.mjs && npx tsx test/test-issue-pause.mjs && npx tsx test/test-workflow-store.mjs && npx tsx test/test-workflow-rpc.mjs && npx tsx test/test-workflow-scheduler.mjs && npx tsx test/test-cron.mjs && npx tsx test/test-workflow-runs.mjs && npx tsx test/test-workflow-idle.mjs && npx tsx test/test-serve-link-subdomain.mjs && npx tsx test/test-short-id.mjs && npx tsx test/test-transcript-edit.mjs && npx tsx test/test-edit-history.mjs && npx tsx test/test-friendly-name.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && npx tsx test/test-frpc-status.mjs && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-session-file.mjs && npx tsx test/test-channel-rpc.mjs && npx tsx test/test-wise-agent.mjs && npx tsx test/test-channel-agent.mjs && npx tsx test/test-channels-service.mjs && npx tsx test/test-hotreload-seam.mjs && npx tsx test/test-hot-reload.mjs && npx tsx test/test-hot-reload-live.mjs && npx tsx test/test-session-core.mjs && npx tsx test/test-channel-async-reply.mjs && npx tsx test/test-channel-binding.mjs && npx tsx test/test-channel-identity.mjs && npx tsx test/test-shared-session-identity.mjs && npx tsx test/test-wise-agent-auth.mjs && npx tsx test/test-channel-http.mjs && npx tsx test/test-channel-upload.mjs && npx tsx test/test-wise-voice.mjs && npx tsx test/test-wise-headless.mjs && npx tsx test/test-wise-machine.mjs && npx tsx test/test-crew-merge.mjs && npx tsx test/test-crew-verdict-routing.mjs && npx tsx test/test-crew-standalone.mjs && npx tsx test/test-session-links.mjs && npx tsx test/test-issue-loop-pause-guard.mjs && npx tsx test/test-artifact-guard.mjs && npx tsx test/test-graceful-restart.mjs",
23
+ "test": "npx tsx test/test-context-window.mjs && npx tsx test/test-ratelimit-retry.mjs && npx tsx test/test-instance-config.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-sharing-notify-sync.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-btw-proxy-env.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-inbox-guard.mjs && npx tsx test/test-auto-topic.mjs && npx tsx test/test-project-info.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-session-send-query.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && node test/test-supervisor-restart.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox-store.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-inbox-cross-machine.mjs && npx tsx test/test-issue-store.mjs && npx tsx test/test-issue-close-gate.mjs && npx tsx test/test-loop-verify.mjs && npx tsx test/test-issue-rpc.mjs && npx tsx test/test-issue-pause.mjs && npx tsx test/test-workflow-store.mjs && npx tsx test/test-workflow-rpc.mjs && npx tsx test/test-workflow-scheduler.mjs && npx tsx test/test-cron.mjs && npx tsx test/test-workflow-runs.mjs && npx tsx test/test-workflow-idle.mjs && npx tsx test/test-serve-link-subdomain.mjs && npx tsx test/test-short-id.mjs && npx tsx test/test-transcript-edit.mjs && npx tsx test/test-edit-history.mjs && npx tsx test/test-friendly-name.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-static-file-server.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && npx tsx test/test-frpc-status.mjs && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-session-file.mjs && npx tsx test/test-channel-rpc.mjs && npx tsx test/test-wise-agent.mjs && npx tsx test/test-channel-agent.mjs && npx tsx test/test-channels-service.mjs && npx tsx test/test-hotreload-seam.mjs && npx tsx test/test-hot-reload.mjs && npx tsx test/test-hot-reload-live.mjs && npx tsx test/test-session-core.mjs && npx tsx test/test-channel-async-reply.mjs && npx tsx test/test-channel-binding.mjs && npx tsx test/test-channel-identity.mjs && npx tsx test/test-shared-session-identity.mjs && npx tsx test/test-wise-agent-auth.mjs && npx tsx test/test-channel-http.mjs && npx tsx test/test-channel-upload.mjs && npx tsx test/test-wise-voice.mjs && npx tsx test/test-wise-headless.mjs && npx tsx test/test-wise-machine.mjs && npx tsx test/test-crew-merge.mjs && npx tsx test/test-crew-verdict-routing.mjs && npx tsx test/test-crew-standalone.mjs && npx tsx test/test-session-links.mjs && npx tsx test/test-issue-loop-pause-guard.mjs && npx tsx test/test-artifact-guard.mjs && npx tsx test/test-graceful-restart.mjs",
24
24
  "test:hypha": "node --no-warnings test/test-hypha-service.mjs",
25
25
  "dev": "tsx src/cli.ts",
26
26
  "dev:daemon": "tsx src/cli.ts daemon start-sync",
@@ -1,322 +0,0 @@
1
- import { spawn, execSync } from 'child_process';
2
- import { mkdirSync, existsSync, statSync, writeFileSync, chmodSync, unlinkSync } from 'fs';
3
- import { join } from 'path';
4
- import { homedir, platform, arch } from 'os';
5
-
6
- const CADDY_VERSION = "2.9.1";
7
- const BIN_DIR = join(homedir(), ".svamp", "bin");
8
- const CADDY_BIN = join(BIN_DIR, platform() === "win32" ? "caddy.exe" : "caddy");
9
- const ADMIN_PORT = 2019;
10
- const DEFAULT_LISTEN_PORT = 18080;
11
- function getCaddyDownloadUrl() {
12
- const os = platform();
13
- const a = arch();
14
- let osStr;
15
- let archStr;
16
- switch (os) {
17
- case "darwin":
18
- osStr = "mac";
19
- break;
20
- case "linux":
21
- osStr = "linux";
22
- break;
23
- case "win32":
24
- osStr = "windows";
25
- break;
26
- default:
27
- throw new Error(`Unsupported platform: ${os}`);
28
- }
29
- switch (a) {
30
- case "x64":
31
- archStr = "amd64";
32
- break;
33
- case "arm64":
34
- archStr = "arm64";
35
- break;
36
- default:
37
- throw new Error(`Unsupported architecture: ${a}`);
38
- }
39
- const ext = os === "win32" ? "zip" : "tar.gz";
40
- return `https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_${osStr}_${archStr}.${ext}`;
41
- }
42
- async function ensureCaddy(log) {
43
- if (existsSync(CADDY_BIN)) return CADDY_BIN;
44
- const logger = log || console.log;
45
- mkdirSync(BIN_DIR, { recursive: true });
46
- const url = getCaddyDownloadUrl();
47
- logger(`Downloading Caddy ${CADDY_VERSION} from ${url}...`);
48
- const response = await fetch(url);
49
- if (!response.ok) {
50
- throw new Error(`Failed to download Caddy: ${response.status} ${response.statusText}`);
51
- }
52
- const buffer = Buffer.from(await response.arrayBuffer());
53
- if (platform() === "win32") {
54
- throw new Error("Windows ZIP extraction not implemented \u2014 please install caddy manually");
55
- }
56
- const tmpTar = join(BIN_DIR, `caddy-${CADDY_VERSION}.tar.gz`);
57
- writeFileSync(tmpTar, buffer);
58
- try {
59
- execSync(
60
- `tar -xzf "${tmpTar}" -C "${BIN_DIR}" caddy`,
61
- { stdio: "pipe" }
62
- );
63
- chmodSync(CADDY_BIN, 493);
64
- logger(`Caddy installed at ${CADDY_BIN}`);
65
- } finally {
66
- try {
67
- unlinkSync(tmpTar);
68
- } catch {
69
- }
70
- }
71
- return CADDY_BIN;
72
- }
73
- const CORS_HEADERS_HANDLER = {
74
- handler: "headers",
75
- response: {
76
- set: {
77
- "Access-Control-Allow-Origin": ["*"],
78
- "Access-Control-Allow-Methods": ["GET, POST, PUT, DELETE, OPTIONS"],
79
- "Access-Control-Allow-Headers": ["*"]
80
- }
81
- }
82
- };
83
- function generateCaddyConfig(listenPort, mounts, adminPort = ADMIN_PORT) {
84
- const routes = [];
85
- for (const mount of mounts) {
86
- if (mount.isFile) {
87
- const parentDir = mount.directory.replace(/[\\/][^\\/]+$/, "") || "/";
88
- const fileName = mount.directory.replace(/.*[\\/]/, "");
89
- routes.push({
90
- match: [{ path: [`/${mount.name}`] }],
91
- handle: [
92
- CORS_HEADERS_HANDLER,
93
- { handler: "rewrite", uri: `/${fileName}` },
94
- { handler: "file_server", root: parentDir }
95
- ]
96
- });
97
- } else {
98
- routes.push({
99
- match: [{ path: [`/${mount.name}`] }],
100
- handle: [
101
- {
102
- handler: "static_response",
103
- status_code: 302,
104
- headers: { Location: [`/${mount.name}/`] }
105
- }
106
- ]
107
- });
108
- const fileServerHandler = { handler: "file_server", root: mount.directory };
109
- if (mount.browse !== false) fileServerHandler.browse = {};
110
- routes.push({
111
- match: [{ path: [`/${mount.name}/*`, `/${mount.name}/`] }],
112
- handle: [
113
- CORS_HEADERS_HANDLER,
114
- { handler: "rewrite", strip_path_prefix: `/${mount.name}` },
115
- fileServerHandler
116
- ]
117
- });
118
- }
119
- }
120
- routes.push({
121
- match: [{ path: ["/"] }],
122
- handle: [
123
- CORS_HEADERS_HANDLER,
124
- {
125
- handler: "static_response",
126
- status_code: 200,
127
- headers: { "Content-Type": ["text/html; charset=utf-8"] },
128
- body: generateMountListHtml(mounts)
129
- }
130
- ]
131
- });
132
- routes.push({
133
- handle: [
134
- CORS_HEADERS_HANDLER,
135
- { handler: "static_response", status_code: 404, body: "Not Found" }
136
- ]
137
- });
138
- return {
139
- admin: {
140
- listen: `127.0.0.1:${adminPort}`
141
- },
142
- apps: {
143
- http: {
144
- servers: {
145
- srv0: {
146
- listen: [`127.0.0.1:${listenPort}`],
147
- routes
148
- }
149
- }
150
- }
151
- }
152
- };
153
- }
154
- function generateMountListHtml(mounts) {
155
- const items = mounts.map(
156
- (m) => `<li><a href="/${m.name}/">${m.name}</a> \u2014 ${m.directory}</li>`
157
- ).join("\n");
158
- return `<!DOCTYPE html>
159
- <html><head><meta charset="utf-8"><title>Svamp File Server</title>
160
- <style>
161
- body{font-family:system-ui,sans-serif;max-width:700px;margin:40px auto;padding:0 20px;color:#333}
162
- h1{font-size:1.3rem}
163
- ul{list-style:none;padding:0}
164
- li{padding:8px 0;border-bottom:1px solid #eee}
165
- a{color:#0969da;text-decoration:none;font-weight:500}
166
- a:hover{text-decoration:underline}
167
- </style>
168
- </head><body>
169
- <h1>Svamp File Server</h1>
170
- <ul>${items || "<li>No mounts registered.</li>"}</ul>
171
- </body></html>`;
172
- }
173
- class CaddyManager {
174
- process = null;
175
- mounts = /* @__PURE__ */ new Map();
176
- listenPort;
177
- adminPort;
178
- log;
179
- configPath;
180
- constructor(options = {}) {
181
- this.listenPort = options.listenPort || DEFAULT_LISTEN_PORT;
182
- this.adminPort = options.adminPort || ADMIN_PORT;
183
- this.log = options.log || ((msg) => console.log(`[CADDY] ${msg}`));
184
- const configDir = options.configDir || join(homedir(), ".svamp", "caddy");
185
- mkdirSync(configDir, { recursive: true });
186
- this.configPath = join(configDir, "config.json");
187
- }
188
- /** Start Caddy if not already running. */
189
- async start() {
190
- if (this.process) return;
191
- const caddyPath = await ensureCaddy(this.log);
192
- this.writeConfig();
193
- return new Promise((resolve, reject) => {
194
- const child = spawn(caddyPath, ["run", "--config", this.configPath], {
195
- stdio: ["ignore", "pipe", "pipe"],
196
- env: { ...process.env }
197
- });
198
- this.process = child;
199
- let resolved = false;
200
- const onOutput = (data) => {
201
- const text = data.toString().trim();
202
- if (!text) return;
203
- if (text.includes("serving initial configuration")) {
204
- if (!resolved) {
205
- resolved = true;
206
- this.log(`Caddy started on 127.0.0.1:${this.listenPort}`);
207
- resolve();
208
- }
209
- }
210
- if (text.includes('"level":"error"')) {
211
- this.log(`Caddy error: ${text}`);
212
- }
213
- };
214
- child.stdout?.on("data", onOutput);
215
- child.stderr?.on("data", onOutput);
216
- child.on("error", (err) => {
217
- if (!resolved) {
218
- resolved = true;
219
- reject(err);
220
- }
221
- });
222
- child.on("exit", (code) => {
223
- this.process = null;
224
- if (!resolved) {
225
- resolved = true;
226
- reject(new Error(`Caddy exited with code ${code}`));
227
- }
228
- });
229
- setTimeout(() => {
230
- if (!resolved) {
231
- resolved = true;
232
- resolve();
233
- }
234
- }, 1e4);
235
- });
236
- }
237
- /** Stop the Caddy process. */
238
- async stop() {
239
- if (!this.process) return;
240
- try {
241
- await fetch(`http://127.0.0.1:${this.adminPort}/stop`, { method: "POST" });
242
- } catch {
243
- this.process.kill("SIGTERM");
244
- }
245
- await new Promise((resolve) => {
246
- if (!this.process) {
247
- resolve();
248
- return;
249
- }
250
- const timeout = setTimeout(() => {
251
- try {
252
- this.process?.kill("SIGKILL");
253
- } catch {
254
- }
255
- resolve();
256
- }, 5e3);
257
- this.process.on("exit", () => {
258
- clearTimeout(timeout);
259
- resolve();
260
- });
261
- });
262
- this.process = null;
263
- this.log("Caddy stopped");
264
- }
265
- /** Add a mount and reload config. */
266
- async addMount(name, directory, browse = true) {
267
- const isFile = existsSync(directory) && statSync(directory).isFile();
268
- this.mounts.set(name, { name, directory, browse, isFile });
269
- await this.reloadConfig();
270
- this.log(`Mount added: ${name} \u2192 ${directory}`);
271
- }
272
- /** Remove a mount and reload config. */
273
- async removeMount(name) {
274
- if (!this.mounts.has(name)) {
275
- throw new Error(`Mount '${name}' not found`);
276
- }
277
- this.mounts.delete(name);
278
- await this.reloadConfig();
279
- this.log(`Mount removed: ${name}`);
280
- }
281
- /** Get all current mounts. */
282
- getMounts() {
283
- return Array.from(this.mounts.values());
284
- }
285
- /** Whether Caddy is running. */
286
- get isRunning() {
287
- return this.process !== null;
288
- }
289
- /** The port Caddy listens on. */
290
- get port() {
291
- return this.listenPort;
292
- }
293
- // ── Internal ─────────────────────────────────────────────────────
294
- writeConfig() {
295
- const config = generateCaddyConfig(this.listenPort, Array.from(this.mounts.values()), this.adminPort);
296
- writeFileSync(this.configPath, JSON.stringify(config, null, 2));
297
- }
298
- async reloadConfig() {
299
- this.writeConfig();
300
- if (!this.process) return;
301
- try {
302
- const config = generateCaddyConfig(this.listenPort, Array.from(this.mounts.values()), this.adminPort);
303
- const resp = await fetch(`http://127.0.0.1:${this.adminPort}/load`, {
304
- method: "POST",
305
- headers: { "Content-Type": "application/json" },
306
- body: JSON.stringify(config)
307
- });
308
- if (!resp.ok) {
309
- const body = await resp.text();
310
- this.log(`Config reload failed: ${resp.status} ${body}`);
311
- await this.stop();
312
- await this.start();
313
- }
314
- } catch (err) {
315
- this.log(`Config reload error: ${err.message} \u2014 restarting Caddy`);
316
- await this.stop();
317
- await this.start();
318
- }
319
- }
320
- }
321
-
322
- export { CaddyManager, ensureCaddy, generateCaddyConfig };