volute 0.24.0 → 0.26.0
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/README.md +15 -20
- package/dist/{activity-events-4O37J7PD.js → activity-events-ZMBAKLUF.js} +2 -2
- package/dist/api.d.ts +590 -10
- package/dist/{auth-HM2RSPY7.js → auth-4TV573WE.js} +2 -2
- package/dist/{channel-HZOSHGNF.js → channel-ZVZV42UD.js} +3 -3
- package/dist/{chunk-NOBRGACV.js → chunk-2VO7453N.js} +56 -19
- package/dist/{chunk-OOW675I3.js → chunk-3CFRE2VC.js} +931 -775
- package/dist/{chunk-PHHKNGA3.js → chunk-3TV4GLFO.js} +2 -2
- package/dist/{chunk-4TJ72QQ3.js → chunk-5Y3PBKW6.js} +3 -3
- package/dist/{chunk-BFK6SOEJ.js → chunk-J2CO4WEV.js} +1 -1
- package/dist/{chunk-TQDITGES.js → chunk-LX22GRG7.js} +10 -13
- package/dist/{chunk-E7GOKNOT.js → chunk-NWI2425I.js} +1 -1
- package/dist/{chunk-2767L2RZ.js → chunk-OZFKBXD6.js} +1 -1
- package/dist/{chunk-XLC342FO.js → chunk-SIAG3QMM.js} +14 -1
- package/dist/{chunk-RVKR2R7F.js → chunk-SSI47XP2.js} +10 -2
- package/dist/chunk-TZKJLDQN.js +78 -0
- package/dist/{chunk-P3W36ZGD.js → chunk-USNBKHYG.js} +33 -5
- package/dist/chunk-UTL75LP6.js +113 -0
- package/dist/{chunk-3AIBT4TW.js → chunk-V63B7DX3.js} +24 -1
- package/dist/{chunk-33XAVCS4.js → chunk-WBHMQ5OZ.js} +49 -0
- package/dist/{chunk-TRQEV3CD.js → chunk-WGOGUMPO.js} +22 -3
- package/dist/chunk-XOXLRRR2.js +176 -0
- package/dist/{chunk-JTDFJWI2.js → chunk-YJA7P64S.js} +1 -1
- package/dist/chunk-ZYGKG6VC.js +22 -0
- package/dist/cli.js +44 -20
- package/dist/{cloud-sync-DIU3OCPV.js → cloud-sync-NI2K3C7G.js} +11 -9
- package/dist/{connector-M6XFI6GM.js → connector-G722WXAU.js} +4 -4
- package/dist/{create-VDQJER52.js → create-4YBRTTJS.js} +1 -1
- package/dist/{daemon-client-JOVQZ52X.js → daemon-client-Z7FAJ6JW.js} +1 -1
- package/dist/{daemon-restart-YMPEATQH.js → daemon-restart-BJZ3O4U4.js} +6 -5
- package/dist/daemon.js +982 -340
- package/dist/{delete-2MRR4JX5.js → delete-27OYNK25.js} +1 -1
- package/dist/{down-674SX2IZ.js → down-7UKFMJJZ.js} +4 -4
- package/dist/{env-2FPOZK37.js → env-M336ONDP.js} +4 -4
- package/dist/{export-IKFAPRAO.js → export-HP4G5DQC.js} +1 -1
- package/dist/{file-KT3UIQM3.js → file-HUDKTRAS.js} +3 -3
- package/dist/{history-46WZN5CN.js → history-B64GTFTD.js} +3 -3
- package/dist/{import-FRDPQPJ2.js → import-XIB7UV4S.js} +2 -2
- package/dist/{log-6SGSSR3D.js → log-PBFNILJ4.js} +3 -3
- package/dist/{login-UO6AOVEA.js → login-6U7U6BNG.js} +1 -1
- package/dist/login-B5E7N7MY.js +46 -0
- package/dist/logout-XSJRYS3U.js +39 -0
- package/dist/{logs-HRBONI5I.js → logs-3CART7O7.js} +3 -3
- package/dist/{merge-KSFJKX6T.js → merge-VK2HSKMA.js} +3 -3
- package/dist/{message-delivery-S7BCNV6Y.js → message-delivery-MS5JYPZX.js} +11 -9
- package/dist/{mind-KPLCRKQA.js → mind-HZ3QSDDJ.js} +17 -17
- package/dist/{mind-activity-tracker-NMDDEV3K.js → mind-activity-tracker-4G6FURY2.js} +3 -3
- package/dist/{mind-manager-ZNRIYEK3.js → mind-manager-VVK67AY3.js} +6 -4
- package/dist/{mind-sleep-GHPTSAYN.js → mind-sleep-DTV7L44D.js} +3 -3
- package/dist/{mind-wake-BJDJFMDF.js → mind-wake-PFN4FN3T.js} +3 -3
- package/dist/notes-37FW2UR2.js +230 -0
- package/dist/{package-S5YF25XV.js → package-VZWLXPHV.js} +3 -1
- package/dist/{pages-TWR6U7DS.js → pages-DIIT5HMQ.js} +1 -1
- package/dist/{publish-BZNHKUUK.js → publish-HQV7YREB.js} +4 -4
- package/dist/{pull-D32SPFVU.js → pull-2MB4SK3C.js} +3 -3
- package/dist/{register-U2UO6TC4.js → register-EFND67FQ.js} +1 -1
- package/dist/{restart-5BMNV7KU.js → restart-CCK7D6TV.js} +3 -3
- package/dist/sandbox-EHGFF52K.js +19 -0
- package/dist/{schedule-YEFDLVMJ.js → schedule-6F7ELB2M.js} +3 -3
- package/dist/{seed-6FEKB3YC.js → seed-E5OQGWX3.js} +1 -1
- package/dist/{send-IISDYFCL.js → send-IH6XZKPC.js} +6 -20
- package/dist/service-LLBV3R7M.js +122 -0
- package/dist/setup-F6TWFYGQ.js +371 -0
- package/dist/setup-YGAAIKKZ.js +17 -0
- package/dist/{shared-LWMNTTZN.js → shared-UMO4S7CC.js} +4 -4
- package/dist/{skill-BQOFACEI.js → skill-42LGFBQC.js} +13 -5
- package/dist/skills/dreaming/SKILL.md +68 -0
- package/dist/skills/dreaming/references/INSTALL.md +56 -0
- package/dist/skills/dreaming/scripts/dream.ts +289 -0
- package/dist/skills/dreaming/scripts/wake-context-dreams.sh +30 -0
- package/dist/skills/imagegen/SKILL.md +37 -0
- package/dist/skills/imagegen/references/INSTALL.md +13 -0
- package/dist/skills/imagegen/scripts/imagegen.ts +136 -0
- package/dist/skills/notes/SKILL.md +34 -0
- package/dist/skills/resonance/SKILL.md +73 -0
- package/dist/skills/resonance/assets/default-config.json +21 -0
- package/dist/skills/resonance/references/INSTALL.md +23 -0
- package/dist/skills/resonance/scripts/resonance.ts +1250 -0
- package/dist/skills/volute-mind/SKILL.md +23 -3
- package/dist/{sleep-manager-XXSWQQLE.js → sleep-manager-EE4NRN2Q.js} +11 -9
- package/dist/{sprout-CGSW4CF5.js → sprout-QL74KR2X.js} +5 -5
- package/dist/{start-C7XITZ5O.js → start-O5JQASRC.js} +3 -3
- package/dist/{status-SIRPLEZC.js → status-FZBEBM7Q.js} +3 -3
- package/dist/{status-LYS4NUOZ.js → status-WXD4HXRL.js} +3 -3
- package/dist/{stop-CVKBSLXY.js → stop-2SOG5NYF.js} +3 -3
- package/dist/up-SDMCSVI3.js +17 -0
- package/dist/{update-7XCZMYBT.js → update-5VUDAI3D.js} +6 -6
- package/dist/{upgrade-7RUIXGOO.js → upgrade-QCCO33BK.js} +1 -1
- package/dist/{variant-UGREB4G5.js → variant-WWLDY6D5.js} +4 -4
- package/dist/{version-notify-SZ75QRGO.js → version-notify-USFZBWMG.js} +11 -9
- package/dist/web-assets/assets/index-CUQ31ieL.js +69 -0
- package/dist/web-assets/assets/index-CW8NSl1o.css +1 -0
- package/dist/web-assets/favicon.png +0 -0
- package/dist/web-assets/index.html +5 -4
- package/dist/web-assets/logo.png +0 -0
- package/drizzle/0015_notes.sql +23 -0
- package/drizzle/0016_note_reactions_and_replies.sql +15 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +3 -1
- package/templates/_base/.init/.config/hooks/wake-context.sh +7 -0
- package/templates/_base/home/public/.gitkeep +0 -0
- package/templates/_base/src/lib/startup.ts +8 -0
- package/templates/claude/src/agent.ts +51 -1
- package/templates/claude/src/server.ts +1 -0
- package/templates/pi/package.json.tmpl +1 -0
- package/templates/pi/src/agent.ts +48 -1
- package/templates/pi/src/lib/subagents.ts +150 -0
- package/templates/pi/src/server.ts +1 -0
- package/dist/chunk-NWPT4ASZ.js +0 -89
- package/dist/service-FASYWLTC.js +0 -247
- package/dist/setup-BMLM2UTK.js +0 -230
- package/dist/up-OMHACRJL.js +0 -15
- package/dist/web-assets/assets/index-Bx9WDoaQ.js +0 -69
- package/dist/web-assets/assets/index-Clz8OhmJ.css +0 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
type AuthStorage,
|
|
4
|
+
bashTool,
|
|
5
|
+
codingTools,
|
|
6
|
+
createAgentSession,
|
|
7
|
+
DefaultResourceLoader,
|
|
8
|
+
type ExtensionFactory,
|
|
9
|
+
editTool,
|
|
10
|
+
type ModelRegistry,
|
|
11
|
+
readTool,
|
|
12
|
+
SessionManager,
|
|
13
|
+
SettingsManager,
|
|
14
|
+
writeTool,
|
|
15
|
+
} from "@mariozechner/pi-coding-agent";
|
|
16
|
+
import { Type } from "@sinclair/typebox";
|
|
17
|
+
import { log } from "./logger.js";
|
|
18
|
+
|
|
19
|
+
export type SubagentDefinition = {
|
|
20
|
+
description: string;
|
|
21
|
+
prompt: string;
|
|
22
|
+
tools?: string[]; // e.g. ["Read", "Write", "Bash"] — defaults to all coding tools
|
|
23
|
+
maxTurns?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function createSubagentExtension(
|
|
27
|
+
agents: Record<string, SubagentDefinition>,
|
|
28
|
+
context: {
|
|
29
|
+
cwd: string;
|
|
30
|
+
model: Model<any>;
|
|
31
|
+
authStorage: AuthStorage;
|
|
32
|
+
modelRegistry: ModelRegistry;
|
|
33
|
+
},
|
|
34
|
+
): ExtensionFactory {
|
|
35
|
+
return (pi) => {
|
|
36
|
+
for (const [name, def] of Object.entries(agents)) {
|
|
37
|
+
pi.registerTool({
|
|
38
|
+
name,
|
|
39
|
+
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
40
|
+
description: def.description,
|
|
41
|
+
parameters: Type.Object({
|
|
42
|
+
prompt: Type.String({ description: "The prompt for the subagent" }),
|
|
43
|
+
}),
|
|
44
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
45
|
+
try {
|
|
46
|
+
const tools = resolveTools(def.tools);
|
|
47
|
+
|
|
48
|
+
const loader = new DefaultResourceLoader({
|
|
49
|
+
cwd: context.cwd,
|
|
50
|
+
systemPromptOverride: () => def.prompt,
|
|
51
|
+
settingsManager: SettingsManager.inMemory({}),
|
|
52
|
+
});
|
|
53
|
+
await loader.reload();
|
|
54
|
+
|
|
55
|
+
const { session } = await createAgentSession({
|
|
56
|
+
cwd: context.cwd,
|
|
57
|
+
model: context.model,
|
|
58
|
+
tools,
|
|
59
|
+
resourceLoader: loader,
|
|
60
|
+
sessionManager: SessionManager.inMemory(),
|
|
61
|
+
settingsManager: SettingsManager.inMemory({}),
|
|
62
|
+
authStorage: context.authStorage,
|
|
63
|
+
modelRegistry: context.modelRegistry,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const textParts: string[] = [];
|
|
67
|
+
let turnCount = 0;
|
|
68
|
+
|
|
69
|
+
const done = new Promise<void>((resolve, reject) => {
|
|
70
|
+
const timeout = setTimeout(() => {
|
|
71
|
+
session.abort();
|
|
72
|
+
reject(new Error(`Subagent "${name}" timed out after 5 minutes`));
|
|
73
|
+
}, 300_000);
|
|
74
|
+
|
|
75
|
+
session.subscribe((event: any) => {
|
|
76
|
+
if (event.type === "agent_error") {
|
|
77
|
+
clearTimeout(timeout);
|
|
78
|
+
reject(
|
|
79
|
+
new Error(`Subagent "${name}" error: ${event.error?.message ?? "unknown"}`),
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (event.type === "turn_end") {
|
|
84
|
+
turnCount++;
|
|
85
|
+
if (def.maxTurns && turnCount >= def.maxTurns) {
|
|
86
|
+
session.abort();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (event.type === "agent_end") {
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
for (const msg of event.messages ?? []) {
|
|
92
|
+
if (msg.role === "assistant" && msg.content) {
|
|
93
|
+
for (const block of msg.content) {
|
|
94
|
+
if (block.type === "text") textParts.push(block.text);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
resolve();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await session.prompt(params.prompt);
|
|
104
|
+
await done;
|
|
105
|
+
|
|
106
|
+
log("mind", `subagent "${name}": completed after ${turnCount} turns`);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text" as const, text: textParts.join("\n") || "(no output)" }],
|
|
110
|
+
details: {},
|
|
111
|
+
};
|
|
112
|
+
} catch (err: any) {
|
|
113
|
+
log("mind", `subagent "${name}" failed: ${err.message}`);
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text" as const, text: `[subagent error] ${err.message}` }],
|
|
116
|
+
details: {},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const TOOL_MAP: Record<string, any> = {
|
|
126
|
+
Read: readTool,
|
|
127
|
+
Write: writeTool,
|
|
128
|
+
Bash: bashTool,
|
|
129
|
+
Edit: editTool,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function resolveTools(names: string[] | undefined) {
|
|
133
|
+
if (!names) return codingTools;
|
|
134
|
+
const resolved = names
|
|
135
|
+
.map((n) => {
|
|
136
|
+
if (!TOOL_MAP[n]) {
|
|
137
|
+
log(
|
|
138
|
+
"mind",
|
|
139
|
+
`unknown subagent tool "${n}" — available: ${Object.keys(TOOL_MAP).join(", ")}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return TOOL_MAP[n];
|
|
143
|
+
})
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
if (resolved.length === 0) {
|
|
146
|
+
log("mind", "no valid tools resolved for subagent, falling back to all coding tools");
|
|
147
|
+
return codingTools;
|
|
148
|
+
}
|
|
149
|
+
return resolved;
|
|
150
|
+
}
|
package/dist/chunk-NWPT4ASZ.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
validateMindName
|
|
4
|
-
} from "./chunk-B2CPS4QU.js";
|
|
5
|
-
|
|
6
|
-
// src/lib/isolation.ts
|
|
7
|
-
import { execFileSync } from "child_process";
|
|
8
|
-
function isIsolationEnabled() {
|
|
9
|
-
return process.env.VOLUTE_ISOLATION === "user";
|
|
10
|
-
}
|
|
11
|
-
function mindUserName(mindName) {
|
|
12
|
-
const err = validateMindName(mindName);
|
|
13
|
-
if (err) throw new Error(`Invalid mind name for isolation: ${err}`);
|
|
14
|
-
const prefix = process.env.VOLUTE_USER_PREFIX ?? "mind-";
|
|
15
|
-
return `${prefix}${mindName}`;
|
|
16
|
-
}
|
|
17
|
-
function ensureVoluteGroup(opts) {
|
|
18
|
-
if (!opts?.force && !isIsolationEnabled()) return;
|
|
19
|
-
try {
|
|
20
|
-
execFileSync("getent", ["group", "volute"], { stdio: "ignore" });
|
|
21
|
-
} catch {
|
|
22
|
-
try {
|
|
23
|
-
execFileSync("groupadd", ["volute"], { stdio: ["ignore", "ignore", "pipe"] });
|
|
24
|
-
} catch (err) {
|
|
25
|
-
const stderr = err?.stderr?.toString().trim();
|
|
26
|
-
throw new Error(`Failed to create volute group${stderr ? `: ${stderr}` : ""}`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function createMindUser(name, homeDir) {
|
|
31
|
-
if (!isIsolationEnabled()) return;
|
|
32
|
-
const user = mindUserName(name);
|
|
33
|
-
try {
|
|
34
|
-
execFileSync("id", [user], { stdio: "ignore" });
|
|
35
|
-
return;
|
|
36
|
-
} catch {
|
|
37
|
-
}
|
|
38
|
-
try {
|
|
39
|
-
const args = ["-r", "-M", "-G", "volute", "-s", "/usr/sbin/nologin"];
|
|
40
|
-
if (homeDir) args.push("-d", homeDir);
|
|
41
|
-
args.push(user);
|
|
42
|
-
execFileSync("useradd", args, {
|
|
43
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
44
|
-
});
|
|
45
|
-
} catch (err) {
|
|
46
|
-
const stderr = err?.stderr?.toString().trim();
|
|
47
|
-
throw new Error(`Failed to create user ${user}${stderr ? `: ${stderr}` : ""}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function deleteMindUser(name) {
|
|
51
|
-
if (!isIsolationEnabled()) return;
|
|
52
|
-
const user = mindUserName(name);
|
|
53
|
-
try {
|
|
54
|
-
execFileSync("userdel", [user], { stdio: "ignore" });
|
|
55
|
-
} catch {
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function wrapForIsolation(cmd, args, mindName) {
|
|
59
|
-
if (!isIsolationEnabled()) return [cmd, args];
|
|
60
|
-
const baseName = mindName.split("@", 2)[0];
|
|
61
|
-
const user = mindUserName(baseName);
|
|
62
|
-
return ["runuser", ["-u", user, "--", cmd, ...args]];
|
|
63
|
-
}
|
|
64
|
-
function chownMindDir(dir, name) {
|
|
65
|
-
if (!isIsolationEnabled()) return;
|
|
66
|
-
const user = mindUserName(name);
|
|
67
|
-
try {
|
|
68
|
-
execFileSync("chown", ["-R", `${user}:${user}`, dir], { stdio: ["ignore", "ignore", "pipe"] });
|
|
69
|
-
} catch (err) {
|
|
70
|
-
const stderr = err?.stderr?.toString().trim();
|
|
71
|
-
throw new Error(`Failed to chown ${dir} to ${user}${stderr ? `: ${stderr}` : ""}`);
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
execFileSync("chmod", ["700", dir], { stdio: ["ignore", "ignore", "pipe"] });
|
|
75
|
-
} catch (err) {
|
|
76
|
-
const stderr = err?.stderr?.toString().trim();
|
|
77
|
-
throw new Error(`Failed to chmod ${dir}${stderr ? `: ${stderr}` : ""}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export {
|
|
82
|
-
isIsolationEnabled,
|
|
83
|
-
mindUserName,
|
|
84
|
-
ensureVoluteGroup,
|
|
85
|
-
createMindUser,
|
|
86
|
-
deleteMindUser,
|
|
87
|
-
wrapForIsolation,
|
|
88
|
-
chownMindDir
|
|
89
|
-
};
|
package/dist/service-FASYWLTC.js
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
LAUNCHD_PLIST_LABEL,
|
|
4
|
-
LAUNCHD_PLIST_PATH,
|
|
5
|
-
SYSTEM_SERVICE_PATH,
|
|
6
|
-
USER_SYSTEMD_UNIT
|
|
7
|
-
} from "./chunk-3AIBT4TW.js";
|
|
8
|
-
import {
|
|
9
|
-
parseArgs
|
|
10
|
-
} from "./chunk-D424ZQGI.js";
|
|
11
|
-
import {
|
|
12
|
-
resolveVoluteBin
|
|
13
|
-
} from "./chunk-JTDFJWI2.js";
|
|
14
|
-
import "./chunk-NWPT4ASZ.js";
|
|
15
|
-
import "./chunk-B2CPS4QU.js";
|
|
16
|
-
import "./chunk-K3NQKI34.js";
|
|
17
|
-
|
|
18
|
-
// src/commands/service.ts
|
|
19
|
-
import { execFile } from "child_process";
|
|
20
|
-
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
|
|
21
|
-
import { homedir } from "os";
|
|
22
|
-
import { resolve } from "path";
|
|
23
|
-
import { promisify } from "util";
|
|
24
|
-
var execFileAsync = promisify(execFile);
|
|
25
|
-
var HOST_RE = /^[a-zA-Z0-9.:_-]+$/;
|
|
26
|
-
function validateHost(host) {
|
|
27
|
-
if (!HOST_RE.test(host)) {
|
|
28
|
-
throw new Error(`Invalid host: ${host}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
function escapeXml(s) {
|
|
32
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
33
|
-
}
|
|
34
|
-
function generatePlist(voluteBin, port, host) {
|
|
35
|
-
const args = ["up", "--foreground"];
|
|
36
|
-
if (port != null) args.push("--port", String(port));
|
|
37
|
-
if (host) args.push("--host", host);
|
|
38
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
39
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
40
|
-
<plist version="1.0">
|
|
41
|
-
<dict>
|
|
42
|
-
<key>Label</key>
|
|
43
|
-
<string>${LAUNCHD_PLIST_LABEL}</string>
|
|
44
|
-
<key>ProgramArguments</key>
|
|
45
|
-
<array>
|
|
46
|
-
${[voluteBin, ...args].map((a) => `<string>${escapeXml(a)}</string>`).join("\n ")}
|
|
47
|
-
</array>
|
|
48
|
-
<key>RunAtLoad</key>
|
|
49
|
-
<true/>
|
|
50
|
-
<key>KeepAlive</key>
|
|
51
|
-
<true/>
|
|
52
|
-
<key>StandardOutPath</key>
|
|
53
|
-
<string>${resolve(homedir(), ".volute", "daemon.log")}</string>
|
|
54
|
-
<key>StandardErrorPath</key>
|
|
55
|
-
<string>${resolve(homedir(), ".volute", "daemon.log")}</string>
|
|
56
|
-
</dict>
|
|
57
|
-
</plist>`;
|
|
58
|
-
}
|
|
59
|
-
function generateUnit(voluteBin, port, host) {
|
|
60
|
-
const args = ["up", "--foreground"];
|
|
61
|
-
if (port != null) args.push("--port", String(port));
|
|
62
|
-
if (host) args.push("--host", host);
|
|
63
|
-
return `[Unit]
|
|
64
|
-
Description=Volute Daemon
|
|
65
|
-
After=network.target
|
|
66
|
-
|
|
67
|
-
[Service]
|
|
68
|
-
Type=exec
|
|
69
|
-
ExecStart=${voluteBin} ${args.join(" ")}
|
|
70
|
-
Restart=on-failure
|
|
71
|
-
RestartSec=5
|
|
72
|
-
|
|
73
|
-
[Install]
|
|
74
|
-
WantedBy=default.target
|
|
75
|
-
`;
|
|
76
|
-
}
|
|
77
|
-
async function install(port, host) {
|
|
78
|
-
if (host) validateHost(host);
|
|
79
|
-
const voluteBin = resolveVoluteBin();
|
|
80
|
-
const platform = process.platform;
|
|
81
|
-
if (platform === "darwin") {
|
|
82
|
-
const path = LAUNCHD_PLIST_PATH;
|
|
83
|
-
mkdirSync(resolve(homedir(), "Library", "LaunchAgents"), { recursive: true });
|
|
84
|
-
writeFileSync(path, generatePlist(voluteBin, port, host));
|
|
85
|
-
console.log(`Wrote ${path}`);
|
|
86
|
-
await execFileAsync("launchctl", ["load", path]);
|
|
87
|
-
console.log("Service installed and loaded. Volute daemon will start on login.");
|
|
88
|
-
} else if (platform === "linux") {
|
|
89
|
-
if (existsSync(SYSTEM_SERVICE_PATH)) {
|
|
90
|
-
console.error(
|
|
91
|
-
"A system-level Volute service is already installed (via `volute service install --system`)."
|
|
92
|
-
);
|
|
93
|
-
console.error("Use `systemctl status volute` to check its status.");
|
|
94
|
-
console.error("To remove it first: sudo volute service uninstall --system");
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
if (process.getuid?.() === 0) {
|
|
98
|
-
console.error(
|
|
99
|
-
"Error: `volute service install` uses systemd user services, which don't work as root."
|
|
100
|
-
);
|
|
101
|
-
console.error(
|
|
102
|
-
"Use `volute service install --system` instead to install a system-level service."
|
|
103
|
-
);
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
const path = USER_SYSTEMD_UNIT;
|
|
107
|
-
mkdirSync(resolve(homedir(), ".config", "systemd", "user"), { recursive: true });
|
|
108
|
-
writeFileSync(path, generateUnit(voluteBin, port, host));
|
|
109
|
-
console.log(`Wrote ${path}`);
|
|
110
|
-
await execFileAsync("systemctl", ["--user", "enable", "--now", "volute"]);
|
|
111
|
-
console.log("Service installed and enabled. Volute daemon will start on login.");
|
|
112
|
-
} else {
|
|
113
|
-
console.error(`Unsupported platform: ${platform}. Only macOS and Linux are supported.`);
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
async function uninstall() {
|
|
118
|
-
const platform = process.platform;
|
|
119
|
-
if (platform === "darwin") {
|
|
120
|
-
const path = LAUNCHD_PLIST_PATH;
|
|
121
|
-
if (existsSync(path)) {
|
|
122
|
-
try {
|
|
123
|
-
await execFileAsync("launchctl", ["unload", path]);
|
|
124
|
-
} catch {
|
|
125
|
-
console.warn("Warning: failed to unload service (may already be unloaded)");
|
|
126
|
-
}
|
|
127
|
-
unlinkSync(path);
|
|
128
|
-
console.log("Service uninstalled.");
|
|
129
|
-
} else {
|
|
130
|
-
console.log("Service not installed.");
|
|
131
|
-
}
|
|
132
|
-
} else if (platform === "linux") {
|
|
133
|
-
const path = USER_SYSTEMD_UNIT;
|
|
134
|
-
if (existsSync(path)) {
|
|
135
|
-
try {
|
|
136
|
-
await execFileAsync("systemctl", ["--user", "disable", "--now", "volute"]);
|
|
137
|
-
} catch {
|
|
138
|
-
console.warn("Warning: failed to disable service (may already be stopped)");
|
|
139
|
-
}
|
|
140
|
-
unlinkSync(path);
|
|
141
|
-
console.log("Service uninstalled.");
|
|
142
|
-
} else {
|
|
143
|
-
console.log("Service not installed.");
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
console.error(`Unsupported platform: ${platform}`);
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
async function status() {
|
|
151
|
-
const platform = process.platform;
|
|
152
|
-
if (platform === "darwin") {
|
|
153
|
-
if (!existsSync(LAUNCHD_PLIST_PATH)) {
|
|
154
|
-
console.log("Service not installed.");
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
try {
|
|
158
|
-
const { stdout } = await execFileAsync("launchctl", ["list", LAUNCHD_PLIST_LABEL]);
|
|
159
|
-
console.log(stdout);
|
|
160
|
-
} catch {
|
|
161
|
-
console.log("Service installed but not currently loaded.");
|
|
162
|
-
}
|
|
163
|
-
} else if (platform === "linux") {
|
|
164
|
-
if (existsSync(SYSTEM_SERVICE_PATH)) {
|
|
165
|
-
try {
|
|
166
|
-
const { stdout } = await execFileAsync("systemctl", ["status", "volute", "--no-pager"]);
|
|
167
|
-
console.log(stdout);
|
|
168
|
-
} catch (err) {
|
|
169
|
-
const e = err;
|
|
170
|
-
if (e.stdout) {
|
|
171
|
-
console.log(e.stdout);
|
|
172
|
-
} else {
|
|
173
|
-
console.error("System service installed but could not retrieve status.");
|
|
174
|
-
if (e.stderr) console.error(e.stderr);
|
|
175
|
-
else if (e.message) console.error(e.message);
|
|
176
|
-
console.error("Try running: systemctl status volute");
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
if (!existsSync(USER_SYSTEMD_UNIT)) {
|
|
182
|
-
console.log("Service not installed.");
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
const { stdout } = await execFileAsync("systemctl", [
|
|
187
|
-
"--user",
|
|
188
|
-
"status",
|
|
189
|
-
"volute",
|
|
190
|
-
"--no-pager"
|
|
191
|
-
]);
|
|
192
|
-
console.log(stdout);
|
|
193
|
-
} catch (err) {
|
|
194
|
-
const e = err;
|
|
195
|
-
if (e.stdout) console.log(e.stdout);
|
|
196
|
-
else console.log("Service installed but status unknown.");
|
|
197
|
-
}
|
|
198
|
-
} else {
|
|
199
|
-
console.error(`Unsupported platform: ${platform}`);
|
|
200
|
-
process.exit(1);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
async function run(args) {
|
|
204
|
-
const { positional, flags } = parseArgs(args, {
|
|
205
|
-
port: { type: "number" },
|
|
206
|
-
host: { type: "string" },
|
|
207
|
-
system: { type: "boolean" },
|
|
208
|
-
force: { type: "boolean" }
|
|
209
|
-
});
|
|
210
|
-
const subcommand = positional[0];
|
|
211
|
-
switch (subcommand) {
|
|
212
|
-
case "install":
|
|
213
|
-
if (flags.system) {
|
|
214
|
-
const setup = await import("./setup-BMLM2UTK.js");
|
|
215
|
-
setup.install(flags.port, flags.host);
|
|
216
|
-
} else {
|
|
217
|
-
await install(flags.port, flags.host);
|
|
218
|
-
}
|
|
219
|
-
break;
|
|
220
|
-
case "uninstall":
|
|
221
|
-
if (flags.system) {
|
|
222
|
-
const setup = await import("./setup-BMLM2UTK.js");
|
|
223
|
-
setup.uninstall(!!flags.force);
|
|
224
|
-
} else {
|
|
225
|
-
await uninstall();
|
|
226
|
-
}
|
|
227
|
-
break;
|
|
228
|
-
case "status":
|
|
229
|
-
await status();
|
|
230
|
-
break;
|
|
231
|
-
default:
|
|
232
|
-
console.log(`Usage:
|
|
233
|
-
volute service install [--port N] [--host H] Install as user-level service
|
|
234
|
-
volute service install --system [--port N] [--host H] Install system service with user isolation
|
|
235
|
-
volute service uninstall Remove user-level service
|
|
236
|
-
volute service uninstall --system [--force] Remove system service (--force removes data + users)
|
|
237
|
-
volute service status Check service status`);
|
|
238
|
-
if (subcommand) {
|
|
239
|
-
console.error(`
|
|
240
|
-
Unknown subcommand: ${subcommand}`);
|
|
241
|
-
process.exit(1);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
export {
|
|
246
|
-
run
|
|
247
|
-
};
|