volute 0.1.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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/dist/channel-Q642YUZE.js +90 -0
  4. package/dist/chunk-5YW4B7CG.js +181 -0
  5. package/dist/chunk-A5ZJEMHT.js +40 -0
  6. package/dist/chunk-D424ZQGI.js +31 -0
  7. package/dist/chunk-GSPKUPKU.js +120 -0
  8. package/dist/chunk-H5XQARAP.js +48 -0
  9. package/dist/chunk-KSMIWOCN.js +84 -0
  10. package/dist/chunk-N4QN44LC.js +74 -0
  11. package/dist/chunk-XZN4WPNC.js +34 -0
  12. package/dist/cli.js +95 -0
  13. package/dist/connect-LW6G23AV.js +48 -0
  14. package/dist/connectors/discord.js +213 -0
  15. package/dist/create-3K6O2SDC.js +62 -0
  16. package/dist/daemon-client-ZTHW7ROS.js +10 -0
  17. package/dist/daemon.js +1731 -0
  18. package/dist/delete-JNGY7ZFH.js +54 -0
  19. package/dist/disconnect-ACVTKTRE.js +30 -0
  20. package/dist/down-FYCUYC5H.js +71 -0
  21. package/dist/env-7SLRN3MG.js +159 -0
  22. package/dist/fork-BB3DZ426.js +112 -0
  23. package/dist/import-W2AMTEV5.js +410 -0
  24. package/dist/logs-BUHRIQ2L.js +35 -0
  25. package/dist/merge-446QTE7Q.js +219 -0
  26. package/dist/schedule-KKSOVUDF.js +113 -0
  27. package/dist/send-WQSVSRDD.js +50 -0
  28. package/dist/start-LKMWS6ZE.js +29 -0
  29. package/dist/status-CIEKUI3V.js +50 -0
  30. package/dist/stop-YTOAGYE4.js +29 -0
  31. package/dist/up-AJJ4GCXY.js +111 -0
  32. package/dist/upgrade-JACA6YMO.js +211 -0
  33. package/dist/variants-HPY4DEWU.js +60 -0
  34. package/dist/web-assets/assets/index-DNNPoxMn.js +158 -0
  35. package/dist/web-assets/index.html +15 -0
  36. package/package.json +76 -0
  37. package/templates/_base/.init/MEMORY.md +2 -0
  38. package/templates/_base/.init/SOUL.md +2 -0
  39. package/templates/_base/.init/memory/.gitkeep +0 -0
  40. package/templates/_base/_skills/memory/SKILL.md +30 -0
  41. package/templates/_base/_skills/volute-agent/SKILL.md +53 -0
  42. package/templates/_base/biome.json.tmpl +21 -0
  43. package/templates/_base/home/VOLUTE.md +19 -0
  44. package/templates/_base/src/lib/auto-commit.ts +46 -0
  45. package/templates/_base/src/lib/logger.ts +47 -0
  46. package/templates/_base/src/lib/types.ts +24 -0
  47. package/templates/_base/src/lib/volute-server.ts +98 -0
  48. package/templates/_base/tsconfig.json +13 -0
  49. package/templates/_base/volute.json.tmpl +3 -0
  50. package/templates/agent-sdk/.init/CLAUDE.md +36 -0
  51. package/templates/agent-sdk/package.json.tmpl +20 -0
  52. package/templates/agent-sdk/src/lib/agent.ts +199 -0
  53. package/templates/agent-sdk/src/lib/hooks/auto-commit.ts +14 -0
  54. package/templates/agent-sdk/src/lib/hooks/identity-reload.ts +26 -0
  55. package/templates/agent-sdk/src/lib/hooks/pre-compact.ts +20 -0
  56. package/templates/agent-sdk/src/lib/message-channel.ts +37 -0
  57. package/templates/agent-sdk/src/server.ts +158 -0
  58. package/templates/agent-sdk/volute-template.json +9 -0
  59. package/templates/pi/.init/AGENTS.md +26 -0
  60. package/templates/pi/package.json.tmpl +20 -0
  61. package/templates/pi/src/lib/agent.ts +205 -0
  62. package/templates/pi/src/server.ts +121 -0
  63. package/templates/pi/volute-template.json +9 -0
  64. package/templates/pi/volute.json.tmpl +3 -0
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ daemonFetch
4
+ } from "./chunk-H5XQARAP.js";
5
+ import "./chunk-5YW4B7CG.js";
6
+
7
+ // src/commands/schedule.ts
8
+ async function run(args) {
9
+ const subcommand = args[0];
10
+ const agentName = args[1];
11
+ if (!subcommand || !agentName) {
12
+ printUsage();
13
+ process.exit(1);
14
+ }
15
+ switch (subcommand) {
16
+ case "list":
17
+ await listSchedules(agentName);
18
+ break;
19
+ case "add":
20
+ await addSchedule(agentName, args.slice(2));
21
+ break;
22
+ case "remove":
23
+ await removeSchedule(agentName, args.slice(2));
24
+ break;
25
+ default:
26
+ printUsage();
27
+ process.exit(1);
28
+ }
29
+ }
30
+ function printUsage() {
31
+ console.error(`Usage:
32
+ volute schedule list <agent>
33
+ volute schedule add <agent> --cron "..." --message "..." [--id name]
34
+ volute schedule remove <agent> --id <id>`);
35
+ }
36
+ async function listSchedules(agent) {
37
+ const res = await daemonFetch(`/api/agents/${encodeURIComponent(agent)}/schedules`);
38
+ if (!res.ok) {
39
+ const data = await res.json();
40
+ console.error(data.error ?? `Failed to list schedules: ${res.status}`);
41
+ process.exit(1);
42
+ }
43
+ const schedules = await res.json();
44
+ if (schedules.length === 0) {
45
+ console.log("No schedules configured.");
46
+ return;
47
+ }
48
+ const idW = Math.max(2, ...schedules.map((s) => s.id.length));
49
+ const cronW = Math.max(4, ...schedules.map((s) => s.cron.length));
50
+ console.log(`${"ID".padEnd(idW)} ${"CRON".padEnd(cronW)} ENABLED MESSAGE`);
51
+ for (const s of schedules) {
52
+ console.log(
53
+ `${s.id.padEnd(idW)} ${s.cron.padEnd(cronW)} ${String(s.enabled).padEnd(7)} ${s.message}`
54
+ );
55
+ }
56
+ }
57
+ async function addSchedule(agent, args) {
58
+ let cron = "";
59
+ let message = "";
60
+ let id = "";
61
+ for (let i = 0; i < args.length; i++) {
62
+ if (args[i] === "--cron" && args[i + 1]) {
63
+ cron = args[++i];
64
+ } else if (args[i] === "--message" && args[i + 1]) {
65
+ message = args[++i];
66
+ } else if (args[i] === "--id" && args[i + 1]) {
67
+ id = args[++i];
68
+ }
69
+ }
70
+ if (!cron || !message) {
71
+ console.error("--cron and --message are required");
72
+ process.exit(1);
73
+ }
74
+ const body = { cron, message };
75
+ if (id) body.id = id;
76
+ const res = await daemonFetch(`/api/agents/${encodeURIComponent(agent)}/schedules`, {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify(body)
80
+ });
81
+ if (!res.ok) {
82
+ const data2 = await res.json();
83
+ console.error(data2.error ?? `Failed to add schedule: ${res.status}`);
84
+ process.exit(1);
85
+ }
86
+ const data = await res.json();
87
+ console.log(`Schedule added: ${data.id}`);
88
+ }
89
+ async function removeSchedule(agent, args) {
90
+ let id = "";
91
+ for (let i = 0; i < args.length; i++) {
92
+ if (args[i] === "--id" && args[i + 1]) {
93
+ id = args[++i];
94
+ }
95
+ }
96
+ if (!id) {
97
+ console.error("--id is required");
98
+ process.exit(1);
99
+ }
100
+ const res = await daemonFetch(
101
+ `/api/agents/${encodeURIComponent(agent)}/schedules/${encodeURIComponent(id)}`,
102
+ { method: "DELETE" }
103
+ );
104
+ if (!res.ok) {
105
+ const data = await res.json();
106
+ console.error(data.error ?? `Failed to remove schedule: ${res.status}`);
107
+ process.exit(1);
108
+ }
109
+ console.log(`Schedule removed: ${id}`);
110
+ }
111
+ export {
112
+ run
113
+ };
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ readNdjson
4
+ } from "./chunk-KSMIWOCN.js";
5
+ import {
6
+ daemonFetch
7
+ } from "./chunk-H5XQARAP.js";
8
+ import "./chunk-5YW4B7CG.js";
9
+
10
+ // src/commands/send.ts
11
+ import { userInfo } from "os";
12
+ async function run(args) {
13
+ const name = args[0];
14
+ const message = args[1];
15
+ if (!name || !message) {
16
+ console.error('Usage: volute send <name> "<message>"');
17
+ process.exit(1);
18
+ }
19
+ const sender = userInfo().username;
20
+ const res = await daemonFetch(`/api/agents/${encodeURIComponent(name)}/message`, {
21
+ method: "POST",
22
+ headers: { "Content-Type": "application/json" },
23
+ body: JSON.stringify({
24
+ content: [{ type: "text", text: message }],
25
+ channel: "cli",
26
+ sender
27
+ })
28
+ });
29
+ if (!res.ok) {
30
+ const data = await res.json();
31
+ console.error(data.error ?? `Failed to send message: ${res.status}`);
32
+ process.exit(1);
33
+ }
34
+ if (!res.body) {
35
+ console.error("No response body");
36
+ process.exit(1);
37
+ }
38
+ for await (const event of readNdjson(res.body)) {
39
+ if (event.type === "text") {
40
+ process.stdout.write(event.content);
41
+ }
42
+ if (event.type === "done") {
43
+ break;
44
+ }
45
+ }
46
+ process.stdout.write("\n");
47
+ }
48
+ export {
49
+ run
50
+ };
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ daemonFetch
4
+ } from "./chunk-H5XQARAP.js";
5
+ import {
6
+ resolveAgent
7
+ } from "./chunk-5YW4B7CG.js";
8
+
9
+ // src/commands/start.ts
10
+ async function run(args) {
11
+ const name = args[0];
12
+ if (!name) {
13
+ console.error("Usage: volute start <name>");
14
+ process.exit(1);
15
+ }
16
+ const { entry } = resolveAgent(name);
17
+ const res = await daemonFetch(`/api/agents/${encodeURIComponent(name)}/start`, {
18
+ method: "POST"
19
+ });
20
+ const data = await res.json();
21
+ if (!res.ok) {
22
+ console.error(data.error || "Failed to start agent");
23
+ process.exit(1);
24
+ }
25
+ console.log(`${name} started on port ${entry.port}`);
26
+ }
27
+ export {
28
+ run
29
+ };
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ daemonFetch
4
+ } from "./chunk-H5XQARAP.js";
5
+ import "./chunk-5YW4B7CG.js";
6
+
7
+ // src/commands/status.ts
8
+ async function run(args) {
9
+ const name = args[0];
10
+ if (!name) {
11
+ const res2 = await daemonFetch("/api/agents");
12
+ if (!res2.ok) {
13
+ const data = await res2.json();
14
+ console.error(data.error ?? `Failed to get status: ${res2.status}`);
15
+ process.exit(1);
16
+ }
17
+ const agents = await res2.json();
18
+ if (agents.length === 0) {
19
+ console.log("No agents registered. Create one with: volute create <name>");
20
+ return;
21
+ }
22
+ const nameW = Math.max(4, ...agents.map((a) => a.name.length));
23
+ const portW = Math.max(4, ...agents.map((a) => String(a.port).length));
24
+ console.log(`${"NAME".padEnd(nameW)} ${"PORT".padEnd(portW)} STATUS CONNECTORS`);
25
+ for (const agent2 of agents) {
26
+ const connected = agent2.channels.filter((ch) => ch.status === "connected").map((ch) => ch.name);
27
+ const connectors = connected.length > 0 ? connected.join(", ") : "-";
28
+ console.log(
29
+ `${agent2.name.padEnd(nameW)} ${String(agent2.port).padEnd(portW)} ${agent2.status.padEnd(8)} ${connectors}`
30
+ );
31
+ }
32
+ return;
33
+ }
34
+ const res = await daemonFetch(`/api/agents/${encodeURIComponent(name)}`);
35
+ if (!res.ok) {
36
+ const data = await res.json();
37
+ console.error(data.error || `Failed to get status for ${name}`);
38
+ process.exit(1);
39
+ }
40
+ const agent = await res.json();
41
+ console.log(`Agent: ${agent.name}`);
42
+ console.log(`Port: ${agent.port}`);
43
+ console.log(`Status: ${agent.status}`);
44
+ for (const ch of agent.channels) {
45
+ console.log(`${ch.name}: ${ch.status}`);
46
+ }
47
+ }
48
+ export {
49
+ run
50
+ };
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ daemonFetch
4
+ } from "./chunk-H5XQARAP.js";
5
+ import {
6
+ resolveAgent
7
+ } from "./chunk-5YW4B7CG.js";
8
+
9
+ // src/commands/stop.ts
10
+ async function run(args) {
11
+ const name = args[0];
12
+ if (!name) {
13
+ console.error("Usage: volute stop <name>");
14
+ process.exit(1);
15
+ }
16
+ resolveAgent(name);
17
+ const res = await daemonFetch(`/api/agents/${encodeURIComponent(name)}/stop`, {
18
+ method: "POST"
19
+ });
20
+ const data = await res.json();
21
+ if (!res.ok) {
22
+ console.error(data.error || "Failed to stop agent");
23
+ process.exit(1);
24
+ }
25
+ console.log(`${name} stopped.`);
26
+ }
27
+ export {
28
+ run
29
+ };
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ parseArgs
4
+ } from "./chunk-D424ZQGI.js";
5
+ import {
6
+ VOLUTE_HOME
7
+ } from "./chunk-5YW4B7CG.js";
8
+
9
+ // src/commands/up.ts
10
+ import { spawn } from "child_process";
11
+ import { existsSync, mkdirSync, openSync, readFileSync } from "fs";
12
+ import { dirname, resolve } from "path";
13
+ async function run(args) {
14
+ const { flags } = parseArgs(args, {
15
+ port: { type: "number" },
16
+ foreground: { type: "boolean" }
17
+ });
18
+ const port = flags.port ?? 4200;
19
+ const pidPath = resolve(VOLUTE_HOME, "daemon.pid");
20
+ if (existsSync(pidPath)) {
21
+ try {
22
+ const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
23
+ process.kill(pid, 0);
24
+ console.error(`Daemon already running (pid ${pid}). Use 'volute down' first.`);
25
+ process.exit(1);
26
+ } catch {
27
+ }
28
+ }
29
+ try {
30
+ const res = await fetch(`http://localhost:${port}/api/health`);
31
+ if (res.ok) {
32
+ console.error(
33
+ `Port ${port} is already in use by a Volute daemon. Use 'volute down' first, or kill the process on that port.`
34
+ );
35
+ process.exit(1);
36
+ }
37
+ } catch {
38
+ }
39
+ if (flags.foreground) {
40
+ const { startDaemon } = await import("./daemon.js");
41
+ await startDaemon({ port, foreground: true });
42
+ return;
43
+ }
44
+ let tsxBin = "";
45
+ let searchDir = dirname(new URL(import.meta.url).pathname);
46
+ for (let i = 0; i < 5; i++) {
47
+ const candidate = resolve(searchDir, "node_modules", ".bin", "tsx");
48
+ if (existsSync(candidate)) {
49
+ tsxBin = candidate;
50
+ break;
51
+ }
52
+ searchDir = dirname(searchDir);
53
+ }
54
+ if (!tsxBin) {
55
+ console.error("Could not find tsx binary.");
56
+ process.exit(1);
57
+ }
58
+ let daemonModule = "";
59
+ searchDir = dirname(new URL(import.meta.url).pathname);
60
+ for (let i = 0; i < 5; i++) {
61
+ const candidate = resolve(searchDir, "src", "daemon.ts");
62
+ if (existsSync(candidate)) {
63
+ daemonModule = candidate;
64
+ break;
65
+ }
66
+ searchDir = dirname(searchDir);
67
+ }
68
+ if (!daemonModule) {
69
+ console.error("Could not find daemon module.");
70
+ process.exit(1);
71
+ }
72
+ mkdirSync(VOLUTE_HOME, { recursive: true });
73
+ const logFile = resolve(VOLUTE_HOME, "daemon.log");
74
+ const logFd = openSync(logFile, "a");
75
+ const child = spawn(tsxBin, [daemonModule, "--port", String(port)], {
76
+ stdio: ["ignore", logFd, logFd],
77
+ detached: true
78
+ });
79
+ child.unref();
80
+ const url = `http://localhost:${port}/api/health`;
81
+ const maxWait = 3e4;
82
+ const start = Date.now();
83
+ while (Date.now() - start < maxWait) {
84
+ try {
85
+ const res = await fetch(url);
86
+ if (res.ok) {
87
+ console.log(`Volute daemon running on port ${port} (pid ${child.pid})`);
88
+ console.log(`Logs: ${logFile}`);
89
+ return;
90
+ }
91
+ } catch {
92
+ }
93
+ await new Promise((r) => setTimeout(r, 500));
94
+ }
95
+ if (child.pid) {
96
+ try {
97
+ process.kill(-child.pid, "SIGTERM");
98
+ } catch {
99
+ try {
100
+ process.kill(child.pid, "SIGTERM");
101
+ } catch {
102
+ }
103
+ }
104
+ }
105
+ console.error("Daemon started but did not become healthy within 30s.");
106
+ console.error(`Check logs: ${logFile}`);
107
+ process.exit(1);
108
+ }
109
+ export {
110
+ run
111
+ };
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ spawnServer
4
+ } from "./chunk-N4QN44LC.js";
5
+ import {
6
+ composeTemplate,
7
+ copyTemplateToDir,
8
+ findTemplatesRoot
9
+ } from "./chunk-GSPKUPKU.js";
10
+ import {
11
+ exec,
12
+ execInherit
13
+ } from "./chunk-XZN4WPNC.js";
14
+ import {
15
+ parseArgs
16
+ } from "./chunk-D424ZQGI.js";
17
+ import {
18
+ addVariant,
19
+ resolveAgent
20
+ } from "./chunk-5YW4B7CG.js";
21
+
22
+ // src/commands/upgrade.ts
23
+ import { existsSync, mkdirSync, rmSync } from "fs";
24
+ import { resolve } from "path";
25
+ var TEMPLATE_BRANCH = "volute/template";
26
+ var VARIANT_NAME = "upgrade";
27
+ async function run(args) {
28
+ const { positional, flags } = parseArgs(args, {
29
+ template: { type: "string" },
30
+ continue: { type: "boolean" }
31
+ });
32
+ const agentName = positional[0];
33
+ if (!agentName) {
34
+ console.error("Usage: volute upgrade <name> [--template <name>] [--continue]");
35
+ process.exit(1);
36
+ }
37
+ const { dir: projectRoot } = resolveAgent(agentName);
38
+ const template = flags.template ?? "agent-sdk";
39
+ if (flags.continue) {
40
+ await continueUpgrade(agentName, projectRoot);
41
+ return;
42
+ }
43
+ const worktreeDir = resolve(projectRoot, ".worktrees", VARIANT_NAME);
44
+ if (existsSync(worktreeDir)) {
45
+ console.error(
46
+ `Upgrade worktree already exists: ${worktreeDir}
47
+ If a previous upgrade is in progress, use --continue to finish it.
48
+ Otherwise, remove it with: git -C ${projectRoot} worktree remove .worktrees/${VARIANT_NAME}`
49
+ );
50
+ process.exit(1);
51
+ }
52
+ await exec("git", ["worktree", "prune"], { cwd: projectRoot });
53
+ try {
54
+ await exec("git", ["branch", "-D", VARIANT_NAME], { cwd: projectRoot });
55
+ } catch {
56
+ }
57
+ console.log("Updating template branch...");
58
+ await updateTemplateBranch(projectRoot, template, agentName);
59
+ console.log("Creating upgrade variant...");
60
+ const parentDir = resolve(projectRoot, ".worktrees");
61
+ if (!existsSync(parentDir)) {
62
+ mkdirSync(parentDir, { recursive: true });
63
+ }
64
+ await exec("git", ["worktree", "add", "-b", VARIANT_NAME, worktreeDir], {
65
+ cwd: projectRoot
66
+ });
67
+ console.log("Merging template changes...");
68
+ const hasConflicts = await mergeTemplateBranch(worktreeDir);
69
+ if (hasConflicts) {
70
+ console.log("\nMerge conflicts detected. Resolve them in:");
71
+ console.log(` ${worktreeDir}`);
72
+ console.log(`
73
+ Then run:`);
74
+ console.log(` volute upgrade ${agentName} --continue`);
75
+ return;
76
+ }
77
+ await installAndVerify(agentName, worktreeDir);
78
+ }
79
+ async function updateTemplateBranch(projectRoot, template, agentName) {
80
+ const tempWorktree = resolve(projectRoot, ".worktrees", "_template_update");
81
+ let branchExists = false;
82
+ try {
83
+ await exec("git", ["rev-parse", "--verify", TEMPLATE_BRANCH], {
84
+ cwd: projectRoot
85
+ });
86
+ branchExists = true;
87
+ } catch {
88
+ }
89
+ try {
90
+ await exec("git", ["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
91
+ } catch {
92
+ }
93
+ if (existsSync(tempWorktree)) {
94
+ rmSync(tempWorktree, { recursive: true, force: true });
95
+ }
96
+ const templatesRoot = findTemplatesRoot();
97
+ const { composedDir, manifest } = composeTemplate(templatesRoot, template);
98
+ try {
99
+ if (branchExists) {
100
+ await exec("git", ["worktree", "add", tempWorktree, TEMPLATE_BRANCH], {
101
+ cwd: projectRoot
102
+ });
103
+ } else {
104
+ await exec("git", ["worktree", "add", "--detach", tempWorktree], {
105
+ cwd: projectRoot
106
+ });
107
+ await exec("git", ["checkout", "--orphan", TEMPLATE_BRANCH], {
108
+ cwd: tempWorktree
109
+ });
110
+ await exec("git", ["rm", "-rf", "--cached", "."], { cwd: tempWorktree });
111
+ await exec("git", ["clean", "-fd"], { cwd: tempWorktree });
112
+ }
113
+ if (branchExists) {
114
+ await exec("git", ["rm", "-rf", "."], { cwd: tempWorktree }).catch(() => {
115
+ });
116
+ }
117
+ copyTemplateToDir(composedDir, tempWorktree, agentName, manifest);
118
+ const initDir = resolve(tempWorktree, ".init");
119
+ if (existsSync(initDir)) {
120
+ rmSync(initDir, { recursive: true, force: true });
121
+ }
122
+ await exec("git", ["add", "-A"], { cwd: tempWorktree });
123
+ try {
124
+ await exec("git", ["diff", "--cached", "--quiet"], { cwd: tempWorktree });
125
+ console.log("Template branch is already up to date.");
126
+ } catch {
127
+ await exec("git", ["commit", "-m", "template update"], {
128
+ cwd: tempWorktree
129
+ });
130
+ }
131
+ } finally {
132
+ try {
133
+ await exec("git", ["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
134
+ } catch {
135
+ }
136
+ if (existsSync(tempWorktree)) {
137
+ rmSync(tempWorktree, { recursive: true, force: true });
138
+ }
139
+ rmSync(composedDir, { recursive: true, force: true });
140
+ }
141
+ }
142
+ async function mergeTemplateBranch(worktreeDir) {
143
+ try {
144
+ await exec(
145
+ "git",
146
+ ["merge", TEMPLATE_BRANCH, "--allow-unrelated-histories", "-m", "merge template update"],
147
+ { cwd: worktreeDir }
148
+ );
149
+ return false;
150
+ } catch (e) {
151
+ try {
152
+ const status = await exec("git", ["status", "--porcelain"], {
153
+ cwd: worktreeDir
154
+ });
155
+ const hasConflictMarkers = status.split("\n").some((line) => line.startsWith("UU") || line.startsWith("AA"));
156
+ if (hasConflictMarkers) return true;
157
+ } catch {
158
+ }
159
+ throw e;
160
+ }
161
+ }
162
+ async function continueUpgrade(agentName, projectRoot) {
163
+ const worktreeDir = resolve(projectRoot, ".worktrees", VARIANT_NAME);
164
+ if (!existsSync(worktreeDir)) {
165
+ console.error("No upgrade in progress. Run `volute upgrade` first.");
166
+ process.exit(1);
167
+ }
168
+ const status = await exec("git", ["status", "--porcelain"], {
169
+ cwd: worktreeDir
170
+ });
171
+ const hasConflicts = status.split("\n").some((line) => line.startsWith("UU") || line.startsWith("AA"));
172
+ if (hasConflicts) {
173
+ console.error("There are still unresolved conflicts. Resolve them first.");
174
+ process.exit(1);
175
+ }
176
+ try {
177
+ await exec("git", ["add", "-A"], { cwd: worktreeDir });
178
+ await exec("git", ["commit", "--no-edit"], { cwd: worktreeDir });
179
+ } catch {
180
+ }
181
+ await installAndVerify(agentName, worktreeDir);
182
+ }
183
+ async function installAndVerify(agentName, worktreeDir) {
184
+ console.log("Installing dependencies...");
185
+ await execInherit("npm", ["install"], { cwd: worktreeDir });
186
+ console.log("Starting upgrade variant...");
187
+ const result = await spawnServer(worktreeDir, 0, { detached: true });
188
+ if (!result) {
189
+ console.error("Server failed to start within timeout");
190
+ process.exit(1);
191
+ }
192
+ const { actualPort, child } = result;
193
+ const pid = child.pid ?? null;
194
+ addVariant(agentName, {
195
+ name: VARIANT_NAME,
196
+ branch: VARIANT_NAME,
197
+ path: worktreeDir,
198
+ port: actualPort,
199
+ pid,
200
+ created: (/* @__PURE__ */ new Date()).toISOString()
201
+ });
202
+ console.log(`
203
+ Upgrade variant running on port ${actualPort}`);
204
+ console.log(`
205
+ Next steps:`);
206
+ console.log(` volute send ${agentName}@${VARIANT_NAME} "hello" # chat with upgraded variant`);
207
+ console.log(` volute merge ${agentName} ${VARIANT_NAME} # merge back when satisfied`);
208
+ }
209
+ export {
210
+ run
211
+ };
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ parseArgs
4
+ } from "./chunk-D424ZQGI.js";
5
+ import {
6
+ checkHealth,
7
+ readVariants,
8
+ resolveAgent,
9
+ writeVariants
10
+ } from "./chunk-5YW4B7CG.js";
11
+
12
+ // src/commands/variants.ts
13
+ async function run(args) {
14
+ const { positional, flags } = parseArgs(args, {
15
+ json: { type: "boolean" }
16
+ });
17
+ const name = positional[0];
18
+ if (!name) {
19
+ console.error("Usage: volute variants <name>");
20
+ process.exit(1);
21
+ }
22
+ const { json } = flags;
23
+ resolveAgent(name);
24
+ const variants = readVariants(name);
25
+ if (variants.length === 0) {
26
+ if (json) {
27
+ console.log("[]");
28
+ } else {
29
+ console.log("No variants.");
30
+ }
31
+ return;
32
+ }
33
+ const results = await Promise.all(
34
+ variants.map(async (v) => {
35
+ if (!v.port) return { ...v, status: "no-server" };
36
+ const health = await checkHealth(v.port);
37
+ return { ...v, status: health.ok ? "running" : "dead" };
38
+ })
39
+ );
40
+ const updated = results.map(({ status, ...v }) => ({
41
+ ...v,
42
+ pid: status === "dead" ? null : v.pid
43
+ }));
44
+ writeVariants(name, updated);
45
+ if (json) {
46
+ console.log(JSON.stringify(results, null, 2));
47
+ return;
48
+ }
49
+ const nameW = Math.max(4, ...results.map((r) => r.name.length));
50
+ const portW = Math.max(4, ...results.map((r) => String(r.port || "-").length));
51
+ console.log(`${"NAME".padEnd(nameW)} ${"PORT".padEnd(portW)} ${"STATUS".padEnd(10)} BRANCH`);
52
+ for (const r of results) {
53
+ console.log(
54
+ `${r.name.padEnd(nameW)} ${String(r.port || "-").padEnd(portW)} ${r.status.padEnd(10)} ${r.branch}`
55
+ );
56
+ }
57
+ }
58
+ export {
59
+ run
60
+ };