@katyella/legio 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.
- package/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/agents/builder.md +141 -0
- package/agents/coordinator.md +351 -0
- package/agents/cto.md +196 -0
- package/agents/gateway.md +276 -0
- package/agents/lead.md +281 -0
- package/agents/merger.md +156 -0
- package/agents/monitor.md +212 -0
- package/agents/reviewer.md +142 -0
- package/agents/scout.md +131 -0
- package/agents/supervisor.md +416 -0
- package/bin/legio.mjs +38 -0
- package/package.json +77 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +102 -0
- package/src/agents/hooks-deployer.test.ts +1820 -0
- package/src/agents/hooks-deployer.ts +574 -0
- package/src/agents/identity.test.ts +614 -0
- package/src/agents/identity.ts +385 -0
- package/src/agents/lifecycle.test.ts +202 -0
- package/src/agents/lifecycle.ts +184 -0
- package/src/agents/manifest.test.ts +558 -0
- package/src/agents/manifest.ts +297 -0
- package/src/agents/overlay.test.ts +592 -0
- package/src/agents/overlay.ts +316 -0
- package/src/beads/client.test.ts +210 -0
- package/src/beads/client.ts +227 -0
- package/src/beads/molecules.test.ts +320 -0
- package/src/beads/molecules.ts +209 -0
- package/src/commands/agents.test.ts +325 -0
- package/src/commands/agents.ts +286 -0
- package/src/commands/clean.test.ts +730 -0
- package/src/commands/clean.ts +653 -0
- package/src/commands/completions.test.ts +346 -0
- package/src/commands/completions.ts +950 -0
- package/src/commands/coordinator.test.ts +1524 -0
- package/src/commands/coordinator.ts +880 -0
- package/src/commands/costs.test.ts +1015 -0
- package/src/commands/costs.ts +473 -0
- package/src/commands/dashboard.test.ts +94 -0
- package/src/commands/dashboard.ts +607 -0
- package/src/commands/doctor.test.ts +295 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/down.test.ts +308 -0
- package/src/commands/down.ts +124 -0
- package/src/commands/errors.test.ts +648 -0
- package/src/commands/errors.ts +255 -0
- package/src/commands/feed.test.ts +579 -0
- package/src/commands/feed.ts +368 -0
- package/src/commands/gateway.test.ts +698 -0
- package/src/commands/gateway.ts +419 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +539 -0
- package/src/commands/hooks.test.ts +292 -0
- package/src/commands/hooks.ts +210 -0
- package/src/commands/init.test.ts +211 -0
- package/src/commands/init.ts +622 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +455 -0
- package/src/commands/log.test.ts +1556 -0
- package/src/commands/log.ts +752 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +544 -0
- package/src/commands/mail.test.ts +1726 -0
- package/src/commands/mail.ts +926 -0
- package/src/commands/merge.test.ts +676 -0
- package/src/commands/merge.ts +374 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +150 -0
- package/src/commands/monitor.test.ts +151 -0
- package/src/commands/monitor.ts +394 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +373 -0
- package/src/commands/prime.test.ts +467 -0
- package/src/commands/prime.ts +386 -0
- package/src/commands/replay.test.ts +742 -0
- package/src/commands/replay.ts +367 -0
- package/src/commands/run.test.ts +443 -0
- package/src/commands/run.ts +365 -0
- package/src/commands/server.test.ts +626 -0
- package/src/commands/server.ts +298 -0
- package/src/commands/sling.test.ts +810 -0
- package/src/commands/sling.ts +700 -0
- package/src/commands/spec.test.ts +206 -0
- package/src/commands/spec.ts +171 -0
- package/src/commands/status.test.ts +276 -0
- package/src/commands/status.ts +339 -0
- package/src/commands/stop.test.ts +357 -0
- package/src/commands/stop.ts +119 -0
- package/src/commands/supervisor.test.ts +186 -0
- package/src/commands/supervisor.ts +544 -0
- package/src/commands/trace.test.ts +746 -0
- package/src/commands/trace.ts +332 -0
- package/src/commands/up.test.ts +597 -0
- package/src/commands/up.ts +275 -0
- package/src/commands/watch.test.ts +152 -0
- package/src/commands/watch.ts +238 -0
- package/src/commands/worktree.test.ts +648 -0
- package/src/commands/worktree.ts +266 -0
- package/src/config.test.ts +496 -0
- package/src/config.ts +616 -0
- package/src/doctor/agents.test.ts +448 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +184 -0
- package/src/doctor/config-check.ts +185 -0
- package/src/doctor/consistency.test.ts +645 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +284 -0
- package/src/doctor/databases.ts +211 -0
- package/src/doctor/dependencies.test.ts +150 -0
- package/src/doctor/dependencies.ts +179 -0
- package/src/doctor/logs.test.ts +244 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +210 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +285 -0
- package/src/doctor/structure.ts +195 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +130 -0
- package/src/doctor/version.ts +131 -0
- package/src/e2e/chat-flow.test.ts +346 -0
- package/src/e2e/init-sling-lifecycle.test.ts +288 -0
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +246 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +344 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/global-setup.ts +14 -0
- package/src/index.ts +339 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +118 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +812 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +258 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +873 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +815 -0
- package/src/mail/store.ts +402 -0
- package/src/merge/queue.test.ts +449 -0
- package/src/merge/queue.ts +262 -0
- package/src/merge/resolver.test.ts +1453 -0
- package/src/merge/resolver.ts +759 -0
- package/src/metrics/store.test.ts +1167 -0
- package/src/metrics/store.ts +511 -0
- package/src/metrics/summary.test.ts +397 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +643 -0
- package/src/metrics/transcript.ts +351 -0
- package/src/mulch/client.test.ts +547 -0
- package/src/mulch/client.ts +416 -0
- package/src/server/audit-store.test.ts +384 -0
- package/src/server/audit-store.ts +257 -0
- package/src/server/headless.test.ts +180 -0
- package/src/server/headless.ts +151 -0
- package/src/server/index.test.ts +241 -0
- package/src/server/index.ts +317 -0
- package/src/server/public/app.js +187 -0
- package/src/server/public/apple-touch-icon.png +0 -0
- package/src/server/public/components/agent-badge.js +37 -0
- package/src/server/public/components/data-table.js +114 -0
- package/src/server/public/components/gateway-chat.js +256 -0
- package/src/server/public/components/issue-card.js +96 -0
- package/src/server/public/components/layout.js +88 -0
- package/src/server/public/components/message-bubble.js +120 -0
- package/src/server/public/components/stat-card.js +26 -0
- package/src/server/public/components/terminal-panel.js +140 -0
- package/src/server/public/favicon-16.png +0 -0
- package/src/server/public/favicon-32.png +0 -0
- package/src/server/public/favicon.ico +0 -0
- package/src/server/public/favicon.png +0 -0
- package/src/server/public/index.html +64 -0
- package/src/server/public/lib/api.js +35 -0
- package/src/server/public/lib/markdown.js +8 -0
- package/src/server/public/lib/preact-setup.js +8 -0
- package/src/server/public/lib/state.js +99 -0
- package/src/server/public/lib/utils.js +309 -0
- package/src/server/public/lib/ws.js +79 -0
- package/src/server/public/views/chat.js +983 -0
- package/src/server/public/views/costs.js +692 -0
- package/src/server/public/views/dashboard.js +781 -0
- package/src/server/public/views/gateway-chat.js +622 -0
- package/src/server/public/views/inspect.js +399 -0
- package/src/server/public/views/issues.js +470 -0
- package/src/server/public/views/setup.js +94 -0
- package/src/server/public/views/task-detail.js +422 -0
- package/src/server/routes.test.ts +3816 -0
- package/src/server/routes.ts +1964 -0
- package/src/server/websocket.test.ts +288 -0
- package/src/server/websocket.ts +196 -0
- package/src/sessions/compat.test.ts +109 -0
- package/src/sessions/compat.ts +17 -0
- package/src/sessions/store.test.ts +969 -0
- package/src/sessions/store.ts +480 -0
- package/src/test-helpers.test.ts +97 -0
- package/src/test-helpers.ts +143 -0
- package/src/types.ts +708 -0
- package/src/watchdog/daemon.test.ts +1233 -0
- package/src/watchdog/daemon.ts +533 -0
- package/src/watchdog/health.test.ts +371 -0
- package/src/watchdog/health.ts +248 -0
- package/src/watchdog/triage.test.ts +162 -0
- package/src/watchdog/triage.ts +193 -0
- package/src/worktree/manager.test.ts +444 -0
- package/src/worktree/manager.ts +224 -0
- package/src/worktree/tmux.test.ts +1238 -0
- package/src/worktree/tmux.ts +644 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +132 -0
- package/templates/overlay.md.tmpl +79 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for legio down command.
|
|
3
|
+
*
|
|
4
|
+
* Uses DI (DownDeps) to inject mock subprocess calls. No real coordinator/server
|
|
5
|
+
* in tests.
|
|
6
|
+
*
|
|
7
|
+
* WHY DI instead of mock.module: mock.module() in vitest is process-global
|
|
8
|
+
* and leaks across test files. DI keeps mocks scoped to each test invocation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import type { DownDeps } from "./down.ts";
|
|
13
|
+
import { downCommand } from "./down.ts";
|
|
14
|
+
|
|
15
|
+
/** Builds a mock runCommand with configurable results per command prefix. */
|
|
16
|
+
function makeRunCommand(
|
|
17
|
+
responses: Record<string, { stdout: string; stderr: string; exitCode: number }>,
|
|
18
|
+
): NonNullable<DownDeps["_runCommand"]> {
|
|
19
|
+
return async (cmd) => {
|
|
20
|
+
const key = cmd.join(" ");
|
|
21
|
+
for (const [prefix, result] of Object.entries(responses)) {
|
|
22
|
+
if (key.startsWith(prefix)) return result;
|
|
23
|
+
}
|
|
24
|
+
return { stdout: "", stderr: `Unexpected command: ${key}`, exitCode: 1 };
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Coordinator stop success response. */
|
|
29
|
+
const COORD_STOP_OK = {
|
|
30
|
+
stdout:
|
|
31
|
+
"Coordinator stopped (session: session-123)\nWatchdog stopped\nNo monitor running\nRun completed\n",
|
|
32
|
+
stderr: "",
|
|
33
|
+
exitCode: 0,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Coordinator stop failure (not running). */
|
|
37
|
+
const COORD_STOP_FAIL = {
|
|
38
|
+
stdout: "",
|
|
39
|
+
stderr: "Error [AGENT_ERROR]: No active coordinator session found",
|
|
40
|
+
exitCode: 1,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Server stop success response. */
|
|
44
|
+
const SERVER_STOP_OK = { stdout: "Server stopped (PID 42000)\n", stderr: "", exitCode: 0 };
|
|
45
|
+
|
|
46
|
+
/** Server stop response when not running. */
|
|
47
|
+
const SERVER_NOT_RUNNING = { stdout: "Server not running\n", stderr: "", exitCode: 0 };
|
|
48
|
+
|
|
49
|
+
/** Server stop response when stale PID cleaned. */
|
|
50
|
+
const SERVER_STALE_PID = {
|
|
51
|
+
stdout: "Server not running (stale PID file cleaned up)\n",
|
|
52
|
+
stderr: "",
|
|
53
|
+
exitCode: 0,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe("downCommand", () => {
|
|
57
|
+
let capturedStdout: string;
|
|
58
|
+
let _capturedStderr: string;
|
|
59
|
+
let originalStdout: typeof process.stdout.write;
|
|
60
|
+
let originalStderr: typeof process.stderr.write;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
capturedStdout = "";
|
|
64
|
+
_capturedStderr = "";
|
|
65
|
+
originalStdout = process.stdout.write;
|
|
66
|
+
originalStderr = process.stderr.write;
|
|
67
|
+
process.stdout.write = vi.fn((chunk: unknown) => {
|
|
68
|
+
capturedStdout += String(chunk);
|
|
69
|
+
return true;
|
|
70
|
+
}) as typeof process.stdout.write;
|
|
71
|
+
process.stderr.write = vi.fn((chunk: unknown) => {
|
|
72
|
+
_capturedStderr += String(chunk);
|
|
73
|
+
return true;
|
|
74
|
+
}) as typeof process.stderr.write;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
process.stdout.write = originalStdout;
|
|
79
|
+
process.stderr.write = originalStderr;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("prints help for --help", async () => {
|
|
83
|
+
await downCommand(["--help"]);
|
|
84
|
+
expect(capturedStdout).toContain("legio down");
|
|
85
|
+
expect(capturedStdout).toContain("--json");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("prints help for -h", async () => {
|
|
89
|
+
await downCommand(["-h"]);
|
|
90
|
+
expect(capturedStdout).toContain("legio down");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("prints Nothing to stop when coordinator not running and server not running", async () => {
|
|
94
|
+
const deps: DownDeps = {
|
|
95
|
+
_runCommand: makeRunCommand({
|
|
96
|
+
"legio coordinator stop": COORD_STOP_FAIL,
|
|
97
|
+
"legio server stop": SERVER_NOT_RUNNING,
|
|
98
|
+
}),
|
|
99
|
+
_projectRoot: "/tmp/test-project",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
await downCommand([], deps);
|
|
103
|
+
|
|
104
|
+
expect(capturedStdout).toContain("Nothing to stop");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("prints Nothing to stop when server has stale PID", async () => {
|
|
108
|
+
const deps: DownDeps = {
|
|
109
|
+
_runCommand: makeRunCommand({
|
|
110
|
+
"legio coordinator stop": COORD_STOP_FAIL,
|
|
111
|
+
"legio server stop": SERVER_STALE_PID,
|
|
112
|
+
}),
|
|
113
|
+
_projectRoot: "/tmp/test-project",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
await downCommand([], deps);
|
|
117
|
+
|
|
118
|
+
expect(capturedStdout).toContain("Nothing to stop");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("stops coordinator when running", async () => {
|
|
122
|
+
const commands: string[][] = [];
|
|
123
|
+
const deps: DownDeps = {
|
|
124
|
+
_runCommand: async (cmd) => {
|
|
125
|
+
commands.push(cmd);
|
|
126
|
+
if (cmd[0] === "legio" && cmd[1] === "coordinator") return COORD_STOP_OK;
|
|
127
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_NOT_RUNNING;
|
|
128
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
129
|
+
},
|
|
130
|
+
_projectRoot: "/tmp/test-project",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
await downCommand([], deps);
|
|
134
|
+
|
|
135
|
+
const ranStop = commands.some(
|
|
136
|
+
(c) => c[0] === "legio" && c[1] === "coordinator" && c[2] === "stop",
|
|
137
|
+
);
|
|
138
|
+
expect(ranStop).toBe(true);
|
|
139
|
+
expect(capturedStdout).toContain("Legio stack stopped");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("stops server when running", async () => {
|
|
143
|
+
const commands: string[][] = [];
|
|
144
|
+
const deps: DownDeps = {
|
|
145
|
+
_runCommand: async (cmd) => {
|
|
146
|
+
commands.push(cmd);
|
|
147
|
+
if (cmd[0] === "legio" && cmd[1] === "coordinator") return COORD_STOP_FAIL;
|
|
148
|
+
if (cmd[0] === "legio" && cmd[1] === "server" && cmd[2] === "stop") return SERVER_STOP_OK;
|
|
149
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
150
|
+
},
|
|
151
|
+
_projectRoot: "/tmp/test-project",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
await downCommand([], deps);
|
|
155
|
+
|
|
156
|
+
const serverStopped = commands.some(
|
|
157
|
+
(c) => c[0] === "legio" && c[1] === "server" && c[2] === "stop",
|
|
158
|
+
);
|
|
159
|
+
expect(serverStopped).toBe(true);
|
|
160
|
+
expect(capturedStdout).toContain("Legio stack stopped");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("stops both coordinator and server when both running", async () => {
|
|
164
|
+
const commands: string[][] = [];
|
|
165
|
+
const deps: DownDeps = {
|
|
166
|
+
_runCommand: async (cmd) => {
|
|
167
|
+
commands.push(cmd);
|
|
168
|
+
if (cmd[0] === "legio" && cmd[1] === "coordinator") return COORD_STOP_OK;
|
|
169
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_STOP_OK;
|
|
170
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
171
|
+
},
|
|
172
|
+
_projectRoot: "/tmp/test-project",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
await downCommand([], deps);
|
|
176
|
+
|
|
177
|
+
const coordStopped = commands.some(
|
|
178
|
+
(c) => c[0] === "legio" && c[1] === "coordinator" && c[2] === "stop",
|
|
179
|
+
);
|
|
180
|
+
const serverStopped = commands.some(
|
|
181
|
+
(c) => c[0] === "legio" && c[1] === "server" && c[2] === "stop",
|
|
182
|
+
);
|
|
183
|
+
expect(coordStopped).toBe(true);
|
|
184
|
+
expect(serverStopped).toBe(true);
|
|
185
|
+
expect(capturedStdout).toContain("Legio stack stopped");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("always calls legio server stop (delegates PID checking)", async () => {
|
|
189
|
+
const commands: string[][] = [];
|
|
190
|
+
const deps: DownDeps = {
|
|
191
|
+
_runCommand: async (cmd) => {
|
|
192
|
+
commands.push(cmd);
|
|
193
|
+
if (cmd[0] === "legio" && cmd[1] === "coordinator") return COORD_STOP_OK;
|
|
194
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_NOT_RUNNING;
|
|
195
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
196
|
+
},
|
|
197
|
+
_projectRoot: "/tmp/test-project",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await downCommand([], deps);
|
|
201
|
+
|
|
202
|
+
const serverStopCalled = commands.some(
|
|
203
|
+
(c) => c[0] === "legio" && c[1] === "server" && c[2] === "stop",
|
|
204
|
+
);
|
|
205
|
+
expect(serverStopCalled).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("outputs JSON with correct fields when --json passed (nothing to stop)", async () => {
|
|
209
|
+
const deps: DownDeps = {
|
|
210
|
+
_runCommand: makeRunCommand({
|
|
211
|
+
"legio coordinator stop": COORD_STOP_FAIL,
|
|
212
|
+
"legio server stop": SERVER_NOT_RUNNING,
|
|
213
|
+
}),
|
|
214
|
+
_projectRoot: "/tmp/test-project",
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
await downCommand(["--json"], deps);
|
|
218
|
+
|
|
219
|
+
const parsed = JSON.parse(capturedStdout.trim()) as {
|
|
220
|
+
coordinatorStopped: boolean;
|
|
221
|
+
serverStopped: boolean;
|
|
222
|
+
nothingToStop: boolean;
|
|
223
|
+
};
|
|
224
|
+
expect(parsed.coordinatorStopped).toBe(false);
|
|
225
|
+
expect(parsed.serverStopped).toBe(false);
|
|
226
|
+
expect(parsed.nothingToStop).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("outputs JSON with coordinatorStopped=true when coordinator stops", async () => {
|
|
230
|
+
const deps: DownDeps = {
|
|
231
|
+
_runCommand: makeRunCommand({
|
|
232
|
+
"legio coordinator stop": COORD_STOP_OK,
|
|
233
|
+
"legio server stop": SERVER_NOT_RUNNING,
|
|
234
|
+
}),
|
|
235
|
+
_projectRoot: "/tmp/test-project",
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
await downCommand(["--json"], deps);
|
|
239
|
+
|
|
240
|
+
const parsed = JSON.parse(capturedStdout.trim()) as {
|
|
241
|
+
coordinatorStopped: boolean;
|
|
242
|
+
serverStopped: boolean;
|
|
243
|
+
nothingToStop: boolean;
|
|
244
|
+
};
|
|
245
|
+
expect(parsed.coordinatorStopped).toBe(true);
|
|
246
|
+
expect(parsed.serverStopped).toBe(false);
|
|
247
|
+
expect(parsed.nothingToStop).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("outputs JSON with serverStopped=true when server stops", async () => {
|
|
251
|
+
const deps: DownDeps = {
|
|
252
|
+
_runCommand: async (cmd) => {
|
|
253
|
+
if (cmd[0] === "legio" && cmd[1] === "coordinator") return COORD_STOP_FAIL;
|
|
254
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_STOP_OK;
|
|
255
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
256
|
+
},
|
|
257
|
+
_projectRoot: "/tmp/test-project",
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
await downCommand(["--json"], deps);
|
|
261
|
+
|
|
262
|
+
const parsed = JSON.parse(capturedStdout.trim()) as {
|
|
263
|
+
coordinatorStopped: boolean;
|
|
264
|
+
serverStopped: boolean;
|
|
265
|
+
nothingToStop: boolean;
|
|
266
|
+
};
|
|
267
|
+
expect(parsed.coordinatorStopped).toBe(false);
|
|
268
|
+
expect(parsed.serverStopped).toBe(true);
|
|
269
|
+
expect(parsed.nothingToStop).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("is graceful when coordinator stop fails (does not throw)", async () => {
|
|
273
|
+
const deps: DownDeps = {
|
|
274
|
+
_runCommand: makeRunCommand({
|
|
275
|
+
"legio coordinator stop": COORD_STOP_FAIL,
|
|
276
|
+
"legio server stop": SERVER_NOT_RUNNING,
|
|
277
|
+
}),
|
|
278
|
+
_projectRoot: "/tmp/test-project",
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Should not throw — graceful no-op
|
|
282
|
+
await expect(downCommand([], deps)).resolves.toBeUndefined();
|
|
283
|
+
expect(capturedStdout).toContain("Nothing to stop");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("stops coordinator before attempting to stop server", async () => {
|
|
287
|
+
const order: string[] = [];
|
|
288
|
+
const deps: DownDeps = {
|
|
289
|
+
_runCommand: async (cmd) => {
|
|
290
|
+
if (cmd[0] === "legio" && cmd[1] === "coordinator") {
|
|
291
|
+
order.push("coordinator");
|
|
292
|
+
return COORD_STOP_OK;
|
|
293
|
+
}
|
|
294
|
+
if (cmd[0] === "legio" && cmd[1] === "server") {
|
|
295
|
+
order.push("server");
|
|
296
|
+
return SERVER_STOP_OK;
|
|
297
|
+
}
|
|
298
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
299
|
+
},
|
|
300
|
+
_projectRoot: "/tmp/test-project",
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
await downCommand([], deps);
|
|
304
|
+
|
|
305
|
+
expect(order[0]).toBe("coordinator");
|
|
306
|
+
expect(order[1]).toBe("server");
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: legio down
|
|
3
|
+
*
|
|
4
|
+
* Single command to cleanly stop the full legio stack:
|
|
5
|
+
* 1. Stop coordinator (if running) via legio coordinator stop
|
|
6
|
+
* This also stops the watchdog and monitor agents.
|
|
7
|
+
* 2. Stop gateway (if running) via legio gateway stop
|
|
8
|
+
* 3. Stop server (if running) via legio server stop
|
|
9
|
+
*
|
|
10
|
+
* Running legio down when nothing is running prints "Nothing to stop".
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
function hasFlag(args: string[], flag: string): boolean {
|
|
16
|
+
return args.includes(flag);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run an external command and collect stdout/stderr + exit code.
|
|
21
|
+
*/
|
|
22
|
+
async function runCommand(
|
|
23
|
+
cmd: string[],
|
|
24
|
+
opts?: { cwd?: string },
|
|
25
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
26
|
+
const [command, ...args] = cmd;
|
|
27
|
+
if (!command) {
|
|
28
|
+
return { stdout: "", stderr: "Empty command", exitCode: 1 };
|
|
29
|
+
}
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const proc = spawn(command, args, {
|
|
32
|
+
cwd: opts?.cwd,
|
|
33
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
34
|
+
});
|
|
35
|
+
const stdoutChunks: Buffer[] = [];
|
|
36
|
+
const stderrChunks: Buffer[] = [];
|
|
37
|
+
proc.stdout?.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
38
|
+
proc.stderr?.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
|
|
39
|
+
proc.on("close", (code) => {
|
|
40
|
+
resolve({
|
|
41
|
+
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
42
|
+
stderr: Buffer.concat(stderrChunks).toString(),
|
|
43
|
+
exitCode: code ?? 1,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Dependency injection interface for testing. */
|
|
50
|
+
export interface DownDeps {
|
|
51
|
+
_runCommand?: (
|
|
52
|
+
cmd: string[],
|
|
53
|
+
opts?: { cwd?: string },
|
|
54
|
+
) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
|
|
55
|
+
_projectRoot?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DOWN_HELP = `legio down — Stop the full legio stack
|
|
59
|
+
|
|
60
|
+
Usage: legio down [options]
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
--json JSON output
|
|
64
|
+
--help, -h Show this help
|
|
65
|
+
|
|
66
|
+
legio down stops the coordinator (including watchdog and monitor), the
|
|
67
|
+
gateway agent, and the server. Running legio down when nothing is running
|
|
68
|
+
is a safe no-op.`;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Entry point for \`legio down [options]\`.
|
|
72
|
+
*
|
|
73
|
+
* @param args - CLI arguments after "down"
|
|
74
|
+
* @param deps - Optional dependency injection for testing
|
|
75
|
+
*/
|
|
76
|
+
export async function downCommand(args: string[], deps: DownDeps = {}): Promise<void> {
|
|
77
|
+
if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
|
|
78
|
+
process.stdout.write(`${DOWN_HELP}\n`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const json = hasFlag(args, "--json");
|
|
83
|
+
const run = deps._runCommand ?? runCommand;
|
|
84
|
+
const projectRoot = deps._projectRoot ?? process.cwd();
|
|
85
|
+
|
|
86
|
+
let coordinatorStopped = false;
|
|
87
|
+
let gatewayStopped = false;
|
|
88
|
+
let serverStopped = false;
|
|
89
|
+
|
|
90
|
+
// 1. Stop coordinator (if running) — also stops watchdog + monitor
|
|
91
|
+
const coordStop = await run(["legio", "coordinator", "stop"], { cwd: projectRoot });
|
|
92
|
+
if (coordStop.exitCode === 0) {
|
|
93
|
+
coordinatorStopped = true;
|
|
94
|
+
if (!json && coordStop.stdout) process.stdout.write(coordStop.stdout);
|
|
95
|
+
}
|
|
96
|
+
// Non-zero exit means coordinator was not running — that's fine, not an error.
|
|
97
|
+
|
|
98
|
+
// 2. Stop gateway (if running) — non-fatal if not running
|
|
99
|
+
const gatewayStop = await run(["legio", "gateway", "stop"], { cwd: projectRoot });
|
|
100
|
+
if (gatewayStop.exitCode === 0) {
|
|
101
|
+
gatewayStopped = true;
|
|
102
|
+
if (!json && gatewayStop.stdout) process.stdout.write(gatewayStop.stdout);
|
|
103
|
+
}
|
|
104
|
+
// Non-zero exit means gateway was not running — that's fine, not an error.
|
|
105
|
+
|
|
106
|
+
// 3. Stop server — delegate all PID checking to `legio server stop`
|
|
107
|
+
const serverStop = await run(["legio", "server", "stop"], { cwd: projectRoot });
|
|
108
|
+
if (serverStop.exitCode === 0 && !serverStop.stdout.includes("not running")) {
|
|
109
|
+
serverStopped = true;
|
|
110
|
+
if (!json && serverStop.stdout) process.stdout.write(serverStop.stdout);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const nothingToStop = !coordinatorStopped && !gatewayStopped && !serverStopped;
|
|
114
|
+
|
|
115
|
+
if (json) {
|
|
116
|
+
process.stdout.write(
|
|
117
|
+
`${JSON.stringify({ coordinatorStopped, gatewayStopped, serverStopped, nothingToStop })}\n`,
|
|
118
|
+
);
|
|
119
|
+
} else if (nothingToStop) {
|
|
120
|
+
process.stdout.write("Nothing to stop\n");
|
|
121
|
+
} else {
|
|
122
|
+
process.stdout.write("Legio stack stopped\n");
|
|
123
|
+
}
|
|
124
|
+
}
|