svamp-cli 0.2.256 → 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.
- package/bin/skills/artifact/SKILL.md +1 -1
- package/dist/{agentCommands-DfTICQUR.mjs → agentCommands-CQ89BZon.mjs} +5 -5
- package/dist/{auth-CaFzUB-H.mjs → auth-CaFsKYbx.mjs} +1 -1
- package/dist/cli.mjs +61 -61
- package/dist/{commands-MopZEqZ7.mjs → commands--X8f4eSr.mjs} +24 -13
- package/dist/{commands-Dp4OLJ80.mjs → commands-B6eGMF5H.mjs} +1 -1
- package/dist/{commands-Cgi-GKQk.mjs → commands-BgX0W0uK.mjs} +1 -1
- package/dist/{commands-BrTyEX8u.mjs → commands-D8l74hsL.mjs} +1 -1
- package/dist/{commands-CRvH25K7.mjs → commands-Dd8xEIf0.mjs} +1 -1
- package/dist/{commands-C6_ClNDg.mjs → commands-DrfYv705.mjs} +2 -2
- package/dist/{commands-DPKRLSJZ.mjs → commands-DutfM3U7.mjs} +31 -15
- package/dist/{fleet-DT3LusNA.mjs → fleet-UvQnIWT1.mjs} +1 -1
- package/dist/{frpc--8kUn3Em.mjs → frpc-IXxyfgPn.mjs} +1 -1
- package/dist/{headlessCli-DJIDtYga.mjs → headlessCli-BK7WSS3D.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{package-CBpvrZJs.mjs → package-BdZX5mDv.mjs} +2 -2
- package/dist/{rpc-DPlerDzK.mjs → rpc-CyACkgY1.mjs} +1 -1
- package/dist/{rpc-BV_9jVwX.mjs → rpc-D7DB3XCH.mjs} +1 -1
- package/dist/{run-BDX-RaEm.mjs → run-C1Unk4Zx.mjs} +108 -84
- package/dist/{run-19FUjQ96.mjs → run-C1ufZjIW.mjs} +1 -1
- package/dist/{scheduler-CueRBYz_.mjs → scheduler-7wve0Fy3.mjs} +1 -1
- package/dist/{serveCommands-DRP03-lg.mjs → serveCommands-iQJJLsMJ.mjs} +5 -5
- package/dist/{serveManager-CQGY6nFz.mjs → serveManager-BszV0oIl.mjs} +62 -74
- package/dist/{sideband-BQ4pkLIQ.mjs → sideband-DMPZ-ZoR.mjs} +1 -1
- package/dist/staticFileServer-FCEOLIYH.mjs +239 -0
- package/package.json +2 -2
- package/dist/caddy-CuTbE3NY.mjs +0 -322
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
1
|
+
import { spawn, execSync } from 'child_process';
|
|
2
2
|
import * as crypto from 'crypto';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as http from 'http';
|
|
5
5
|
import * as net from 'net';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
import {
|
|
7
|
+
import { serveStaticMount } from './staticFileServer-FCEOLIYH.mjs';
|
|
8
|
+
import { l as getHyphaServerUrl, S as ServeAuth, m as hasCookieToken } from './run-C1Unk4Zx.mjs';
|
|
8
9
|
import 'os';
|
|
9
10
|
import 'fs/promises';
|
|
10
11
|
import 'url';
|
|
@@ -56,6 +57,27 @@ async function tryReservePort(preferred) {
|
|
|
56
57
|
});
|
|
57
58
|
});
|
|
58
59
|
}
|
|
60
|
+
function killOrphanedCaddy(log) {
|
|
61
|
+
if (process.platform === "win32") return;
|
|
62
|
+
try {
|
|
63
|
+
const out = execSync("ps -axo pid=,command=", { encoding: "utf-8" });
|
|
64
|
+
const pids = [];
|
|
65
|
+
for (const line of out.split("\n")) {
|
|
66
|
+
const m = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
67
|
+
if (!m) continue;
|
|
68
|
+
const [, pid, cmd] = m;
|
|
69
|
+
if (cmd.includes(".svamp/bin/caddy") && pid !== String(process.pid)) pids.push(pid);
|
|
70
|
+
}
|
|
71
|
+
for (const pid of pids) {
|
|
72
|
+
try {
|
|
73
|
+
process.kill(Number(pid), "SIGKILL");
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (pids.length) log(`Reaped ${pids.length} orphaned Caddy process(es) \u2014 retired in favor of the native file server (#0358).`);
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
59
81
|
const MOUNT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
60
82
|
function validateMountName(name) {
|
|
61
83
|
if (!name || name.length > 128) {
|
|
@@ -68,18 +90,14 @@ function validateMountName(name) {
|
|
|
68
90
|
class ServeManager {
|
|
69
91
|
mounts = /* @__PURE__ */ new Map();
|
|
70
92
|
port = 0;
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
// Caddy internal port
|
|
74
|
-
/** Last-used ports loaded from disk — used as preferred values on next start
|
|
93
|
+
// public HTTP server port (serves static files natively — #0358)
|
|
94
|
+
/** Last-used port loaded from disk — used as the preferred value on next start
|
|
75
95
|
* so external configs (tunnels, cron, bookmarks) survive daemon restarts. */
|
|
76
96
|
persistedPort;
|
|
77
|
-
persistedCaddyPort;
|
|
78
97
|
/** Per-mount frpc tunnel — each mount gets its own subdomain `static-<name>-<hash>`. */
|
|
79
98
|
mountTunnels = /* @__PURE__ */ new Map();
|
|
80
99
|
/** hostname (lowercased, no port) → mount name. Updated when tunnels connect. */
|
|
81
100
|
hostToMount = /* @__PURE__ */ new Map();
|
|
82
|
-
caddy = null;
|
|
83
101
|
proxyServer = null;
|
|
84
102
|
auth = null;
|
|
85
103
|
/** Live child processes for managed mounts. Keyed by mount name. */
|
|
@@ -159,9 +177,6 @@ class ServeManager {
|
|
|
159
177
|
};
|
|
160
178
|
this.mounts.set(spec.name, mount);
|
|
161
179
|
await this.ensureRunning();
|
|
162
|
-
if (resolvedDir && this.caddy?.isRunning) {
|
|
163
|
-
await this.caddy.addMount(spec.name, resolvedDir);
|
|
164
|
-
}
|
|
165
180
|
await this.startMountTunnel(spec.name);
|
|
166
181
|
if (spec.process && !spec.process.wakeOnRequest) {
|
|
167
182
|
try {
|
|
@@ -198,13 +213,6 @@ class ServeManager {
|
|
|
198
213
|
for (const [host, mountName] of this.hostToMount.entries()) {
|
|
199
214
|
if (mountName === name) this.hostToMount.delete(host);
|
|
200
215
|
}
|
|
201
|
-
if (this.caddy?.isRunning) {
|
|
202
|
-
try {
|
|
203
|
-
await this.caddy.removeMount(name);
|
|
204
|
-
} catch (err) {
|
|
205
|
-
this.log(`Warning: Caddy mount removal failed: ${err.message}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
216
|
this.persist();
|
|
209
217
|
this.log(`Mount removed: ${name}`);
|
|
210
218
|
if (this.mounts.size === 0) {
|
|
@@ -225,12 +233,12 @@ class ServeManager {
|
|
|
225
233
|
* Get server info — each mount has its own URL (per-mount subdomain).
|
|
226
234
|
*/
|
|
227
235
|
getInfo() {
|
|
228
|
-
const running = this.
|
|
236
|
+
const running = this.proxyServer != null;
|
|
229
237
|
const firstMount = this.mounts.values().next().value;
|
|
230
238
|
const firstUrl = firstMount ? this.getMountUrl(firstMount.name) : null;
|
|
231
239
|
return {
|
|
232
240
|
url: firstUrl,
|
|
233
|
-
port: running ? this.
|
|
241
|
+
port: running ? this.port : 0,
|
|
234
242
|
authProxyPort: running ? this.port : 0,
|
|
235
243
|
running,
|
|
236
244
|
mountCount: this.mounts.size,
|
|
@@ -249,7 +257,6 @@ class ServeManager {
|
|
|
249
257
|
if (!fs.existsSync(this.persistFile)) return;
|
|
250
258
|
const raw = JSON.parse(fs.readFileSync(this.persistFile, "utf-8"));
|
|
251
259
|
this.persistedPort = raw.port;
|
|
252
|
-
this.persistedCaddyPort = raw.caddyPort;
|
|
253
260
|
if (!raw.mounts || raw.mounts.length === 0) return;
|
|
254
261
|
let restoredCount = 0;
|
|
255
262
|
for (const m of raw.mounts) {
|
|
@@ -327,11 +334,6 @@ class ServeManager {
|
|
|
327
334
|
await new Promise((resolve) => this.proxyServer.close(() => resolve()));
|
|
328
335
|
this.proxyServer = null;
|
|
329
336
|
}
|
|
330
|
-
if (this.caddy) {
|
|
331
|
-
await this.caddy.stop();
|
|
332
|
-
this.caddy = null;
|
|
333
|
-
this.log("Caddy stopped");
|
|
334
|
-
}
|
|
335
337
|
this.auth?.destroy();
|
|
336
338
|
}
|
|
337
339
|
// ── Managed-process lifecycle ─────────────────────────────────────────
|
|
@@ -488,8 +490,7 @@ class ServeManager {
|
|
|
488
490
|
persist() {
|
|
489
491
|
const state = {
|
|
490
492
|
mounts: Array.from(this.mounts.values()),
|
|
491
|
-
...this.port > 0 ? { port: this.port } : {}
|
|
492
|
-
...this.caddyPort > 0 ? { caddyPort: this.caddyPort } : {}
|
|
493
|
+
...this.port > 0 ? { port: this.port } : {}
|
|
493
494
|
};
|
|
494
495
|
try {
|
|
495
496
|
fs.writeFileSync(this.persistFile, JSON.stringify(state, null, 2));
|
|
@@ -499,32 +500,24 @@ class ServeManager {
|
|
|
499
500
|
}
|
|
500
501
|
/** Start auth proxy + Caddy if not already running. Per-mount tunnels are started separately. */
|
|
501
502
|
async ensureRunning() {
|
|
502
|
-
if (this.
|
|
503
|
-
|
|
503
|
+
if (this.proxyServer) return;
|
|
504
|
+
killOrphanedCaddy((m) => this.log(m));
|
|
504
505
|
this.port = await tryReservePort(this.persistedPort);
|
|
505
|
-
const adminPort = await findFreePort();
|
|
506
506
|
if (this.persistedPort && this.port !== this.persistedPort) {
|
|
507
507
|
this.log(`\u26A0 Previous serve port ${this.persistedPort} unavailable \u2014 using ${this.port}. Downstream configs referencing the old port will need updating.`);
|
|
508
508
|
}
|
|
509
|
-
if (this.persistedCaddyPort && this.caddyPort !== this.persistedCaddyPort) {
|
|
510
|
-
this.log(`\u26A0 Previous Caddy port ${this.persistedCaddyPort} unavailable \u2014 using ${this.caddyPort} (Caddy is internal; no downstream impact).`);
|
|
511
|
-
}
|
|
512
|
-
const { CaddyManager } = await import('./caddy-CuTbE3NY.mjs');
|
|
513
|
-
this.caddy = new CaddyManager({
|
|
514
|
-
listenPort: this.caddyPort,
|
|
515
|
-
adminPort,
|
|
516
|
-
configDir: path.join(path.dirname(this.persistFile), "caddy"),
|
|
517
|
-
log: (msg) => this.log(`[Caddy] ${msg}`)
|
|
518
|
-
});
|
|
519
|
-
for (const mount of this.mounts.values()) {
|
|
520
|
-
if (mount.directory) {
|
|
521
|
-
await this.caddy.addMount(mount.name, mount.directory);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
await this.caddy.start();
|
|
525
|
-
this.log(`Caddy file server started on 127.0.0.1:${this.caddyPort}`);
|
|
526
509
|
await this.startAuthProxy();
|
|
527
|
-
this.log(`
|
|
510
|
+
this.log(`Static file server started on 127.0.0.1:${this.port}`);
|
|
511
|
+
}
|
|
512
|
+
/** Minimal mount-listing page for the server root (replaces Caddy's root route). */
|
|
513
|
+
mountListHtml() {
|
|
514
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
515
|
+
const items = Array.from(this.mounts.values()).filter((m) => m.directory).map((m) => `<li><a href="/${esc(m.name)}/">${esc(m.name)}</a> \u2014 ${esc(m.directory || "")}</li>`).join("\n");
|
|
516
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Svamp File Server</title>
|
|
517
|
+
<style>body{font-family:system-ui,sans-serif;max-width:700px;margin:40px auto;padding:0 20px;color:#333}
|
|
518
|
+
h1{font-size:1.3rem}ul{list-style:none;padding:0}li{padding:8px 0;border-bottom:1px solid #eee}
|
|
519
|
+
a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:underline}</style></head>
|
|
520
|
+
<body><h1>Svamp File Server</h1><ul>${items || "<li>No mounts registered.</li>"}</ul></body></html>`;
|
|
528
521
|
}
|
|
529
522
|
/** Start a lightweight Node.js HTTP proxy with auth + upload support. */
|
|
530
523
|
startAuthProxy() {
|
|
@@ -658,7 +651,7 @@ class ServeManager {
|
|
|
658
651
|
const handle = this.managedProcs.get(mount.name);
|
|
659
652
|
if (handle) handle.lastRequestAt = Date.now();
|
|
660
653
|
const targetPath = mountResolvedByHost ? req.url || "/" : (basePath || "/") + (url.search || "");
|
|
661
|
-
const
|
|
654
|
+
const proxyReq = http.request({
|
|
662
655
|
hostname: "127.0.0.1",
|
|
663
656
|
port: cfg.port,
|
|
664
657
|
path: targetPath,
|
|
@@ -668,37 +661,32 @@ class ServeManager {
|
|
|
668
661
|
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
669
662
|
proxyRes.pipe(res);
|
|
670
663
|
});
|
|
671
|
-
|
|
664
|
+
proxyReq.on("error", (err) => {
|
|
672
665
|
if (!res.headersSent) {
|
|
673
666
|
res.writeHead(502);
|
|
674
667
|
res.end(`Backend error: ${err.message}`);
|
|
675
668
|
}
|
|
676
669
|
});
|
|
677
|
-
req.pipe(
|
|
670
|
+
req.pipe(proxyReq);
|
|
678
671
|
return;
|
|
679
672
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
673
|
+
if (mount && mount.directory) {
|
|
674
|
+
serveStaticMount(req, res, {
|
|
675
|
+
rootDir: mount.directory,
|
|
676
|
+
relPath: basePath || "/",
|
|
677
|
+
// Path-based access is prefixed with /<mountName>; host-based has no prefix.
|
|
678
|
+
mountUrlPrefix: mountResolvedByHost ? "" : `/${mountName}`,
|
|
679
|
+
browse: true});
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const cors = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS", "Access-Control-Allow-Headers": "*" };
|
|
683
|
+
if (url.pathname === "/" || basePath === "/") {
|
|
684
|
+
res.writeHead(200, { ...cors, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
685
|
+
res.end(this.mountListHtml());
|
|
686
|
+
} else {
|
|
687
|
+
res.writeHead(404, { ...cors, "Content-Type": "text/plain" });
|
|
688
|
+
res.end("Not found");
|
|
684
689
|
}
|
|
685
|
-
const proxyReq = http.request({
|
|
686
|
-
hostname: "127.0.0.1",
|
|
687
|
-
port: this.caddyPort,
|
|
688
|
-
path: proxyPath,
|
|
689
|
-
method: req.method,
|
|
690
|
-
headers: req.headers
|
|
691
|
-
}, (proxyRes) => {
|
|
692
|
-
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
693
|
-
proxyRes.pipe(res);
|
|
694
|
-
});
|
|
695
|
-
proxyReq.on("error", (err) => {
|
|
696
|
-
if (!res.headersSent) {
|
|
697
|
-
res.writeHead(502);
|
|
698
|
-
res.end("Proxy error");
|
|
699
|
-
}
|
|
700
|
-
});
|
|
701
|
-
req.pipe(proxyReq);
|
|
702
690
|
});
|
|
703
691
|
this.proxyServer = server;
|
|
704
692
|
server.listen(this.port, "127.0.0.1", () => resolve());
|
|
@@ -751,7 +739,7 @@ class ServeManager {
|
|
|
751
739
|
const mount = this.mounts.get(mountName);
|
|
752
740
|
const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, buildLinkSubdomain(subdomainSafe, mount.linkToken)]]) : void 0;
|
|
753
741
|
try {
|
|
754
|
-
const { FrpcTunnel } = await import('./frpc
|
|
742
|
+
const { FrpcTunnel } = await import('./frpc-IXxyfgPn.mjs');
|
|
755
743
|
let tunnel;
|
|
756
744
|
tunnel = new FrpcTunnel({
|
|
757
745
|
name: tunnelName,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as READ_ONLY_TOOLS, N as loadMachineContext, O as buildMachineInstructions, P as machineToolsForRole, Q as buildMachineTools } from './run-
|
|
1
|
+
import { R as READ_ONLY_TOOLS, N as loadMachineContext, O as buildMachineInstructions, P as machineToolsForRole, Q as buildMachineTools } from './run-C1Unk4Zx.mjs';
|
|
2
2
|
import 'node:child_process';
|
|
3
3
|
import 'os';
|
|
4
4
|
import 'fs/promises';
|
|
@@ -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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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.
|
|
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",
|