svamp-cli 0.2.225 → 0.2.227
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/dist/agentCommands-7sqKMVIC.mjs +526 -0
- package/dist/auth-BfDZbHl6.mjs +83 -0
- package/dist/caddy-CuTbE3NY.mjs +322 -0
- package/dist/cli.mjs +2338 -0
- package/dist/commands-CU8JDM2d.mjs +428 -0
- package/dist/commands-CWP-J9Cf.mjs +610 -0
- package/dist/commands-CnlQyDxb.mjs +2762 -0
- package/dist/commands-D2iQ2v7_.mjs +196 -0
- package/dist/commands-DA_Nx893.mjs +265 -0
- package/dist/commands-H9h_Mtc4.mjs +493 -0
- package/dist/commands-YML8Rs_R.mjs +643 -0
- package/dist/fleet-B_78L11T.mjs +356 -0
- package/dist/frpc-BQyt6j5A.mjs +681 -0
- package/dist/headlessCli-CYfN9XvR.mjs +333 -0
- package/dist/httpServer-B1KVQJfm.mjs +276 -0
- package/dist/index.mjs +22 -0
- package/dist/package-VYP3g593.mjs +63 -0
- package/dist/pinnedClaudeCode-VIupR1NK.mjs +58 -0
- package/dist/rpc-DMj8ZJPz.mjs +92 -0
- package/dist/rpc-a0fp9HhD.mjs +194 -0
- package/dist/run-DkjwFIFH.mjs +1086 -0
- package/dist/run-LEPH8aka.mjs +16075 -0
- package/dist/runStore-CtptN7US.mjs +168 -0
- package/dist/sandboxDetect-DNTcbgWD.mjs +12 -0
- package/dist/scheduler-Bz80Ipep.mjs +102 -0
- package/dist/serveCommands-u_GoFaBX.mjs +310 -0
- package/dist/serveManager-9vhexH1L.mjs +781 -0
- package/dist/serviceManager-hlOVxkhW.mjs +78 -0
- package/dist/sideband-RJijk2GE.mjs +80 -0
- package/dist/store-BTs0H_y0.mjs +148 -0
- package/dist/supervisorLock-DmfzJx7B.mjs +159 -0
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { c as workflowSteps, d as workflowsDir } from './store-BTs0H_y0.mjs';
|
|
5
|
+
|
|
6
|
+
const MAX_RUNS_PER_WORKFLOW = 50;
|
|
7
|
+
const MAX_STREAM_CHARS = 8e3;
|
|
8
|
+
const STEP_TIMEOUT_MS = 12e4;
|
|
9
|
+
function capStream(s, max = MAX_STREAM_CHARS) {
|
|
10
|
+
if (s.length <= max) return s;
|
|
11
|
+
const head = Math.floor(max * 0.6);
|
|
12
|
+
const tail = max - head;
|
|
13
|
+
const omitted = s.length - max;
|
|
14
|
+
return `${s.slice(0, head)}
|
|
15
|
+
\u2026 [${omitted} chars omitted] \u2026
|
|
16
|
+
${s.slice(s.length - tail)}`;
|
|
17
|
+
}
|
|
18
|
+
function runsDir(projectRoot) {
|
|
19
|
+
return join(workflowsDir(projectRoot), ".runs");
|
|
20
|
+
}
|
|
21
|
+
function runsFile(projectRoot, workflow) {
|
|
22
|
+
const safe = workflow.replace(/[^\w.-]+/g, "_");
|
|
23
|
+
return join(runsDir(projectRoot), `${safe}.jsonl`);
|
|
24
|
+
}
|
|
25
|
+
function shortRunId(seed) {
|
|
26
|
+
let h = 2166136261;
|
|
27
|
+
for (let i = 0; i < seed.length; i++) {
|
|
28
|
+
h ^= seed.charCodeAt(i);
|
|
29
|
+
h = Math.imul(h, 16777619);
|
|
30
|
+
}
|
|
31
|
+
return (h >>> 0).toString(36).padStart(7, "0").slice(0, 7);
|
|
32
|
+
}
|
|
33
|
+
function recordRun(projectRoot, run) {
|
|
34
|
+
try {
|
|
35
|
+
const dir = runsDir(projectRoot);
|
|
36
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
37
|
+
const file = runsFile(projectRoot, run.workflow);
|
|
38
|
+
const existing = loadRuns(projectRoot, run.workflow);
|
|
39
|
+
const idx = existing.findIndex((r) => r.id === run.id);
|
|
40
|
+
if (idx >= 0) existing[idx] = run;
|
|
41
|
+
else existing.push(run);
|
|
42
|
+
const kept = existing.slice(-MAX_RUNS_PER_WORKFLOW);
|
|
43
|
+
const tmp = `${file}.tmp`;
|
|
44
|
+
writeFileSync(tmp, kept.map((r) => JSON.stringify(r)).join("\n") + "\n");
|
|
45
|
+
renameSync(tmp, file);
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function loadRuns(projectRoot, workflow) {
|
|
50
|
+
try {
|
|
51
|
+
const file = runsFile(projectRoot, workflow);
|
|
52
|
+
if (!existsSync(file)) return [];
|
|
53
|
+
return readFileSync(file, "utf-8").split("\n").filter(Boolean).map((l) => {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(l);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}).filter((r) => !!r);
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function listRuns(projectRoot, workflow, limit = MAX_RUNS_PER_WORKFLOW) {
|
|
65
|
+
return loadRuns(projectRoot, workflow).slice(-limit).reverse().map((r) => {
|
|
66
|
+
const failedStep = r.steps.findIndex((s) => s.exitCode !== 0 && s.exitCode !== null || s.timedOut);
|
|
67
|
+
const { steps, ...rest } = r;
|
|
68
|
+
return { ...rest, stepCount: steps.length, failedStep: failedStep >= 0 ? failedStep : void 0 };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function getRun(projectRoot, workflow, runId) {
|
|
72
|
+
return loadRuns(projectRoot, workflow).find((r) => r.id === runId) || null;
|
|
73
|
+
}
|
|
74
|
+
function defaultExecStep(root, cmd, env) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
let stdout = "", stderr = "", timedOut = false, settled = false;
|
|
77
|
+
const finish = (exitCode) => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
settled = true;
|
|
80
|
+
resolve({ exitCode, stdout, stderr, timedOut });
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
const child = spawn("sh", ["-c", cmd], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
timedOut = true;
|
|
86
|
+
try {
|
|
87
|
+
child.kill("SIGKILL");
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}, STEP_TIMEOUT_MS);
|
|
91
|
+
child.stdout?.on("data", (d) => {
|
|
92
|
+
stdout += d.toString();
|
|
93
|
+
if (stdout.length > MAX_STREAM_CHARS * 4) stdout = capStream(stdout);
|
|
94
|
+
});
|
|
95
|
+
child.stderr?.on("data", (d) => {
|
|
96
|
+
stderr += d.toString();
|
|
97
|
+
if (stderr.length > MAX_STREAM_CHARS * 4) stderr = capStream(stderr);
|
|
98
|
+
});
|
|
99
|
+
child.on("error", (e) => {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
stderr += `
|
|
102
|
+
[spawn error] ${e.message}`;
|
|
103
|
+
finish(null);
|
|
104
|
+
});
|
|
105
|
+
child.on("close", (code) => {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
finish(code);
|
|
108
|
+
});
|
|
109
|
+
} catch (e) {
|
|
110
|
+
stderr += `[spawn threw] ${e?.message || e}`;
|
|
111
|
+
finish(null);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async function runWorkflow(projectRoot, wf, opts) {
|
|
116
|
+
const now = opts.now || (() => {
|
|
117
|
+
const d = /* @__PURE__ */ new Date();
|
|
118
|
+
return { iso: d.toISOString(), ms: d.getTime() };
|
|
119
|
+
});
|
|
120
|
+
const exec = opts.execStep || defaultExecStep;
|
|
121
|
+
const start = now();
|
|
122
|
+
const id = shortRunId(`${wf.name}:${start.iso}:${start.ms}`);
|
|
123
|
+
const env = wf.session ? { ...process.env, SVAMP_SESSION_ID: wf.session } : process.env;
|
|
124
|
+
const run = {
|
|
125
|
+
id,
|
|
126
|
+
workflow: wf.name,
|
|
127
|
+
trigger: opts.trigger,
|
|
128
|
+
triggerContext: opts.triggerContext,
|
|
129
|
+
actor: opts.actor ?? wf.session ?? null,
|
|
130
|
+
startedAt: start.iso,
|
|
131
|
+
status: "running",
|
|
132
|
+
steps: []
|
|
133
|
+
};
|
|
134
|
+
recordRun(projectRoot, run);
|
|
135
|
+
const steps = workflowSteps(wf);
|
|
136
|
+
try {
|
|
137
|
+
for (let i = 0; i < steps.length; i++) {
|
|
138
|
+
const s0 = now();
|
|
139
|
+
const r = await exec(projectRoot, steps[i].run, env);
|
|
140
|
+
const s1 = now();
|
|
141
|
+
run.steps.push({
|
|
142
|
+
index: i,
|
|
143
|
+
cmd: steps[i].run,
|
|
144
|
+
exitCode: r.exitCode,
|
|
145
|
+
stdout: capStream(r.stdout),
|
|
146
|
+
stderr: capStream(r.stderr),
|
|
147
|
+
durationMs: s1.ms - s0.ms,
|
|
148
|
+
timedOut: r.timedOut || void 0
|
|
149
|
+
});
|
|
150
|
+
if (r.timedOut || r.exitCode !== 0) {
|
|
151
|
+
run.status = "failed";
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (run.status === "running") run.status = "success";
|
|
156
|
+
} catch (e) {
|
|
157
|
+
run.status = "failed";
|
|
158
|
+
run.error = e?.message || String(e);
|
|
159
|
+
}
|
|
160
|
+
const end = now();
|
|
161
|
+
run.finishedAt = end.iso;
|
|
162
|
+
run.durationMs = end.ms - start.ms;
|
|
163
|
+
recordRun(projectRoot, run);
|
|
164
|
+
opts.log?.(`[workflow] run ${id} "${wf.name}" \u2192 ${run.status} (${run.steps.length} step${run.steps.length === 1 ? "" : "s"})`);
|
|
165
|
+
return run;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { getRun as g, listRuns as l, runWorkflow as r };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function isSandboxed() {
|
|
2
|
+
return process.env.SVAMP_SANDBOXED === "1";
|
|
3
|
+
}
|
|
4
|
+
const SANDBOX_BLOCKED_MSG = "This command is not available in sandboxed sessions. Available commands: set-title, set-link";
|
|
5
|
+
function requireNotSandboxed(commandName) {
|
|
6
|
+
if (isSandboxed()) {
|
|
7
|
+
console.error(`${commandName}: ${SANDBOX_BLOCKED_MSG}`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { SANDBOX_BLOCKED_MSG, isSandboxed, requireNotSandboxed };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { m as resolveProjectRoot, y as cronMatches } from './run-LEPH8aka.mjs';
|
|
2
|
+
import { l as listWorkflows, i as isWorkflowEnabled, w as workflowCrons } from './store-BTs0H_y0.mjs';
|
|
3
|
+
import { r as runWorkflow } from './runStore-CtptN7US.mjs';
|
|
4
|
+
import 'os';
|
|
5
|
+
import 'fs/promises';
|
|
6
|
+
import 'fs';
|
|
7
|
+
import 'path';
|
|
8
|
+
import 'url';
|
|
9
|
+
import 'child_process';
|
|
10
|
+
import 'crypto';
|
|
11
|
+
import 'node:crypto';
|
|
12
|
+
import 'node:fs';
|
|
13
|
+
import 'node:child_process';
|
|
14
|
+
import 'util';
|
|
15
|
+
import 'node:path';
|
|
16
|
+
import 'node:events';
|
|
17
|
+
import 'node:os';
|
|
18
|
+
import '@agentclientprotocol/sdk';
|
|
19
|
+
import '@modelcontextprotocol/sdk/client/index.js';
|
|
20
|
+
import '@modelcontextprotocol/sdk/client/stdio.js';
|
|
21
|
+
import '@modelcontextprotocol/sdk/types.js';
|
|
22
|
+
import 'zod';
|
|
23
|
+
import 'node:fs/promises';
|
|
24
|
+
import 'node:util';
|
|
25
|
+
import 'yaml';
|
|
26
|
+
|
|
27
|
+
function cronMatchesSafe(expr, date) {
|
|
28
|
+
try {
|
|
29
|
+
return cronMatches(expr, date);
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
class WorkflowScheduler {
|
|
35
|
+
constructor(deps) {
|
|
36
|
+
this.deps = deps;
|
|
37
|
+
}
|
|
38
|
+
// Wall-clock minute we last processed, so multiple sub-minute ticks fire a workflow at most once
|
|
39
|
+
// per minute (cron granularity is one minute).
|
|
40
|
+
lastMinuteKey = null;
|
|
41
|
+
// In-flight runs (fire-and-forget). `idle()` awaits them — used by tests; harmless in prod.
|
|
42
|
+
pending = /* @__PURE__ */ new Set();
|
|
43
|
+
/** Await all in-flight scheduled runs (test helper; the daemon never needs to block on these). */
|
|
44
|
+
async idle() {
|
|
45
|
+
await Promise.all([...this.pending]);
|
|
46
|
+
}
|
|
47
|
+
static minuteKey(d) {
|
|
48
|
+
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}-${d.getHours()}-${d.getMinutes()}`;
|
|
49
|
+
}
|
|
50
|
+
/** Fire all scheduled workflows whose cron matches `now`. Returns the names fired (for tests/logs). */
|
|
51
|
+
tick(now) {
|
|
52
|
+
const key = WorkflowScheduler.minuteKey(now);
|
|
53
|
+
if (key === this.lastMinuteKey) return [];
|
|
54
|
+
this.lastMinuteKey = key;
|
|
55
|
+
const fired = [];
|
|
56
|
+
const seen = /* @__PURE__ */ new Set();
|
|
57
|
+
for (const dir of this.deps.projectRoots()) {
|
|
58
|
+
let root;
|
|
59
|
+
try {
|
|
60
|
+
root = resolveProjectRoot(dir);
|
|
61
|
+
} catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (seen.has(root)) continue;
|
|
65
|
+
seen.add(root);
|
|
66
|
+
let workflows;
|
|
67
|
+
try {
|
|
68
|
+
workflows = listWorkflows(root);
|
|
69
|
+
} catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
for (const wf of workflows) {
|
|
73
|
+
if (!isWorkflowEnabled(wf)) continue;
|
|
74
|
+
const crons = workflowCrons(wf);
|
|
75
|
+
if (!crons.length) continue;
|
|
76
|
+
if (crons.some((c) => cronMatchesSafe(c, now))) {
|
|
77
|
+
this.fire(root, wf);
|
|
78
|
+
fired.push(wf.name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return fired;
|
|
83
|
+
}
|
|
84
|
+
fire(root, wf) {
|
|
85
|
+
const owner = wf.session || null;
|
|
86
|
+
this.deps.log?.(`[workflow] schedule fired "${wf.name}" in ${root}${owner ? ` as ${owner}` : ""}`);
|
|
87
|
+
const p = runWorkflow(root, wf, {
|
|
88
|
+
trigger: "schedule",
|
|
89
|
+
actor: owner,
|
|
90
|
+
execStep: this.deps.execStep,
|
|
91
|
+
now: this.deps.now,
|
|
92
|
+
log: this.deps.log
|
|
93
|
+
}).catch((e) => {
|
|
94
|
+
this.deps.log?.(`[workflow] scheduled run threw for "${wf.name}": ${e?.message || e}`);
|
|
95
|
+
return void 0;
|
|
96
|
+
});
|
|
97
|
+
this.pending.add(p);
|
|
98
|
+
p.finally(() => this.pending.delete(p));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { WorkflowScheduler, cronMatchesSafe };
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
|
|
3
|
+
function getFlag(args, flag) {
|
|
4
|
+
const idx = args.indexOf(flag);
|
|
5
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
|
|
6
|
+
}
|
|
7
|
+
function hasFlag(args, ...flags) {
|
|
8
|
+
return flags.some((f) => args.includes(f));
|
|
9
|
+
}
|
|
10
|
+
function positionalArgs(args) {
|
|
11
|
+
const result = [];
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
if (args[i].startsWith("-")) {
|
|
14
|
+
if (!args[i].startsWith("--no-") && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
15
|
+
i++;
|
|
16
|
+
}
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
result.push(args[i]);
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
async function handleServeCommand() {
|
|
24
|
+
const args = process.argv.slice(3);
|
|
25
|
+
const sub = args[0];
|
|
26
|
+
let machineId;
|
|
27
|
+
const mIdx = args.findIndex((a) => a === "--machine" || a === "-m");
|
|
28
|
+
if (mIdx !== -1 && mIdx + 1 < args.length) {
|
|
29
|
+
machineId = args[mIdx + 1];
|
|
30
|
+
}
|
|
31
|
+
const filteredArgs = args.filter((_a, i) => {
|
|
32
|
+
if (args[i] === "--machine" || args[i] === "-m") return false;
|
|
33
|
+
if (i > 0 && (args[i - 1] === "--machine" || args[i - 1] === "-m")) return false;
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
if (sub === "--help" || sub === "-h") {
|
|
37
|
+
printServeHelp();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (sub === "remove" || sub === "rm") {
|
|
41
|
+
await serveRemove(filteredArgs.slice(1), machineId);
|
|
42
|
+
} else if (sub === "list" || sub === "ls") {
|
|
43
|
+
await serveList(filteredArgs.slice(1), machineId);
|
|
44
|
+
} else if (sub === "info") {
|
|
45
|
+
await serveInfo(machineId);
|
|
46
|
+
} else if (sub === "add") {
|
|
47
|
+
await serveAdd(filteredArgs.slice(1), machineId);
|
|
48
|
+
} else if (sub === "apply") {
|
|
49
|
+
await serveApply(filteredArgs.slice(1), machineId);
|
|
50
|
+
} else if (sub && !sub.startsWith("-")) {
|
|
51
|
+
await serveAdd(filteredArgs, machineId);
|
|
52
|
+
} else {
|
|
53
|
+
printServeHelp();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function serveAdd(args, machineId) {
|
|
57
|
+
const { connectAndGetMachine } = await import('./commands-CnlQyDxb.mjs');
|
|
58
|
+
const pos = positionalArgs(args);
|
|
59
|
+
const name = pos[0];
|
|
60
|
+
if (!name) {
|
|
61
|
+
console.error("Usage: svamp serve [add] <name> [directory] [--public | --owner | --access email1,email2]");
|
|
62
|
+
console.error(" Default: capability URL (link mode) \u2014 anyone with the URL can view, no login required.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
const directory = path.resolve(pos[1] || ".");
|
|
66
|
+
let access = "link";
|
|
67
|
+
if (hasFlag(args, "--public")) {
|
|
68
|
+
access = "public";
|
|
69
|
+
} else if (hasFlag(args, "--owner")) {
|
|
70
|
+
access = "owner";
|
|
71
|
+
} else {
|
|
72
|
+
const accessFlag = getFlag(args, "--access");
|
|
73
|
+
if (accessFlag) {
|
|
74
|
+
access = accessFlag.split(",").map((e) => e.trim()).filter(Boolean);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const { machine, server } = await connectAndGetMachine(machineId);
|
|
78
|
+
try {
|
|
79
|
+
const result = await machine.serveAdd({ name, directory, access });
|
|
80
|
+
console.log(`Mount added: ${name} \u2192 ${directory}`);
|
|
81
|
+
const accessLabel = access === "public" ? "public (no auth)" : access === "link" ? "link (capability URL \u2014 anyone with the URL can view)" : access === "owner" ? "owner only (Hypha login)" : `${access.join(", ")} (Hypha login)`;
|
|
82
|
+
console.log(`Access: ${accessLabel}`);
|
|
83
|
+
console.log(`URL: ${result.url}`);
|
|
84
|
+
if (access === "link") {
|
|
85
|
+
console.log(`(URL embeds a long capability token; keep it secret to keep the mount private.)`);
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(`Error: ${err.message || err}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
} finally {
|
|
91
|
+
await server.disconnect().catch(() => {
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function serveApply(args, machineId) {
|
|
96
|
+
const { connectAndGetMachine } = await import('./commands-CnlQyDxb.mjs');
|
|
97
|
+
const fs = await import('fs');
|
|
98
|
+
const yaml = await import('yaml');
|
|
99
|
+
const file = positionalArgs(args)[0];
|
|
100
|
+
if (!file) {
|
|
101
|
+
console.error("Usage: svamp serve apply <yaml-or-json-file> [--machine <id>]");
|
|
102
|
+
console.error("");
|
|
103
|
+
console.error("YAML format:");
|
|
104
|
+
console.error(" name: my-app");
|
|
105
|
+
console.error(" # Static mount:");
|
|
106
|
+
console.error(" directory: ./public");
|
|
107
|
+
console.error(" # OR managed-process mount:");
|
|
108
|
+
console.error(" process:");
|
|
109
|
+
console.error(" command: npm");
|
|
110
|
+
console.error(" args: [start]");
|
|
111
|
+
console.error(" port: 3000");
|
|
112
|
+
console.error(" workdir: .");
|
|
113
|
+
console.error(" warmup_path: / # default '/'");
|
|
114
|
+
console.error(" warmup_timeout_ms: 30000");
|
|
115
|
+
console.error(" idle_timeout_sec: 600 # 0 = always running");
|
|
116
|
+
console.error(" wake_on_request: true # spawn lazily on first request");
|
|
117
|
+
console.error(" access: link # link (default) | public | owner | [emails]");
|
|
118
|
+
console.error(" # link \u2014 capability URL, anyone with the URL can view");
|
|
119
|
+
console.error(" # public \u2014 fully open, short URL");
|
|
120
|
+
console.error(" # owner / [emails] \u2014 Hypha login required");
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
if (!fs.existsSync(file)) {
|
|
124
|
+
console.error(`Error: file not found: ${file}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = file.endsWith(".json") ? JSON.parse(raw) : yaml.parse(raw);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error(`Error: failed to parse ${file}: ${err.message}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
if (!parsed || typeof parsed !== "object") {
|
|
136
|
+
console.error("Error: top-level config must be an object");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
if (!parsed.name) {
|
|
140
|
+
console.error("Error: name is required");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
if (!parsed.directory && !parsed.process) {
|
|
144
|
+
console.error("Error: must specify either directory (static) or process (managed)");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
const proc = parsed.process ? {
|
|
148
|
+
command: parsed.process.command,
|
|
149
|
+
args: parsed.process.args,
|
|
150
|
+
port: parsed.process.port,
|
|
151
|
+
workdir: parsed.process.workdir ? path.resolve(parsed.process.workdir) : void 0,
|
|
152
|
+
env: parsed.process.env,
|
|
153
|
+
warmupPath: parsed.process.warmupPath ?? parsed.process.warmup_path,
|
|
154
|
+
warmupTimeoutMs: parsed.process.warmupTimeoutMs ?? parsed.process.warmup_timeout_ms,
|
|
155
|
+
idleTimeoutSec: parsed.process.idleTimeoutSec ?? parsed.process.idle_timeout_sec,
|
|
156
|
+
wakeOnRequest: parsed.process.wakeOnRequest ?? parsed.process.wake_on_request
|
|
157
|
+
} : void 0;
|
|
158
|
+
const params = {
|
|
159
|
+
name: parsed.name,
|
|
160
|
+
directory: parsed.directory ? path.resolve(parsed.directory) : void 0,
|
|
161
|
+
process: proc,
|
|
162
|
+
sessionId: parsed.sessionId ?? parsed.session_id,
|
|
163
|
+
access: parsed.access ?? "link",
|
|
164
|
+
// default flipped from 'owner' to 'link' (capability URL)
|
|
165
|
+
ownerEmail: parsed.ownerEmail ?? parsed.owner_email
|
|
166
|
+
};
|
|
167
|
+
const { machine, server } = await connectAndGetMachine(machineId);
|
|
168
|
+
try {
|
|
169
|
+
const result = await machine.serveApply(params);
|
|
170
|
+
const kind = proc ? "managed" : "static";
|
|
171
|
+
const what = proc ? `${proc.command}${proc.args?.length ? " " + proc.args.join(" ") : ""} (port ${proc.port})` : params.directory;
|
|
172
|
+
console.log(`Mount applied (${kind}): ${params.name} \u2192 ${what}`);
|
|
173
|
+
if (proc?.wakeOnRequest) console.log("Wake-on-request: enabled (process starts on first incoming request)");
|
|
174
|
+
if (proc?.idleTimeoutSec) console.log(`Idle timeout: ${proc.idleTimeoutSec}s`);
|
|
175
|
+
console.log(`URL: ${result.url}`);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(`Error: ${err.message || err}`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
} finally {
|
|
180
|
+
await server.disconnect().catch(() => {
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function serveRemove(args, machineId) {
|
|
185
|
+
const { connectAndGetMachine } = await import('./commands-CnlQyDxb.mjs');
|
|
186
|
+
const pos = positionalArgs(args);
|
|
187
|
+
const name = pos[0];
|
|
188
|
+
if (!name) {
|
|
189
|
+
console.error("Usage: svamp serve remove <name>");
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
const { machine, server } = await connectAndGetMachine(machineId);
|
|
193
|
+
try {
|
|
194
|
+
await machine.serveRemove({ name });
|
|
195
|
+
console.log(`Mount '${name}' removed.`);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(`Error: ${err.message || err}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
} finally {
|
|
200
|
+
await server.disconnect().catch(() => {
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function serveList(args, machineId) {
|
|
205
|
+
const { connectAndGetMachine } = await import('./commands-CnlQyDxb.mjs');
|
|
206
|
+
const all = hasFlag(args, "--all", "-a");
|
|
207
|
+
const json = hasFlag(args, "--json");
|
|
208
|
+
const sessionId = getFlag(args, "--session");
|
|
209
|
+
const { machine, server } = await connectAndGetMachine(machineId);
|
|
210
|
+
try {
|
|
211
|
+
const mounts = await machine.serveList({ sessionId, all });
|
|
212
|
+
if (json) {
|
|
213
|
+
console.log(JSON.stringify(mounts, null, 2));
|
|
214
|
+
} else if (mounts.length === 0) {
|
|
215
|
+
console.log(all ? "No mounts registered." : "No mounts for this session. Use --all to see all mounts.");
|
|
216
|
+
} else {
|
|
217
|
+
const label = all ? "All mounts" : `Mounts${sessionId ? ` (session ${sessionId.slice(0, 8)})` : ""}`;
|
|
218
|
+
console.log(`${label}:
|
|
219
|
+
`);
|
|
220
|
+
for (const m of mounts) {
|
|
221
|
+
const session = m.sessionId ? m.sessionId.slice(0, 8) : "-";
|
|
222
|
+
console.log(` ${m.name}`);
|
|
223
|
+
console.log(` Directory: ${m.directory}`);
|
|
224
|
+
console.log(` Session: ${session}`);
|
|
225
|
+
console.log(` Added: ${new Date(m.addedAt).toISOString().slice(0, 16).replace("T", " ")}`);
|
|
226
|
+
console.log("");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.error(`Error: ${err.message || err}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
} finally {
|
|
233
|
+
await server.disconnect().catch(() => {
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function serveInfo(machineId) {
|
|
238
|
+
const { connectAndGetMachine } = await import('./commands-CnlQyDxb.mjs');
|
|
239
|
+
const { machine, server } = await connectAndGetMachine(machineId);
|
|
240
|
+
try {
|
|
241
|
+
const info = await machine.serveInfo();
|
|
242
|
+
console.log(`Static file server:`);
|
|
243
|
+
console.log(` Status: ${info.running ? "running" : "stopped"}`);
|
|
244
|
+
console.log(` Port: ${info.port || "-"}`);
|
|
245
|
+
console.log(` URL: ${info.url || "(not exposed)"}`);
|
|
246
|
+
console.log(` Mounts: ${info.mountCount}`);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.error(`Error: ${err.message || err}`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
} finally {
|
|
251
|
+
await server.disconnect().catch(() => {
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function printServeHelp() {
|
|
256
|
+
console.log(`
|
|
257
|
+
svamp serve \u2014 Shared static file server
|
|
258
|
+
|
|
259
|
+
Serve local directories via a single daemon-managed HTTP server with one public URL.
|
|
260
|
+
Multiple sessions can register different mount points without port conflicts.
|
|
261
|
+
|
|
262
|
+
Usage:
|
|
263
|
+
svamp serve <name> [directory] Add a mount and print its URL (dir defaults to .)
|
|
264
|
+
svamp serve add <name> [directory] Same as above
|
|
265
|
+
svamp serve apply <yaml> Apply a declarative mount config (idempotent).
|
|
266
|
+
Supports static and managed-process mounts
|
|
267
|
+
with wake-on-request and idle timeout.
|
|
268
|
+
svamp serve remove <name> Remove a mount
|
|
269
|
+
svamp serve list [--all] [--json] List mounts (default: current session only)
|
|
270
|
+
svamp serve info Show server status and URL
|
|
271
|
+
|
|
272
|
+
Access tiers (default: link):
|
|
273
|
+
(no flag) Capability URL \u2014 anyone with the URL can view.
|
|
274
|
+
URL embeds a long random token (~192-bit entropy)
|
|
275
|
+
as the first path segment. No login required.
|
|
276
|
+
Best for embedding in agent artifact iframes.
|
|
277
|
+
--public Fully public \u2014 no auth, short URL.
|
|
278
|
+
--owner Hypha login required, owner-only.
|
|
279
|
+
--access email1,email2 Hypha login required, specific allowed emails.
|
|
280
|
+
|
|
281
|
+
Options:
|
|
282
|
+
-m, --machine <id> Target a specific machine
|
|
283
|
+
--session <id> Filter by session ID
|
|
284
|
+
--all, -a Show mounts from all sessions
|
|
285
|
+
--json Output as JSON
|
|
286
|
+
|
|
287
|
+
Examples:
|
|
288
|
+
svamp serve my-report ./output # Default: capability URL
|
|
289
|
+
svamp serve dashboard ./dist --public # Anyone, short URL
|
|
290
|
+
svamp serve data ./csv --owner # Hypha login required
|
|
291
|
+
svamp serve data ./csv --access a@x.com,b@y.com # Specific Hypha users
|
|
292
|
+
svamp serve apply my-app.yaml # Declarative apply
|
|
293
|
+
svamp serve list --all # Show all mounts
|
|
294
|
+
svamp serve remove my-report # Stop serving
|
|
295
|
+
|
|
296
|
+
Declarative apply (svamp serve apply <yaml>):
|
|
297
|
+
name: my-app
|
|
298
|
+
process:
|
|
299
|
+
command: npm
|
|
300
|
+
args: [start]
|
|
301
|
+
port: 3000
|
|
302
|
+
workdir: .
|
|
303
|
+
warmup_path: /
|
|
304
|
+
idle_timeout_sec: 600
|
|
305
|
+
wake_on_request: true
|
|
306
|
+
access: public
|
|
307
|
+
`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export { handleServeCommand };
|