svamp-cli 0.2.46 → 0.2.48
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-Bqjcj1EX.mjs → agentCommands-BuGwfYhd.mjs} +2 -2
- package/dist/cli.mjs +32 -32
- package/dist/{commands-Cs5-T_lh.mjs → commands-BJR_98XX.mjs} +2 -2
- package/dist/{commands-B6zZtCuh.mjs → commands-JWrmpGcs.mjs} +1 -1
- package/dist/{commands-DsCkxa3k.mjs → commands-TyAIFJx-.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{package-CyXoMaC5.mjs → package-DVfaovNL.mjs} +1 -1
- package/dist/{run-wfhhtkl1.mjs → run-6umeTX-K.mjs} +40 -1
- package/dist/{run-yJ1mtT8S.mjs → run-DR7E3IZL.mjs} +1 -1
- package/dist/{serveCommands-DhtNhaur.mjs → serveCommands-FUE8m232.mjs} +107 -4
- package/dist/{serveManager-CUcu_V3q.mjs → serveManager-RvRL-weX.mjs} +260 -25
- package/package.json +1 -1
|
@@ -148,7 +148,7 @@ async function sessionBroadcast(action, args) {
|
|
|
148
148
|
console.log(`Broadcast sent: ${action}`);
|
|
149
149
|
}
|
|
150
150
|
async function connectToMachineService() {
|
|
151
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
151
|
+
const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
|
|
152
152
|
return connectAndGetMachine();
|
|
153
153
|
}
|
|
154
154
|
async function inboxSend(targetSessionId, opts) {
|
|
@@ -165,7 +165,7 @@ async function inboxSend(targetSessionId, opts) {
|
|
|
165
165
|
}
|
|
166
166
|
const { server, machine } = await connectToMachineService();
|
|
167
167
|
try {
|
|
168
|
-
const { resolveSessionId } = await import('./commands-
|
|
168
|
+
const { resolveSessionId } = await import('./commands-JWrmpGcs.mjs');
|
|
169
169
|
const sessions = await machine.listSessions();
|
|
170
170
|
const match = resolveSessionId(sessions, targetSessionId);
|
|
171
171
|
const fullTargetId = match.sessionId;
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { s as startDaemon, b as stopDaemon, d as daemonStatus } from './run-
|
|
1
|
+
import { s as startDaemon, b as stopDaemon, d as daemonStatus } from './run-6umeTX-K.mjs';
|
|
2
2
|
import 'os';
|
|
3
3
|
import 'fs/promises';
|
|
4
4
|
import 'fs';
|
|
@@ -42,7 +42,7 @@ async function main() {
|
|
|
42
42
|
console.error(`svamp daemon restart: ${err.message || err}`);
|
|
43
43
|
process.exit(1);
|
|
44
44
|
}
|
|
45
|
-
const { restartDaemon } = await import('./run-
|
|
45
|
+
const { restartDaemon } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.o; });
|
|
46
46
|
await restartDaemon();
|
|
47
47
|
process.exit(0);
|
|
48
48
|
}
|
|
@@ -277,7 +277,7 @@ async function main() {
|
|
|
277
277
|
console.error("svamp service: Service commands are not available in sandboxed sessions.");
|
|
278
278
|
process.exit(1);
|
|
279
279
|
}
|
|
280
|
-
const { handleServiceCommand } = await import('./commands-
|
|
280
|
+
const { handleServiceCommand } = await import('./commands-TyAIFJx-.mjs');
|
|
281
281
|
await handleServiceCommand();
|
|
282
282
|
} else if (subcommand === "serve") {
|
|
283
283
|
const { isSandboxed: isSandboxedServe } = await import('./sandboxDetect-DNTcbgWD.mjs');
|
|
@@ -285,7 +285,7 @@ async function main() {
|
|
|
285
285
|
console.error("svamp serve: Serve commands are not available in sandboxed sessions.");
|
|
286
286
|
process.exit(1);
|
|
287
287
|
}
|
|
288
|
-
const { handleServeCommand } = await import('./serveCommands-
|
|
288
|
+
const { handleServeCommand } = await import('./serveCommands-FUE8m232.mjs');
|
|
289
289
|
await handleServeCommand();
|
|
290
290
|
process.exit(0);
|
|
291
291
|
} else if (subcommand === "process" || subcommand === "proc") {
|
|
@@ -294,7 +294,7 @@ async function main() {
|
|
|
294
294
|
console.error("svamp process: Process commands are not available in sandboxed sessions.");
|
|
295
295
|
process.exit(1);
|
|
296
296
|
}
|
|
297
|
-
const { processCommand } = await import('./commands-
|
|
297
|
+
const { processCommand } = await import('./commands-BJR_98XX.mjs');
|
|
298
298
|
let machineId;
|
|
299
299
|
const processArgs = args.slice(1);
|
|
300
300
|
const mIdx = processArgs.findIndex((a) => a === "--machine" || a === "-m");
|
|
@@ -312,7 +312,7 @@ async function main() {
|
|
|
312
312
|
} else if (!subcommand || subcommand === "start") {
|
|
313
313
|
await handleInteractiveCommand();
|
|
314
314
|
} else if (subcommand === "--version" || subcommand === "-v") {
|
|
315
|
-
const pkg = await import('./package-
|
|
315
|
+
const pkg = await import('./package-DVfaovNL.mjs').catch(() => ({ default: { version: "unknown" } }));
|
|
316
316
|
console.log(`svamp version: ${pkg.default.version}`);
|
|
317
317
|
} else {
|
|
318
318
|
console.error(`Unknown command: ${subcommand}`);
|
|
@@ -321,7 +321,7 @@ async function main() {
|
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
async function handleInteractiveCommand() {
|
|
324
|
-
const { runInteractive } = await import('./run-
|
|
324
|
+
const { runInteractive } = await import('./run-DR7E3IZL.mjs');
|
|
325
325
|
const interactiveArgs = subcommand === "start" ? args.slice(1) : args;
|
|
326
326
|
let directory = process.cwd();
|
|
327
327
|
let resumeSessionId;
|
|
@@ -366,7 +366,7 @@ async function handleAgentCommand() {
|
|
|
366
366
|
return;
|
|
367
367
|
}
|
|
368
368
|
if (agentArgs[0] === "list") {
|
|
369
|
-
const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-
|
|
369
|
+
const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.j; });
|
|
370
370
|
console.log("Known agents:");
|
|
371
371
|
for (const [name, config2] of Object.entries(KNOWN_ACP_AGENTS)) {
|
|
372
372
|
console.log(` ${name.padEnd(12)} ${config2.command} ${config2.args.join(" ")} (ACP)`);
|
|
@@ -378,7 +378,7 @@ async function handleAgentCommand() {
|
|
|
378
378
|
console.log('Use "svamp agent -- <command> [args]" for a custom ACP agent.');
|
|
379
379
|
return;
|
|
380
380
|
}
|
|
381
|
-
const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-
|
|
381
|
+
const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.j; });
|
|
382
382
|
let cwd = process.cwd();
|
|
383
383
|
const filteredArgs = [];
|
|
384
384
|
for (let i = 0; i < agentArgs.length; i++) {
|
|
@@ -402,12 +402,12 @@ async function handleAgentCommand() {
|
|
|
402
402
|
console.log(`Starting ${config.agentName} agent in ${cwd}...`);
|
|
403
403
|
let backend;
|
|
404
404
|
if (KNOWN_MCP_AGENTS[config.agentName]) {
|
|
405
|
-
const { CodexMcpBackend } = await import('./run-
|
|
405
|
+
const { CodexMcpBackend } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.k; });
|
|
406
406
|
backend = new CodexMcpBackend({ cwd, log: logFn });
|
|
407
407
|
} else {
|
|
408
|
-
const { AcpBackend } = await import('./run-
|
|
409
|
-
const { GeminiTransport } = await import('./run-
|
|
410
|
-
const { DefaultTransport } = await import('./run-
|
|
408
|
+
const { AcpBackend } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.i; });
|
|
409
|
+
const { GeminiTransport } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.G; });
|
|
410
|
+
const { DefaultTransport } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.D; });
|
|
411
411
|
const transportHandler = config.agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(config.agentName);
|
|
412
412
|
backend = new AcpBackend({
|
|
413
413
|
agentName: config.agentName,
|
|
@@ -534,7 +534,7 @@ async function handleSessionCommand() {
|
|
|
534
534
|
process.exit(1);
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
|
-
const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-
|
|
537
|
+
const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-JWrmpGcs.mjs');
|
|
538
538
|
const parseFlagStr = (flag, shortFlag) => {
|
|
539
539
|
for (let i = 1; i < sessionArgs.length; i++) {
|
|
540
540
|
if ((sessionArgs[i] === flag || shortFlag) && i + 1 < sessionArgs.length) {
|
|
@@ -594,7 +594,7 @@ async function handleSessionCommand() {
|
|
|
594
594
|
allowDomain.push(sessionArgs[++i]);
|
|
595
595
|
}
|
|
596
596
|
}
|
|
597
|
-
const { parseShareArg } = await import('./commands-
|
|
597
|
+
const { parseShareArg } = await import('./commands-JWrmpGcs.mjs');
|
|
598
598
|
const shareEntries = share.map((s) => parseShareArg(s));
|
|
599
599
|
await sessionSpawn(agent, dir, targetMachineId, {
|
|
600
600
|
message,
|
|
@@ -680,7 +680,7 @@ async function handleSessionCommand() {
|
|
|
680
680
|
console.error("Usage: svamp session approve <session-id> [request-id] [--json]");
|
|
681
681
|
process.exit(1);
|
|
682
682
|
}
|
|
683
|
-
const { sessionApprove } = await import('./commands-
|
|
683
|
+
const { sessionApprove } = await import('./commands-JWrmpGcs.mjs');
|
|
684
684
|
const approveReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
|
|
685
685
|
await sessionApprove(sessionArgs[1], approveReqId, targetMachineId, {
|
|
686
686
|
json: hasFlag("--json")
|
|
@@ -690,7 +690,7 @@ async function handleSessionCommand() {
|
|
|
690
690
|
console.error("Usage: svamp session deny <session-id> [request-id] [--json]");
|
|
691
691
|
process.exit(1);
|
|
692
692
|
}
|
|
693
|
-
const { sessionDeny } = await import('./commands-
|
|
693
|
+
const { sessionDeny } = await import('./commands-JWrmpGcs.mjs');
|
|
694
694
|
const denyReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
|
|
695
695
|
await sessionDeny(sessionArgs[1], denyReqId, targetMachineId, {
|
|
696
696
|
json: hasFlag("--json")
|
|
@@ -726,7 +726,7 @@ async function handleSessionCommand() {
|
|
|
726
726
|
console.error("Usage: svamp session set-title <title>");
|
|
727
727
|
process.exit(1);
|
|
728
728
|
}
|
|
729
|
-
const { sessionSetTitle } = await import('./agentCommands-
|
|
729
|
+
const { sessionSetTitle } = await import('./agentCommands-BuGwfYhd.mjs');
|
|
730
730
|
await sessionSetTitle(title);
|
|
731
731
|
} else if (sessionSubcommand === "set-link") {
|
|
732
732
|
const url = sessionArgs[1];
|
|
@@ -735,7 +735,7 @@ async function handleSessionCommand() {
|
|
|
735
735
|
process.exit(1);
|
|
736
736
|
}
|
|
737
737
|
const label = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
|
|
738
|
-
const { sessionSetLink } = await import('./agentCommands-
|
|
738
|
+
const { sessionSetLink } = await import('./agentCommands-BuGwfYhd.mjs');
|
|
739
739
|
await sessionSetLink(url, label);
|
|
740
740
|
} else if (sessionSubcommand === "notify") {
|
|
741
741
|
const message = sessionArgs[1];
|
|
@@ -744,7 +744,7 @@ async function handleSessionCommand() {
|
|
|
744
744
|
process.exit(1);
|
|
745
745
|
}
|
|
746
746
|
const level = parseFlagStr("--level") || "info";
|
|
747
|
-
const { sessionNotify } = await import('./agentCommands-
|
|
747
|
+
const { sessionNotify } = await import('./agentCommands-BuGwfYhd.mjs');
|
|
748
748
|
await sessionNotify(message, level);
|
|
749
749
|
} else if (sessionSubcommand === "broadcast") {
|
|
750
750
|
const action = sessionArgs[1];
|
|
@@ -752,7 +752,7 @@ async function handleSessionCommand() {
|
|
|
752
752
|
console.error("Usage: svamp session broadcast <action> [args...]\nActions: open-canvas <url> [label], close-canvas, toast <message>");
|
|
753
753
|
process.exit(1);
|
|
754
754
|
}
|
|
755
|
-
const { sessionBroadcast } = await import('./agentCommands-
|
|
755
|
+
const { sessionBroadcast } = await import('./agentCommands-BuGwfYhd.mjs');
|
|
756
756
|
await sessionBroadcast(action, sessionArgs.slice(2).filter((a) => !a.startsWith("--")));
|
|
757
757
|
} else if (sessionSubcommand === "inbox") {
|
|
758
758
|
const inboxSubcmd = sessionArgs[1];
|
|
@@ -763,7 +763,7 @@ async function handleSessionCommand() {
|
|
|
763
763
|
process.exit(1);
|
|
764
764
|
}
|
|
765
765
|
if (agentSessionId) {
|
|
766
|
-
const { inboxSend } = await import('./agentCommands-
|
|
766
|
+
const { inboxSend } = await import('./agentCommands-BuGwfYhd.mjs');
|
|
767
767
|
await inboxSend(sessionArgs[2], {
|
|
768
768
|
body: sessionArgs[3],
|
|
769
769
|
subject: parseFlagStr("--subject"),
|
|
@@ -778,7 +778,7 @@ async function handleSessionCommand() {
|
|
|
778
778
|
}
|
|
779
779
|
} else if (inboxSubcmd === "list" || inboxSubcmd === "ls") {
|
|
780
780
|
if (agentSessionId && !sessionArgs[2]) {
|
|
781
|
-
const { inboxList } = await import('./agentCommands-
|
|
781
|
+
const { inboxList } = await import('./agentCommands-BuGwfYhd.mjs');
|
|
782
782
|
await inboxList({
|
|
783
783
|
unread: hasFlag("--unread"),
|
|
784
784
|
limit: parseFlagInt("--limit"),
|
|
@@ -800,7 +800,7 @@ async function handleSessionCommand() {
|
|
|
800
800
|
process.exit(1);
|
|
801
801
|
}
|
|
802
802
|
if (agentSessionId && !sessionArgs[3]) {
|
|
803
|
-
const { inboxList } = await import('./agentCommands-
|
|
803
|
+
const { inboxList } = await import('./agentCommands-BuGwfYhd.mjs');
|
|
804
804
|
await sessionInboxRead(agentSessionId, sessionArgs[2], targetMachineId);
|
|
805
805
|
} else if (sessionArgs[3]) {
|
|
806
806
|
await sessionInboxRead(sessionArgs[2], sessionArgs[3], targetMachineId);
|
|
@@ -810,7 +810,7 @@ async function handleSessionCommand() {
|
|
|
810
810
|
}
|
|
811
811
|
} else if (inboxSubcmd === "reply") {
|
|
812
812
|
if (agentSessionId && sessionArgs[2] && sessionArgs[3] && !sessionArgs[4]) {
|
|
813
|
-
const { inboxReply } = await import('./agentCommands-
|
|
813
|
+
const { inboxReply } = await import('./agentCommands-BuGwfYhd.mjs');
|
|
814
814
|
await inboxReply(sessionArgs[2], sessionArgs[3]);
|
|
815
815
|
} else if (sessionArgs[2] && sessionArgs[3] && sessionArgs[4]) {
|
|
816
816
|
await sessionInboxReply(sessionArgs[2], sessionArgs[3], sessionArgs[4], targetMachineId);
|
|
@@ -846,7 +846,7 @@ async function handleMachineCommand() {
|
|
|
846
846
|
return;
|
|
847
847
|
}
|
|
848
848
|
if (machineSubcommand === "share") {
|
|
849
|
-
const { machineShare } = await import('./commands-
|
|
849
|
+
const { machineShare } = await import('./commands-JWrmpGcs.mjs');
|
|
850
850
|
let machineId;
|
|
851
851
|
const shareArgs = [];
|
|
852
852
|
for (let i = 1; i < machineArgs.length; i++) {
|
|
@@ -876,7 +876,7 @@ async function handleMachineCommand() {
|
|
|
876
876
|
}
|
|
877
877
|
await machineShare(machineId, { add, remove, list, configPath, showConfig });
|
|
878
878
|
} else if (machineSubcommand === "exec") {
|
|
879
|
-
const { machineExec } = await import('./commands-
|
|
879
|
+
const { machineExec } = await import('./commands-JWrmpGcs.mjs');
|
|
880
880
|
let machineId;
|
|
881
881
|
let cwd;
|
|
882
882
|
const cmdParts = [];
|
|
@@ -896,7 +896,7 @@ async function handleMachineCommand() {
|
|
|
896
896
|
}
|
|
897
897
|
await machineExec(machineId, command, cwd);
|
|
898
898
|
} else if (machineSubcommand === "info") {
|
|
899
|
-
const { machineInfo } = await import('./commands-
|
|
899
|
+
const { machineInfo } = await import('./commands-JWrmpGcs.mjs');
|
|
900
900
|
let machineId;
|
|
901
901
|
for (let i = 1; i < machineArgs.length; i++) {
|
|
902
902
|
if ((machineArgs[i] === "--machine" || machineArgs[i] === "-m") && i + 1 < machineArgs.length) {
|
|
@@ -916,10 +916,10 @@ async function handleMachineCommand() {
|
|
|
916
916
|
level = machineArgs[++i];
|
|
917
917
|
}
|
|
918
918
|
}
|
|
919
|
-
const { machineNotify } = await import('./agentCommands-
|
|
919
|
+
const { machineNotify } = await import('./agentCommands-BuGwfYhd.mjs');
|
|
920
920
|
await machineNotify(message, level);
|
|
921
921
|
} else if (machineSubcommand === "ls") {
|
|
922
|
-
const { machineLs } = await import('./commands-
|
|
922
|
+
const { machineLs } = await import('./commands-JWrmpGcs.mjs');
|
|
923
923
|
let machineId;
|
|
924
924
|
let showHidden = false;
|
|
925
925
|
let path;
|
|
@@ -1381,7 +1381,7 @@ async function applyClaudeAuthFlags(argv) {
|
|
|
1381
1381
|
"--use-hypha-proxy, --use-claude-login, and --anthropic-base-url/--anthropic-api-key are mutually exclusive"
|
|
1382
1382
|
);
|
|
1383
1383
|
}
|
|
1384
|
-
const mod = await import('./run-
|
|
1384
|
+
const mod = await import('./run-6umeTX-K.mjs').then(function (n) { return n.n; });
|
|
1385
1385
|
if (hasHypha) {
|
|
1386
1386
|
mod.setClaudeAuthHyphaProxy();
|
|
1387
1387
|
console.log("Claude auth configured: hypha-proxy (uses HYPHA_TOKEN live at each spawn).");
|
|
@@ -1404,7 +1404,7 @@ async function applyClaudeAuthFlags(argv) {
|
|
|
1404
1404
|
}
|
|
1405
1405
|
async function handleDaemonAuthCommand(argv) {
|
|
1406
1406
|
const sub = (argv[0] || "status").toLowerCase();
|
|
1407
|
-
const mod = await import('./run-
|
|
1407
|
+
const mod = await import('./run-6umeTX-K.mjs').then(function (n) { return n.n; });
|
|
1408
1408
|
if (sub === "--help" || sub === "-h" || sub === "help") {
|
|
1409
1409
|
console.log(`
|
|
1410
1410
|
svamp daemon auth \u2014 Configure how Claude subprocesses authenticate
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { writeFileSync, readFileSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
-
import { connectAndGetMachine } from './commands-
|
|
3
|
+
import { connectAndGetMachine } from './commands-JWrmpGcs.mjs';
|
|
4
4
|
import 'node:fs';
|
|
5
5
|
import 'node:child_process';
|
|
6
6
|
import 'node:path';
|
|
7
7
|
import 'node:os';
|
|
8
|
-
import './run-
|
|
8
|
+
import './run-6umeTX-K.mjs';
|
|
9
9
|
import 'os';
|
|
10
10
|
import 'fs/promises';
|
|
11
11
|
import 'url';
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
3
|
import { resolve, join } from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
-
import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-
|
|
5
|
+
import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-6umeTX-K.mjs';
|
|
6
6
|
import 'os';
|
|
7
7
|
import 'fs/promises';
|
|
8
8
|
import 'fs';
|
|
@@ -97,7 +97,7 @@ async function serviceServe(args) {
|
|
|
97
97
|
}
|
|
98
98
|
async function serviceList(_args) {
|
|
99
99
|
try {
|
|
100
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
100
|
+
const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
|
|
101
101
|
const { server, machine } = await connectAndGetMachine();
|
|
102
102
|
try {
|
|
103
103
|
const tunnels = await machine.tunnelList({});
|
|
@@ -126,7 +126,7 @@ async function serviceDelete(args) {
|
|
|
126
126
|
process.exit(1);
|
|
127
127
|
}
|
|
128
128
|
try {
|
|
129
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
129
|
+
const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
|
|
130
130
|
const { server, machine } = await connectAndGetMachine();
|
|
131
131
|
try {
|
|
132
132
|
await machine.tunnelStop({ name });
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-
|
|
1
|
+
export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-6umeTX-K.mjs';
|
|
2
2
|
import 'os';
|
|
3
3
|
import 'fs/promises';
|
|
4
4
|
import 'fs';
|
|
@@ -1149,6 +1149,26 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
1149
1149
|
const access = params.access || "owner";
|
|
1150
1150
|
return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail);
|
|
1151
1151
|
},
|
|
1152
|
+
/**
|
|
1153
|
+
* Apply a mount declaratively. Replaces existing mount with same name.
|
|
1154
|
+
* Supports static (directory) and managed-process (process) mounts.
|
|
1155
|
+
* Used by `svamp serve apply <yaml>`.
|
|
1156
|
+
*/
|
|
1157
|
+
serveApply: async (params, context) => {
|
|
1158
|
+
authorizeRequest(context, currentMetadata.sharing, "interact");
|
|
1159
|
+
const sm = handlers.serveManager;
|
|
1160
|
+
if (!sm) throw new Error("Serve manager not available");
|
|
1161
|
+
const ownerEmail = params.ownerEmail || context?.user?.email || void 0;
|
|
1162
|
+
const access = params.access ?? "owner";
|
|
1163
|
+
return sm.applyMount({
|
|
1164
|
+
name: params.name,
|
|
1165
|
+
directory: params.directory,
|
|
1166
|
+
process: params.process,
|
|
1167
|
+
sessionId: params.sessionId,
|
|
1168
|
+
access,
|
|
1169
|
+
ownerEmail
|
|
1170
|
+
});
|
|
1171
|
+
},
|
|
1152
1172
|
/** Remove a mount from the shared static file server. */
|
|
1153
1173
|
serveRemove: async (params, context) => {
|
|
1154
1174
|
authorizeRequest(context, currentMetadata.sharing, "interact");
|
|
@@ -1325,6 +1345,19 @@ function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
|
|
|
1325
1345
|
return { messages: [], hasMore: false };
|
|
1326
1346
|
}
|
|
1327
1347
|
}
|
|
1348
|
+
function countMessagesOnDisk(messagesDir, fallbackInMemory) {
|
|
1349
|
+
const filePath = join$1(messagesDir, "messages.jsonl");
|
|
1350
|
+
if (!existsSync(filePath)) return fallbackInMemory;
|
|
1351
|
+
try {
|
|
1352
|
+
const data = readFileSync(filePath, "utf-8");
|
|
1353
|
+
let count = 0;
|
|
1354
|
+
for (let i = 0; i < data.length; i++) if (data.charCodeAt(i) === 10) count++;
|
|
1355
|
+
if (data.length > 0 && data[data.length - 1] !== "\n") count++;
|
|
1356
|
+
return count;
|
|
1357
|
+
} catch {
|
|
1358
|
+
return fallbackInMemory;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1328
1361
|
function appendMessage(messagesDir, sessionId, msg) {
|
|
1329
1362
|
try {
|
|
1330
1363
|
const filePath = join$1(messagesDir, "messages.jsonl");
|
|
@@ -1464,6 +1497,12 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1464
1497
|
hasMore: filtered.length > lim
|
|
1465
1498
|
};
|
|
1466
1499
|
},
|
|
1500
|
+
getMessageCount: async (context) => {
|
|
1501
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1502
|
+
const latestSeq = nextSeq - 1;
|
|
1503
|
+
const count = options?.messagesDir ? countMessagesOnDisk(options.messagesDir, messages.length) : messages.length;
|
|
1504
|
+
return { count, latestSeq };
|
|
1505
|
+
},
|
|
1467
1506
|
getLatestMessages: async (beforeSeq, limit, context) => {
|
|
1468
1507
|
authorizeRequest(context, metadata.sharing, "view");
|
|
1469
1508
|
const lim = Math.min(limit ?? 100, 500);
|
|
@@ -6874,7 +6913,7 @@ async function startDaemon(options) {
|
|
|
6874
6913
|
const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
|
|
6875
6914
|
await supervisor.init();
|
|
6876
6915
|
const tunnels = /* @__PURE__ */ new Map();
|
|
6877
|
-
const { ServeManager } = await import('./serveManager-
|
|
6916
|
+
const { ServeManager } = await import('./serveManager-RvRL-weX.mjs');
|
|
6878
6917
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
6879
6918
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
6880
6919
|
});
|
|
@@ -2,7 +2,7 @@ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(im
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { resolve, join } from 'node:path';
|
|
4
4
|
import { existsSync, readFileSync, watch } from 'node:fs';
|
|
5
|
-
import { c as connectToHypha, a as registerSessionService, h as generateHookSettings } from './run-
|
|
5
|
+
import { c as connectToHypha, a as registerSessionService, h as generateHookSettings } from './run-6umeTX-K.mjs';
|
|
6
6
|
import { createServer } from 'node:http';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
@@ -45,6 +45,8 @@ async function handleServeCommand() {
|
|
|
45
45
|
await serveInfo(machineId);
|
|
46
46
|
} else if (sub === "add") {
|
|
47
47
|
await serveAdd(filteredArgs.slice(1), machineId);
|
|
48
|
+
} else if (sub === "apply") {
|
|
49
|
+
await serveApply(filteredArgs.slice(1), machineId);
|
|
48
50
|
} else if (sub && !sub.startsWith("-")) {
|
|
49
51
|
await serveAdd(filteredArgs, machineId);
|
|
50
52
|
} else {
|
|
@@ -52,7 +54,7 @@ async function handleServeCommand() {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
async function serveAdd(args, machineId) {
|
|
55
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
57
|
+
const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
|
|
56
58
|
const pos = positionalArgs(args);
|
|
57
59
|
const name = pos[0];
|
|
58
60
|
if (!name) {
|
|
@@ -83,8 +85,93 @@ async function serveAdd(args, machineId) {
|
|
|
83
85
|
});
|
|
84
86
|
}
|
|
85
87
|
}
|
|
88
|
+
async function serveApply(args, machineId) {
|
|
89
|
+
const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
|
|
90
|
+
const fs = await import('fs');
|
|
91
|
+
const yaml = await import('yaml');
|
|
92
|
+
const file = positionalArgs(args)[0];
|
|
93
|
+
if (!file) {
|
|
94
|
+
console.error("Usage: svamp serve apply <yaml-or-json-file> [--machine <id>]");
|
|
95
|
+
console.error("");
|
|
96
|
+
console.error("YAML format:");
|
|
97
|
+
console.error(" name: my-app");
|
|
98
|
+
console.error(" # Static mount:");
|
|
99
|
+
console.error(" directory: ./public");
|
|
100
|
+
console.error(" # OR managed-process mount:");
|
|
101
|
+
console.error(" process:");
|
|
102
|
+
console.error(" command: npm");
|
|
103
|
+
console.error(" args: [start]");
|
|
104
|
+
console.error(" port: 3000");
|
|
105
|
+
console.error(" workdir: .");
|
|
106
|
+
console.error(" warmup_path: / # default '/'");
|
|
107
|
+
console.error(" warmup_timeout_ms: 30000");
|
|
108
|
+
console.error(" idle_timeout_sec: 600 # 0 = always running");
|
|
109
|
+
console.error(" wake_on_request: true # spawn lazily on first request");
|
|
110
|
+
console.error(" access: public # public | owner (default) | [emails]");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
if (!fs.existsSync(file)) {
|
|
114
|
+
console.error(`Error: file not found: ${file}`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = file.endsWith(".json") ? JSON.parse(raw) : yaml.parse(raw);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error(`Error: failed to parse ${file}: ${err.message}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
if (!parsed || typeof parsed !== "object") {
|
|
126
|
+
console.error("Error: top-level config must be an object");
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
if (!parsed.name) {
|
|
130
|
+
console.error("Error: name is required");
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
if (!parsed.directory && !parsed.process) {
|
|
134
|
+
console.error("Error: must specify either directory (static) or process (managed)");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const proc = parsed.process ? {
|
|
138
|
+
command: parsed.process.command,
|
|
139
|
+
args: parsed.process.args,
|
|
140
|
+
port: parsed.process.port,
|
|
141
|
+
workdir: parsed.process.workdir ? path.resolve(parsed.process.workdir) : void 0,
|
|
142
|
+
env: parsed.process.env,
|
|
143
|
+
warmupPath: parsed.process.warmupPath ?? parsed.process.warmup_path,
|
|
144
|
+
warmupTimeoutMs: parsed.process.warmupTimeoutMs ?? parsed.process.warmup_timeout_ms,
|
|
145
|
+
idleTimeoutSec: parsed.process.idleTimeoutSec ?? parsed.process.idle_timeout_sec,
|
|
146
|
+
wakeOnRequest: parsed.process.wakeOnRequest ?? parsed.process.wake_on_request
|
|
147
|
+
} : void 0;
|
|
148
|
+
const params = {
|
|
149
|
+
name: parsed.name,
|
|
150
|
+
directory: parsed.directory ? path.resolve(parsed.directory) : void 0,
|
|
151
|
+
process: proc,
|
|
152
|
+
sessionId: parsed.sessionId ?? parsed.session_id,
|
|
153
|
+
access: parsed.access ?? "owner",
|
|
154
|
+
ownerEmail: parsed.ownerEmail ?? parsed.owner_email
|
|
155
|
+
};
|
|
156
|
+
const { machine, server } = await connectAndGetMachine(machineId);
|
|
157
|
+
try {
|
|
158
|
+
const result = await machine.serveApply(params);
|
|
159
|
+
const kind = proc ? "managed" : "static";
|
|
160
|
+
const what = proc ? `${proc.command}${proc.args?.length ? " " + proc.args.join(" ") : ""} (port ${proc.port})` : params.directory;
|
|
161
|
+
console.log(`Mount applied (${kind}): ${params.name} \u2192 ${what}`);
|
|
162
|
+
if (proc?.wakeOnRequest) console.log("Wake-on-request: enabled (process starts on first incoming request)");
|
|
163
|
+
if (proc?.idleTimeoutSec) console.log(`Idle timeout: ${proc.idleTimeoutSec}s`);
|
|
164
|
+
console.log(`URL: ${result.url}`);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error(`Error: ${err.message || err}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
} finally {
|
|
169
|
+
await server.disconnect().catch(() => {
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
86
173
|
async function serveRemove(args, machineId) {
|
|
87
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
174
|
+
const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
|
|
88
175
|
const pos = positionalArgs(args);
|
|
89
176
|
const name = pos[0];
|
|
90
177
|
if (!name) {
|
|
@@ -104,7 +191,7 @@ async function serveRemove(args, machineId) {
|
|
|
104
191
|
}
|
|
105
192
|
}
|
|
106
193
|
async function serveList(args, machineId) {
|
|
107
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
194
|
+
const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
|
|
108
195
|
const all = hasFlag(args, "--all", "-a");
|
|
109
196
|
const json = hasFlag(args, "--json");
|
|
110
197
|
const sessionId = getFlag(args, "--session");
|
|
@@ -137,7 +224,7 @@ async function serveList(args, machineId) {
|
|
|
137
224
|
}
|
|
138
225
|
}
|
|
139
226
|
async function serveInfo(machineId) {
|
|
140
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
227
|
+
const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
|
|
141
228
|
const { machine, server } = await connectAndGetMachine(machineId);
|
|
142
229
|
try {
|
|
143
230
|
const info = await machine.serveInfo();
|
|
@@ -164,6 +251,9 @@ Multiple sessions can register different mount points without port conflicts.
|
|
|
164
251
|
Usage:
|
|
165
252
|
svamp serve <name> [directory] Add a mount and print its URL (dir defaults to .)
|
|
166
253
|
svamp serve add <name> [directory] Same as above
|
|
254
|
+
svamp serve apply <yaml> Apply a declarative mount config (idempotent).
|
|
255
|
+
Supports static and managed-process mounts
|
|
256
|
+
with wake-on-request and idle timeout.
|
|
167
257
|
svamp serve remove <name> Remove a mount
|
|
168
258
|
svamp serve list [--all] [--json] List mounts (default: current session only)
|
|
169
259
|
svamp serve info Show server status and URL
|
|
@@ -183,8 +273,21 @@ Examples:
|
|
|
183
273
|
svamp serve my-report ./output # Owner-only (default)
|
|
184
274
|
svamp serve dashboard ./dist --public # Anyone can access
|
|
185
275
|
svamp serve data ./csv --access a@x.com,b@y.com # Specific users
|
|
276
|
+
svamp serve apply my-app.yaml # Declarative apply
|
|
186
277
|
svamp serve list --all # Show all mounts
|
|
187
278
|
svamp serve remove my-report # Stop serving
|
|
279
|
+
|
|
280
|
+
Declarative apply (svamp serve apply <yaml>):
|
|
281
|
+
name: my-app
|
|
282
|
+
process:
|
|
283
|
+
command: npm
|
|
284
|
+
args: [start]
|
|
285
|
+
port: 3000
|
|
286
|
+
workdir: .
|
|
287
|
+
warmup_path: /
|
|
288
|
+
idle_timeout_sec: 600
|
|
289
|
+
wake_on_request: true
|
|
290
|
+
access: public
|
|
188
291
|
`);
|
|
189
292
|
}
|
|
190
293
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
1
2
|
import * as fs from 'fs';
|
|
2
3
|
import * as http from 'http';
|
|
3
4
|
import * as net from 'net';
|
|
@@ -281,6 +282,10 @@ class ServeManager {
|
|
|
281
282
|
caddy = null;
|
|
282
283
|
proxyServer = null;
|
|
283
284
|
auth = null;
|
|
285
|
+
/** Live child processes for managed mounts. Keyed by mount name. */
|
|
286
|
+
managedProcs = /* @__PURE__ */ new Map();
|
|
287
|
+
/** Single timer that scans managed mounts every 30s for idle eviction. */
|
|
288
|
+
idleTimer = null;
|
|
284
289
|
persistFile;
|
|
285
290
|
log;
|
|
286
291
|
hyphaServerUrl;
|
|
@@ -292,35 +297,72 @@ class ServeManager {
|
|
|
292
297
|
}
|
|
293
298
|
// ── Public API ───────────────────────────────────────────────────────
|
|
294
299
|
/**
|
|
295
|
-
* Add a mount
|
|
296
|
-
*
|
|
300
|
+
* Add a static mount (backward-compatible thin wrapper around applyMount).
|
|
301
|
+
* Throws if a mount with the same name already exists, preserving the
|
|
302
|
+
* pre-existing semantics that callers may depend on.
|
|
297
303
|
*/
|
|
298
304
|
async addMount(name, directory, sessionId, access = "owner", ownerEmail) {
|
|
299
|
-
validateMountName(name);
|
|
300
|
-
const resolvedDir = path.resolve(directory);
|
|
301
|
-
if (!fs.existsSync(resolvedDir)) {
|
|
302
|
-
throw new Error(`Path does not exist: ${resolvedDir}`);
|
|
303
|
-
}
|
|
304
305
|
if (this.mounts.has(name)) {
|
|
305
306
|
throw new Error(`Mount '${name}' already exists. Remove it first or choose a different name.`);
|
|
306
307
|
}
|
|
308
|
+
return this.applyMount({ name, directory, sessionId, access, ownerEmail });
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Apply a mount declaratively. Unifies static and managed-process mounts:
|
|
312
|
+
* pass `directory` for static, `process` for managed (or both — `process`
|
|
313
|
+
* takes precedence for routing). Replaces an existing mount with the same
|
|
314
|
+
* name (idempotent), so repeated `svamp serve apply` is safe.
|
|
315
|
+
*
|
|
316
|
+
* Backward-compatible: existing `addMount(name, directory, ...)` callers
|
|
317
|
+
* still work and produce the same result as `applyMount({name, directory, ...})`.
|
|
318
|
+
*/
|
|
319
|
+
async applyMount(spec) {
|
|
320
|
+
validateMountName(spec.name);
|
|
321
|
+
if (!spec.directory && !spec.process) {
|
|
322
|
+
throw new Error(`Mount '${spec.name}': must specify either directory (static) or process (managed)`);
|
|
323
|
+
}
|
|
324
|
+
if (spec.process) {
|
|
325
|
+
if (!spec.process.command) {
|
|
326
|
+
throw new Error(`Mount '${spec.name}': process.command is required`);
|
|
327
|
+
}
|
|
328
|
+
if (!spec.process.port || spec.process.port <= 0) {
|
|
329
|
+
throw new Error(`Mount '${spec.name}': process.port must be a positive integer`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const resolvedDir = spec.directory ? path.resolve(spec.directory) : void 0;
|
|
333
|
+
if (resolvedDir && !fs.existsSync(resolvedDir)) {
|
|
334
|
+
throw new Error(`Path does not exist: ${resolvedDir}`);
|
|
335
|
+
}
|
|
336
|
+
if (this.mounts.has(spec.name)) {
|
|
337
|
+
await this.removeMount(spec.name);
|
|
338
|
+
}
|
|
307
339
|
const mount = {
|
|
308
|
-
name,
|
|
340
|
+
name: spec.name,
|
|
309
341
|
directory: resolvedDir,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
342
|
+
process: spec.process,
|
|
343
|
+
sessionId: spec.sessionId,
|
|
344
|
+
ownerEmail: spec.ownerEmail,
|
|
345
|
+
access: spec.access ?? "owner",
|
|
313
346
|
addedAt: Date.now()
|
|
314
347
|
};
|
|
315
|
-
this.mounts.set(name, mount);
|
|
348
|
+
this.mounts.set(spec.name, mount);
|
|
316
349
|
await this.ensureRunning();
|
|
317
|
-
if (this.caddy?.isRunning) {
|
|
318
|
-
await this.caddy.addMount(name, resolvedDir);
|
|
350
|
+
if (resolvedDir && this.caddy?.isRunning) {
|
|
351
|
+
await this.caddy.addMount(spec.name, resolvedDir);
|
|
319
352
|
}
|
|
320
|
-
await this.startMountTunnel(name);
|
|
353
|
+
await this.startMountTunnel(spec.name);
|
|
354
|
+
if (spec.process && !spec.process.wakeOnRequest) {
|
|
355
|
+
try {
|
|
356
|
+
await this.ensureManagedRunning(spec.name);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
this.log(`Mount '${spec.name}': initial process start failed: ${err.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
this.ensureIdleTimer();
|
|
321
362
|
this.persist();
|
|
322
|
-
const url = this.getMountUrl(name);
|
|
323
|
-
|
|
363
|
+
const url = this.getMountUrl(spec.name);
|
|
364
|
+
const what = spec.process ? `${spec.process.command}${spec.process.args?.length ? " " + spec.process.args.join(" ") : ""} (port ${spec.process.port})` : resolvedDir;
|
|
365
|
+
this.log(`Mount applied: ${spec.name} \u2192 ${what} (${url ?? "tunnel pending"})`);
|
|
324
366
|
return { url: url || "", mount };
|
|
325
367
|
}
|
|
326
368
|
/**
|
|
@@ -331,6 +373,8 @@ class ServeManager {
|
|
|
331
373
|
throw new Error(`Mount '${name}' not found`);
|
|
332
374
|
}
|
|
333
375
|
this.mounts.delete(name);
|
|
376
|
+
await this.stopManagedProcess(name).catch(() => {
|
|
377
|
+
});
|
|
334
378
|
const tunnel = this.mountTunnels.get(name);
|
|
335
379
|
if (tunnel) {
|
|
336
380
|
try {
|
|
@@ -397,12 +441,16 @@ class ServeManager {
|
|
|
397
441
|
if (!raw.mounts || raw.mounts.length === 0) return;
|
|
398
442
|
let restoredCount = 0;
|
|
399
443
|
for (const m of raw.mounts) {
|
|
400
|
-
if (fs.existsSync(m.directory)) {
|
|
401
|
-
this.mounts.set(m.name, m);
|
|
402
|
-
restoredCount++;
|
|
403
|
-
} else {
|
|
444
|
+
if (m.directory && !fs.existsSync(m.directory)) {
|
|
404
445
|
this.log(`Skipping mount '${m.name}': directory no longer exists (${m.directory})`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (!m.directory && !m.process) {
|
|
449
|
+
this.log(`Skipping mount '${m.name}': neither directory nor process configured`);
|
|
450
|
+
continue;
|
|
405
451
|
}
|
|
452
|
+
this.mounts.set(m.name, m);
|
|
453
|
+
restoredCount++;
|
|
406
454
|
}
|
|
407
455
|
if (restoredCount > 0) {
|
|
408
456
|
this.log(`Restoring ${restoredCount} mount(s)...`);
|
|
@@ -413,7 +461,15 @@ class ServeManager {
|
|
|
413
461
|
} catch (err) {
|
|
414
462
|
this.log(`Failed to start tunnel for restored mount '${m.name}': ${err.message}`);
|
|
415
463
|
}
|
|
464
|
+
if (m.process && !m.process.wakeOnRequest) {
|
|
465
|
+
try {
|
|
466
|
+
await this.ensureManagedRunning(m.name);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
this.log(`Restored managed process '${m.name}' failed to start: ${err.message}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
416
471
|
}
|
|
472
|
+
this.ensureIdleTimer();
|
|
417
473
|
this.persist();
|
|
418
474
|
}
|
|
419
475
|
} catch (err) {
|
|
@@ -421,9 +477,18 @@ class ServeManager {
|
|
|
421
477
|
}
|
|
422
478
|
}
|
|
423
479
|
/**
|
|
424
|
-
* Shut down auth proxy + Caddy + all per-mount frpc tunnels
|
|
480
|
+
* Shut down auth proxy + Caddy + all per-mount frpc tunnels + all
|
|
481
|
+
* managed processes.
|
|
425
482
|
*/
|
|
426
483
|
async shutdown() {
|
|
484
|
+
for (const name of Array.from(this.managedProcs.keys())) {
|
|
485
|
+
await this.stopManagedProcess(name).catch(() => {
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (this.idleTimer) {
|
|
489
|
+
clearInterval(this.idleTimer);
|
|
490
|
+
this.idleTimer = null;
|
|
491
|
+
}
|
|
427
492
|
for (const [name, tunnel] of this.mountTunnels.entries()) {
|
|
428
493
|
try {
|
|
429
494
|
tunnel.destroy();
|
|
@@ -444,6 +509,141 @@ class ServeManager {
|
|
|
444
509
|
}
|
|
445
510
|
this.auth?.destroy();
|
|
446
511
|
}
|
|
512
|
+
// ── Managed-process lifecycle ─────────────────────────────────────────
|
|
513
|
+
/**
|
|
514
|
+
* Ensure the managed process for a mount is running and warm. If a warmup
|
|
515
|
+
* is already in flight, awaits the same promise (no duplicate spawns).
|
|
516
|
+
* Throws if warmup probe never returns 200/3xx within warmupTimeoutMs.
|
|
517
|
+
*/
|
|
518
|
+
async ensureManagedRunning(name) {
|
|
519
|
+
const mount = this.mounts.get(name);
|
|
520
|
+
if (!mount?.process) return;
|
|
521
|
+
const handle = this.managedProcs.get(name);
|
|
522
|
+
if (handle && handle.warmupPromise) {
|
|
523
|
+
return handle.warmupPromise;
|
|
524
|
+
}
|
|
525
|
+
if (handle && handle.child.exitCode === null && !handle.child.killed) {
|
|
526
|
+
handle.lastRequestAt = Date.now();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const cfg = mount.process;
|
|
530
|
+
const child = spawn(cfg.command, cfg.args ?? [], {
|
|
531
|
+
cwd: cfg.workdir ?? process.cwd(),
|
|
532
|
+
env: { ...process.env, ...cfg.env ?? {} },
|
|
533
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
534
|
+
detached: false
|
|
535
|
+
});
|
|
536
|
+
child.stdout?.on("data", (d) => {
|
|
537
|
+
const line = d.toString().trimEnd();
|
|
538
|
+
if (line) this.log(`[${name}] ${line}`);
|
|
539
|
+
});
|
|
540
|
+
child.stderr?.on("data", (d) => {
|
|
541
|
+
const line = d.toString().trimEnd();
|
|
542
|
+
if (line) this.log(`[${name}] ${line}`);
|
|
543
|
+
});
|
|
544
|
+
child.on("exit", (code, signal) => {
|
|
545
|
+
this.log(`Managed process '${name}' exited (code=${code}, signal=${signal})`);
|
|
546
|
+
const h = this.managedProcs.get(name);
|
|
547
|
+
if (h && h.child === child) this.managedProcs.delete(name);
|
|
548
|
+
});
|
|
549
|
+
const warmupPath = cfg.warmupPath ?? "/";
|
|
550
|
+
const warmupTimeoutMs = cfg.warmupTimeoutMs ?? 3e4;
|
|
551
|
+
const warmupPromise = this.warmupProbe(`http://127.0.0.1:${cfg.port}${warmupPath}`, warmupTimeoutMs).then(() => {
|
|
552
|
+
const h = this.managedProcs.get(name);
|
|
553
|
+
if (h) {
|
|
554
|
+
h.warmupPromise = null;
|
|
555
|
+
h.lastRequestAt = Date.now();
|
|
556
|
+
}
|
|
557
|
+
this.log(`Managed process '${name}' ready on 127.0.0.1:${cfg.port}`);
|
|
558
|
+
}).catch((err) => {
|
|
559
|
+
this.log(`Managed process '${name}' warmup failed: ${err.message}`);
|
|
560
|
+
try {
|
|
561
|
+
child.kill("SIGTERM");
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
this.managedProcs.delete(name);
|
|
565
|
+
throw err;
|
|
566
|
+
});
|
|
567
|
+
const newHandle = {
|
|
568
|
+
child,
|
|
569
|
+
pid: child.pid ?? 0,
|
|
570
|
+
lastRequestAt: Date.now(),
|
|
571
|
+
warmupPromise
|
|
572
|
+
};
|
|
573
|
+
this.managedProcs.set(name, newHandle);
|
|
574
|
+
this.log(`Managed process '${name}' starting: ${cfg.command} ${(cfg.args ?? []).join(" ")} (port ${cfg.port})`);
|
|
575
|
+
return warmupPromise;
|
|
576
|
+
}
|
|
577
|
+
/** Poll a URL until it returns <500 or the deadline passes. */
|
|
578
|
+
async warmupProbe(url, timeoutMs) {
|
|
579
|
+
const deadline = Date.now() + timeoutMs;
|
|
580
|
+
let lastErr;
|
|
581
|
+
while (Date.now() < deadline) {
|
|
582
|
+
try {
|
|
583
|
+
const ctrl = new AbortController();
|
|
584
|
+
const t = setTimeout(() => ctrl.abort(), 2e3);
|
|
585
|
+
let resp;
|
|
586
|
+
try {
|
|
587
|
+
resp = await fetch(url, {
|
|
588
|
+
method: "GET",
|
|
589
|
+
signal: ctrl.signal,
|
|
590
|
+
redirect: "manual"
|
|
591
|
+
});
|
|
592
|
+
} finally {
|
|
593
|
+
clearTimeout(t);
|
|
594
|
+
}
|
|
595
|
+
if (resp.status < 500) return;
|
|
596
|
+
lastErr = new Error(`status ${resp.status}`);
|
|
597
|
+
} catch (err) {
|
|
598
|
+
lastErr = err;
|
|
599
|
+
}
|
|
600
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
601
|
+
}
|
|
602
|
+
throw new Error(`warmup probe ${url} did not respond within ${timeoutMs}ms (${lastErr?.message || "no response"})`);
|
|
603
|
+
}
|
|
604
|
+
/** Stop a managed process (SIGTERM + SIGKILL after 5s). */
|
|
605
|
+
async stopManagedProcess(name) {
|
|
606
|
+
const h = this.managedProcs.get(name);
|
|
607
|
+
if (!h) return;
|
|
608
|
+
this.managedProcs.delete(name);
|
|
609
|
+
try {
|
|
610
|
+
h.child.kill("SIGTERM");
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
const child = h.child;
|
|
614
|
+
await new Promise((resolve) => {
|
|
615
|
+
const t = setTimeout(() => {
|
|
616
|
+
try {
|
|
617
|
+
child.kill("SIGKILL");
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
resolve();
|
|
621
|
+
}, 5e3);
|
|
622
|
+
child.once("exit", () => {
|
|
623
|
+
clearTimeout(t);
|
|
624
|
+
resolve();
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
this.log(`Managed process '${name}' stopped`);
|
|
628
|
+
}
|
|
629
|
+
/** Idle eviction loop — stops processes that have been idle longer than configured. */
|
|
630
|
+
ensureIdleTimer() {
|
|
631
|
+
if (this.idleTimer) return;
|
|
632
|
+
this.idleTimer = setInterval(() => {
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
for (const [name, mount] of this.mounts) {
|
|
635
|
+
const cfg = mount.process;
|
|
636
|
+
if (!cfg || !cfg.idleTimeoutSec || cfg.idleTimeoutSec <= 0) continue;
|
|
637
|
+
const h = this.managedProcs.get(name);
|
|
638
|
+
if (!h || h.warmupPromise) continue;
|
|
639
|
+
if (now - h.lastRequestAt >= cfg.idleTimeoutSec * 1e3) {
|
|
640
|
+
this.log(`Idle eviction: stopping '${name}' (idle ${Math.round((now - h.lastRequestAt) / 1e3)}s \u2265 ${cfg.idleTimeoutSec}s)`);
|
|
641
|
+
this.stopManagedProcess(name).catch(() => {
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}, 3e4);
|
|
646
|
+
}
|
|
447
647
|
// ── Internal ─────────────────────────────────────────────────────────
|
|
448
648
|
/** Get the public URL for a mount (mount-specific subdomain). */
|
|
449
649
|
getMountUrl(name) {
|
|
@@ -485,7 +685,9 @@ class ServeManager {
|
|
|
485
685
|
log: (msg) => this.log(`[Caddy] ${msg}`)
|
|
486
686
|
});
|
|
487
687
|
for (const mount of this.mounts.values()) {
|
|
488
|
-
|
|
688
|
+
if (mount.directory) {
|
|
689
|
+
await this.caddy.addMount(mount.name, mount.directory);
|
|
690
|
+
}
|
|
489
691
|
}
|
|
490
692
|
await this.caddy.start();
|
|
491
693
|
this.log(`Caddy file server started on 127.0.0.1:${this.caddyPort}`);
|
|
@@ -548,7 +750,7 @@ class ServeManager {
|
|
|
548
750
|
return;
|
|
549
751
|
}
|
|
550
752
|
}
|
|
551
|
-
if (req.method === "PUT" && mount) {
|
|
753
|
+
if (req.method === "PUT" && mount && mount.directory) {
|
|
552
754
|
const filePath = path.join(mount.directory, basePath);
|
|
553
755
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
554
756
|
const ws = fs.createWriteStream(filePath);
|
|
@@ -563,7 +765,7 @@ class ServeManager {
|
|
|
563
765
|
});
|
|
564
766
|
return;
|
|
565
767
|
}
|
|
566
|
-
if (req.method === "DELETE" && mount) {
|
|
768
|
+
if (req.method === "DELETE" && mount && mount.directory) {
|
|
567
769
|
const filePath = path.join(mount.directory, basePath);
|
|
568
770
|
try {
|
|
569
771
|
fs.unlinkSync(filePath);
|
|
@@ -575,6 +777,39 @@ class ServeManager {
|
|
|
575
777
|
}
|
|
576
778
|
return;
|
|
577
779
|
}
|
|
780
|
+
if (mount && mount.process) {
|
|
781
|
+
const cfg = mount.process;
|
|
782
|
+
if (cfg.wakeOnRequest || !this.managedProcs.has(mount.name)) {
|
|
783
|
+
try {
|
|
784
|
+
await this.ensureManagedRunning(mount.name);
|
|
785
|
+
} catch (err) {
|
|
786
|
+
res.writeHead(503, { "Content-Type": "text/plain" });
|
|
787
|
+
res.end(`Backend not ready: ${err?.message || err}`);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const handle = this.managedProcs.get(mount.name);
|
|
792
|
+
if (handle) handle.lastRequestAt = Date.now();
|
|
793
|
+
const targetPath = mountResolvedByHost ? req.url || "/" : (basePath || "/") + (url.search || "");
|
|
794
|
+
const proxyReq2 = http.request({
|
|
795
|
+
hostname: "127.0.0.1",
|
|
796
|
+
port: cfg.port,
|
|
797
|
+
path: targetPath,
|
|
798
|
+
method: req.method,
|
|
799
|
+
headers: req.headers
|
|
800
|
+
}, (proxyRes) => {
|
|
801
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
802
|
+
proxyRes.pipe(res);
|
|
803
|
+
});
|
|
804
|
+
proxyReq2.on("error", (err) => {
|
|
805
|
+
if (!res.headersSent) {
|
|
806
|
+
res.writeHead(502);
|
|
807
|
+
res.end(`Backend error: ${err.message}`);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
req.pipe(proxyReq2);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
578
813
|
let proxyPath = req.url || "/";
|
|
579
814
|
if (mountResolvedByHost && mountName) {
|
|
580
815
|
const search = url.search || "";
|