@inceptionstack/roundhouse 0.5.0 → 0.5.2
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 +2 -1
- package/architecture.md +2 -1
- package/package.json +1 -1
- package/src/cli/cli.ts +111 -11
- package/src/cli/doctor/checks/config.ts +1 -1
- package/src/cli/doctor/checks/systemd.ts +1 -1
- package/src/cli/launchd.ts +144 -0
- package/src/cli/setup.ts +63 -19
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Multiple chat inputs (Telegram, Slack, Discord via [Vercel Chat SDK](https://cha
|
|
|
10
10
|
```bash
|
|
11
11
|
npm install -g @inceptionstack/roundhouse
|
|
12
12
|
roundhouse setup --telegram
|
|
13
|
-
roundhouse start #
|
|
13
|
+
roundhouse start # Auto-starts via LaunchAgent (macOS) or systemd (Linux)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## Architecture
|
|
@@ -483,6 +483,7 @@ No other changes needed — the gateway's unified handler covers all platforms.
|
|
|
483
483
|
| `src/cli/cli.ts` | CLI: start, run, install, tui, update, logs, etc. |
|
|
484
484
|
| `src/cli/env-file.ts` | Shared env file parsing, serialization, and quoting |
|
|
485
485
|
| `src/cli/systemd.ts` | Shared systemd service management (unit generation, install, status) |
|
|
486
|
+
| `src/cli/launchd.ts` | macOS LaunchAgent management (plist generation, install, status) |
|
|
486
487
|
| `src/cli/doctor.ts` | CLI doctor command |
|
|
487
488
|
| `src/cli/doctor/runner.ts` | Shared doctor runner (CLI + gateway) |
|
|
488
489
|
| `src/cli/doctor/checks/` | Individual health check modules |
|
package/architecture.md
CHANGED
|
@@ -276,9 +276,10 @@ cli/cli.ts
|
|
|
276
276
|
├── agents/registry.ts (getAgentSdkPackage)
|
|
277
277
|
├── cli/env-file.ts (parseEnvFile, serializeEnvFile, envQuote)
|
|
278
278
|
├── cli/systemd.ts (resolveExecStart, generateUnit, writeServiceUnit, systemctl, etc.)
|
|
279
|
+
├── cli/launchd.ts (generatePlist, installLaunchAgent, uninstallLaunchAgent, isLaunchAgentRunning)
|
|
279
280
|
├── cli/doctor.ts → cli/doctor/runner.ts → cli/doctor/checks/*
|
|
280
281
|
├── cli/cron.ts → cron/store.ts, cron/runner.ts, cron/helpers.ts
|
|
281
|
-
└── cli/setup.ts → cli/env-file.ts, cli/systemd.ts, cli/setup-telegram.ts, bundle.ts
|
|
282
|
+
└── cli/setup.ts → cli/env-file.ts, cli/systemd.ts, cli/launchd.ts, cli/setup-telegram.ts, bundle.ts
|
|
282
283
|
|
|
283
284
|
gateway.ts also imports:
|
|
284
285
|
→ commands/update.ts → bundle.ts (bundle provisioning)
|
package/package.json
CHANGED
package/src/cli/cli.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { resolve, dirname } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
8
9
|
import { readFile } from "node:fs/promises";
|
|
9
10
|
import { readdirSync, statSync } from "node:fs";
|
|
10
11
|
import { execSync, execFileSync, spawn } from "node:child_process";
|
|
@@ -54,6 +55,28 @@ function run(cmd: string, opts?: { silent?: boolean }): string {
|
|
|
54
55
|
// ── Commands ────────────────────────────────────────
|
|
55
56
|
|
|
56
57
|
async function cmdStart() {
|
|
58
|
+
// macOS: check launchd agent
|
|
59
|
+
if (process.platform === "darwin") {
|
|
60
|
+
const { isLaunchAgentInstalled, isLaunchAgentRunning, PLIST_PATH } = await import("./launchd.ts");
|
|
61
|
+
if (isLaunchAgentInstalled()) {
|
|
62
|
+
if (isLaunchAgentRunning()) {
|
|
63
|
+
console.log("Roundhouse is already running (LaunchAgent).");
|
|
64
|
+
console.log(" Logs: ~/.roundhouse/logs/roundhouse.log");
|
|
65
|
+
console.log(" Stop: roundhouse stop");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Load it
|
|
69
|
+
try {
|
|
70
|
+
execFileSync("launchctl", ["load", PLIST_PATH], { stdio: "pipe" });
|
|
71
|
+
console.log("LaunchAgent started.");
|
|
72
|
+
console.log(" Logs: ~/.roundhouse/logs/roundhouse.log");
|
|
73
|
+
return;
|
|
74
|
+
} catch {
|
|
75
|
+
// Fall through to foreground
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
if (isServiceInstalled()) {
|
|
58
81
|
if (isServiceActive()) {
|
|
59
82
|
console.log("Roundhouse is already running.");
|
|
@@ -74,7 +97,7 @@ async function cmdStart() {
|
|
|
74
97
|
|
|
75
98
|
console.log("No systemd service found. Running in foreground (use Ctrl+C to stop)...");
|
|
76
99
|
if (process.platform !== "darwin") {
|
|
77
|
-
console.log(" Tip: run 'roundhouse
|
|
100
|
+
console.log(" Tip: run 'roundhouse setup --telegram' to install as systemd daemon.\n");
|
|
78
101
|
} else {
|
|
79
102
|
console.log("");
|
|
80
103
|
}
|
|
@@ -147,6 +170,16 @@ async function cmdInstall() {
|
|
|
147
170
|
console.log(" and installs the systemd service — all in one command.\n");
|
|
148
171
|
}
|
|
149
172
|
async function cmdUninstall() {
|
|
173
|
+
if (process.platform === "darwin") {
|
|
174
|
+
const { uninstallLaunchAgent, isLaunchAgentInstalled } = await import("./launchd.ts");
|
|
175
|
+
if (isLaunchAgentInstalled()) {
|
|
176
|
+
await uninstallLaunchAgent();
|
|
177
|
+
console.log(" ✅ LaunchAgent removed. Config preserved at:", CONFIG_PATH);
|
|
178
|
+
} else {
|
|
179
|
+
console.log(" No LaunchAgent installed.");
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
150
183
|
|
|
151
184
|
console.log("[roundhouse] Removing systemd daemon...");
|
|
152
185
|
try { systemctl("stop"); } catch {}
|
|
@@ -166,19 +199,54 @@ async function cmdUpdate() {
|
|
|
166
199
|
}
|
|
167
200
|
|
|
168
201
|
console.log(`[roundhouse] Updated to v${result.latestVersion}`);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
202
|
+
|
|
203
|
+
if (process.platform === "darwin") {
|
|
204
|
+
// Try to restart launchd agent
|
|
205
|
+
const { isLaunchAgentInstalled, PLIST_PATH } = await import("./launchd.ts");
|
|
206
|
+
if (isLaunchAgentInstalled()) {
|
|
207
|
+
try {
|
|
208
|
+
execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
|
|
209
|
+
execFileSync("launchctl", ["load", PLIST_PATH], { stdio: "pipe" });
|
|
210
|
+
console.log("\n ✅ Updated and restarted (LaunchAgent).");
|
|
211
|
+
} catch {
|
|
212
|
+
console.log("\n ✅ Update complete. Restart with: roundhouse start");
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
console.log("\n ✅ Update complete. Restart with: roundhouse start");
|
|
216
|
+
}
|
|
217
|
+
} else if (!isServiceInstalled()) {
|
|
218
|
+
console.log("\n ✅ Update complete. Restart with: roundhouse start");
|
|
219
|
+
} else {
|
|
220
|
+
console.log("\n[roundhouse] Restarting daemon...");
|
|
221
|
+
try {
|
|
222
|
+
systemctl("restart", "Updated and restarted.");
|
|
223
|
+
} catch {
|
|
224
|
+
console.log(" ⚠️ Could not restart. Run: roundhouse start");
|
|
225
|
+
}
|
|
174
226
|
}
|
|
175
227
|
}
|
|
176
228
|
|
|
177
229
|
async function cmdStatus() {
|
|
230
|
+
// macOS: check launchd
|
|
231
|
+
if (process.platform === "darwin") {
|
|
232
|
+
const { isLaunchAgentInstalled, isLaunchAgentRunning } = await import("./launchd.ts");
|
|
233
|
+
if (isLaunchAgentRunning()) {
|
|
234
|
+
console.log("\n ✅ Roundhouse is running (LaunchAgent).\n");
|
|
235
|
+
console.log(" Logs: ~/.roundhouse/logs/roundhouse.log");
|
|
236
|
+
console.log(" Stop: roundhouse stop\n");
|
|
237
|
+
} else if (isLaunchAgentInstalled()) {
|
|
238
|
+
console.log("\n ⚠️ LaunchAgent installed but not running.\n");
|
|
239
|
+
console.log(" Start with: roundhouse start\n");
|
|
240
|
+
} else {
|
|
241
|
+
console.log("\n ❌ Roundhouse is not running.\n");
|
|
242
|
+
console.log(" Start with: roundhouse start\n");
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
178
247
|
if (!isServiceActive()) {
|
|
179
248
|
console.log("\n ❌ Roundhouse is not running.\n");
|
|
180
|
-
console.log("
|
|
181
|
-
console.log(" Or start foreground: roundhouse start\n");
|
|
249
|
+
console.log(" Start with: roundhouse start\n");
|
|
182
250
|
return;
|
|
183
251
|
}
|
|
184
252
|
|
|
@@ -264,15 +332,47 @@ async function cmdStatus() {
|
|
|
264
332
|
console.log();
|
|
265
333
|
}
|
|
266
334
|
|
|
267
|
-
function cmdLogs() {
|
|
335
|
+
async function cmdLogs() {
|
|
336
|
+
if (process.platform === "darwin") {
|
|
337
|
+
const logPath = resolve(homedir(), ".roundhouse", "logs", "roundhouse.log");
|
|
338
|
+
const child = spawn("tail", ["-f", "-n", "100", logPath], { stdio: "inherit" });
|
|
339
|
+
child.on("error", () => console.log("Could not read logs. Check ~/.roundhouse/logs/"));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
268
342
|
const child = spawn("journalctl", ["-u", SERVICE_NAME, "-f", "--no-pager", "-n", "100"], {
|
|
269
343
|
stdio: "inherit",
|
|
270
344
|
});
|
|
271
345
|
child.on("error", () => console.log("Could not read logs. Is the daemon installed?"));
|
|
272
346
|
}
|
|
273
347
|
|
|
274
|
-
function cmdStop() {
|
|
275
|
-
|
|
348
|
+
async function cmdStop() {
|
|
349
|
+
if (process.platform === "darwin") {
|
|
350
|
+
const { isLaunchAgentInstalled, PLIST_PATH } = await import("./launchd.ts");
|
|
351
|
+
if (isLaunchAgentInstalled()) {
|
|
352
|
+
try { execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" }); } catch (e: any) { if (!e.message?.includes("Could not find")) console.warn(" (unload warning:", e.message?.split("\n")[0], ")"); }
|
|
353
|
+
console.log("LaunchAgent stopped.");
|
|
354
|
+
} else {
|
|
355
|
+
console.log("No LaunchAgent installed. Nothing to stop.");
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
systemctl("stop", "Daemon stopped.");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function cmdRestart() {
|
|
363
|
+
if (process.platform === "darwin") {
|
|
364
|
+
const { isLaunchAgentInstalled, PLIST_PATH } = await import("./launchd.ts");
|
|
365
|
+
if (isLaunchAgentInstalled()) {
|
|
366
|
+
try { execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" }); } catch {}
|
|
367
|
+
execFileSync("launchctl", ["load", PLIST_PATH], { stdio: "pipe" });
|
|
368
|
+
console.log("LaunchAgent restarted.");
|
|
369
|
+
} else {
|
|
370
|
+
console.log("No LaunchAgent installed. Run: roundhouse setup --telegram");
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
systemctl("restart", "Daemon restarted.");
|
|
375
|
+
}
|
|
276
376
|
|
|
277
377
|
async function cmdConfig() {
|
|
278
378
|
console.log(`Config path: ${CONFIG_PATH}\n`);
|
|
@@ -47,7 +47,7 @@ export const configChecks: DoctorCheck[] = [
|
|
|
47
47
|
details: [`${ctx.configPath} does not exist`],
|
|
48
48
|
fix: {
|
|
49
49
|
description: "Create default config",
|
|
50
|
-
command: `roundhouse
|
|
50
|
+
command: `roundhouse setup --telegram`,
|
|
51
51
|
run: async () => {
|
|
52
52
|
const configDir = dirname(ctx.configPath);
|
|
53
53
|
await mkdir(configDir, { recursive: true });
|
|
@@ -23,7 +23,7 @@ export const systemdChecks: DoctorCheck[] = [
|
|
|
23
23
|
id: "systemd-unit", category: "systemd", name: "Service unit",
|
|
24
24
|
status: result ? "pass" : "warn",
|
|
25
25
|
summary: result ? "installed" : "not installed",
|
|
26
|
-
details: !result ? ["Run: roundhouse
|
|
26
|
+
details: !result ? ["Run: roundhouse setup --telegram"] : undefined,
|
|
27
27
|
};
|
|
28
28
|
},
|
|
29
29
|
},
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* launchd.ts — macOS launchd service management for roundhouse
|
|
3
|
+
*
|
|
4
|
+
* Generates and installs a LaunchAgent plist so roundhouse
|
|
5
|
+
* auto-starts on login and can be managed via launchctl.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { whichSync } from "./systemd";
|
|
14
|
+
import { ROUNDHOUSE_DIR } from "../config";
|
|
15
|
+
const __dirname = new URL(".", import.meta.url).pathname.replace(/\/$/, "");
|
|
16
|
+
|
|
17
|
+
const LABEL = "com.inceptionstack.roundhouse";
|
|
18
|
+
const PLIST_DIR = resolve(homedir(), "Library", "LaunchAgents");
|
|
19
|
+
export const PLIST_PATH = resolve(PLIST_DIR, `${LABEL}.plist`);
|
|
20
|
+
/**
|
|
21
|
+
* Generate a LaunchAgent plist for roundhouse.
|
|
22
|
+
*/
|
|
23
|
+
export function generatePlist(): string {
|
|
24
|
+
const nodeBin = whichSync("node") || process.execPath;
|
|
25
|
+
const roundhouseBin = whichSync("roundhouse");
|
|
26
|
+
|
|
27
|
+
let programArgs: string[];
|
|
28
|
+
if (roundhouseBin) {
|
|
29
|
+
programArgs = [nodeBin, roundhouseBin, "run"];
|
|
30
|
+
} else {
|
|
31
|
+
// Fallback: tsx path
|
|
32
|
+
const tsxBin = whichSync("tsx") || resolve(__dirname, "..", "..", "node_modules", ".bin", "tsx");
|
|
33
|
+
const cliPath = resolve(__dirname, "cli.ts");
|
|
34
|
+
programArgs = [nodeBin, tsxBin, cliPath, "run"];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const logDir = resolve(ROUNDHOUSE_DIR, "logs");
|
|
38
|
+
let envSection = "";
|
|
39
|
+
const envVars: Record<string, string> = {
|
|
40
|
+
HOME: homedir(),
|
|
41
|
+
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
|
|
42
|
+
ROUNDHOUSE_CONFIG: resolve(ROUNDHOUSE_DIR, "gateway.config.json"),
|
|
43
|
+
NODE_NO_WARNINGS: "1",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
envSection = Object.entries(envVars)
|
|
47
|
+
.map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
|
|
48
|
+
.join("\n");
|
|
49
|
+
|
|
50
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
51
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
52
|
+
<plist version="1.0">
|
|
53
|
+
<dict>
|
|
54
|
+
<key>Label</key>
|
|
55
|
+
<string>${LABEL}</string>
|
|
56
|
+
|
|
57
|
+
<key>ProgramArguments</key>
|
|
58
|
+
<array>
|
|
59
|
+
${programArgs.map(a => ` <string>${escapeXml(a)}</string>`).join("\n")}
|
|
60
|
+
</array>
|
|
61
|
+
|
|
62
|
+
<key>EnvironmentVariables</key>
|
|
63
|
+
<dict>
|
|
64
|
+
${envSection}
|
|
65
|
+
</dict>
|
|
66
|
+
|
|
67
|
+
<key>RunAtLoad</key>
|
|
68
|
+
<true/>
|
|
69
|
+
|
|
70
|
+
<key>KeepAlive</key>
|
|
71
|
+
<true/>
|
|
72
|
+
|
|
73
|
+
<key>StandardOutPath</key>
|
|
74
|
+
<string>${escapeXml(resolve(logDir, "roundhouse.log"))}</string>
|
|
75
|
+
|
|
76
|
+
<key>StandardErrorPath</key>
|
|
77
|
+
<string>${escapeXml(resolve(logDir, "roundhouse.err"))}</string>
|
|
78
|
+
|
|
79
|
+
<key>WorkingDirectory</key>
|
|
80
|
+
<string>${escapeXml(homedir())}</string>
|
|
81
|
+
|
|
82
|
+
<key>ThrottleInterval</key>
|
|
83
|
+
<integer>5</integer>
|
|
84
|
+
</dict>
|
|
85
|
+
</plist>
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Install the plist and load the service.
|
|
91
|
+
*/
|
|
92
|
+
export async function installLaunchAgent(): Promise<void> {
|
|
93
|
+
await mkdir(PLIST_DIR, { recursive: true });
|
|
94
|
+
await mkdir(resolve(ROUNDHOUSE_DIR, "logs"), { recursive: true });
|
|
95
|
+
|
|
96
|
+
const plist = generatePlist();
|
|
97
|
+
await writeFile(PLIST_PATH, plist, { mode: 0o644 });
|
|
98
|
+
|
|
99
|
+
// Unload first if already loaded (ignore errors)
|
|
100
|
+
try {
|
|
101
|
+
execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
|
|
102
|
+
} catch {}
|
|
103
|
+
|
|
104
|
+
// Load the agent
|
|
105
|
+
execFileSync("launchctl", ["load", PLIST_PATH], { stdio: "pipe" });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Unload and remove the launch agent.
|
|
110
|
+
*/
|
|
111
|
+
export async function uninstallLaunchAgent(): Promise<void> {
|
|
112
|
+
try {
|
|
113
|
+
execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
|
|
114
|
+
} catch {}
|
|
115
|
+
|
|
116
|
+
const { unlink } = await import("node:fs/promises");
|
|
117
|
+
try {
|
|
118
|
+
await unlink(PLIST_PATH);
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if the launch agent is loaded and running.
|
|
124
|
+
*/
|
|
125
|
+
export function isLaunchAgentRunning(): boolean {
|
|
126
|
+
try {
|
|
127
|
+
const output = execFileSync("launchctl", ["list", LABEL], { encoding: "utf8", stdio: "pipe" });
|
|
128
|
+
return output.includes(LABEL);
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if the plist file exists.
|
|
136
|
+
*/
|
|
137
|
+
export function isLaunchAgentInstalled(): boolean {
|
|
138
|
+
return existsSync(PLIST_PATH);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function escapeXml(s: string): string {
|
|
142
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
143
|
+
}
|
|
144
|
+
|
package/src/cli/setup.ts
CHANGED
|
@@ -448,11 +448,26 @@ async function stepValidateToken(opts: SetupOptions): Promise<BotInfo> {
|
|
|
448
448
|
async function stepStopGateway(): Promise<void> {
|
|
449
449
|
step("④", "Checking for running gateway...");
|
|
450
450
|
|
|
451
|
-
if (platform()
|
|
452
|
-
|
|
451
|
+
if (platform() === "darwin") {
|
|
452
|
+
try {
|
|
453
|
+
const { isLaunchAgentRunning, PLIST_PATH } = await import("./launchd.ts");
|
|
454
|
+
if (isLaunchAgentRunning()) {
|
|
455
|
+
log(" Stopping existing LaunchAgent...");
|
|
456
|
+
execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
|
|
457
|
+
ok("LaunchAgent stopped");
|
|
458
|
+
} else {
|
|
459
|
+
ok("No running gateway");
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
ok("No running gateway");
|
|
463
|
+
}
|
|
453
464
|
return;
|
|
454
465
|
}
|
|
455
466
|
|
|
467
|
+
if (platform() !== "linux") {
|
|
468
|
+
ok("Skipped (not Linux or macOS)");
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
456
471
|
if (isServiceActive()) {
|
|
457
472
|
log(" Stopping existing gateway...");
|
|
458
473
|
try {
|
|
@@ -835,16 +850,30 @@ async function stepRegisterCommands(opts: SetupOptions): Promise<void> {
|
|
|
835
850
|
}
|
|
836
851
|
|
|
837
852
|
async function stepInstallSystemd(opts: SetupOptions): Promise<void> {
|
|
838
|
-
step("⑩b", "Installing
|
|
853
|
+
step("⑩b", "Installing service...");
|
|
854
|
+
|
|
855
|
+
// macOS: install launchd agent
|
|
856
|
+
if (platform() === "darwin") {
|
|
857
|
+
try {
|
|
858
|
+
const { installLaunchAgent } = await import("./launchd.ts");
|
|
859
|
+
await installLaunchAgent();
|
|
860
|
+
ok("LaunchAgent installed and loaded");
|
|
861
|
+
log(" Logs: ~/.roundhouse/logs/roundhouse.log");
|
|
862
|
+
} catch (err: any) {
|
|
863
|
+
warn(`LaunchAgent install failed: ${err.message}`);
|
|
864
|
+
log(" Run manually: roundhouse start");
|
|
865
|
+
}
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
839
869
|
|
|
840
870
|
if (!opts.systemd) {
|
|
841
871
|
ok("Skipped (--no-systemd)");
|
|
842
872
|
log(" Run manually: roundhouse start");
|
|
843
873
|
return;
|
|
844
874
|
}
|
|
845
|
-
|
|
846
875
|
if (platform() !== "linux") {
|
|
847
|
-
warn(`
|
|
876
|
+
warn(`Service install not supported on ${platform()}`);
|
|
848
877
|
log(" Run manually: roundhouse start");
|
|
849
878
|
return;
|
|
850
879
|
}
|
|
@@ -853,7 +882,7 @@ async function stepInstallSystemd(opts: SetupOptions): Promise<void> {
|
|
|
853
882
|
if (!hasSudoAccess()) {
|
|
854
883
|
warn("No passwordless sudo — cannot install systemd service");
|
|
855
884
|
log(" Run manually: roundhouse start");
|
|
856
|
-
log(" Or install with:
|
|
885
|
+
log(" Or install with: roundhouse setup --telegram");
|
|
857
886
|
return;
|
|
858
887
|
}
|
|
859
888
|
|
|
@@ -1135,25 +1164,40 @@ async function runHeadlessTelegramSetup(opts: SetupOptions): Promise<void> {
|
|
|
1135
1164
|
await stepRegisterCommands(opts);
|
|
1136
1165
|
logger.ok("Bot commands registered");
|
|
1137
1166
|
|
|
1167
|
+
let serviceInstalled = false;
|
|
1138
1168
|
// Step 9: Install and start service
|
|
1139
1169
|
logger.step(9, 9, "service.install", "Installing and starting service");
|
|
1140
|
-
if (!opts.systemd) {
|
|
1170
|
+
if (!opts.systemd && platform() !== "darwin") {
|
|
1141
1171
|
logger.warn("service.skip", "--no-systemd: service not installed. Start manually: roundhouse start");
|
|
1142
1172
|
} else {
|
|
1143
1173
|
await stepInstallSystemd(opts);
|
|
1144
|
-
logger.ok("Service installed and started");
|
|
1145
1174
|
|
|
1146
|
-
// Verify service is active
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1175
|
+
// Verify service is active and set serviceInstalled based on reality
|
|
1176
|
+
if (platform() === "darwin") {
|
|
1177
|
+
try {
|
|
1178
|
+
const { isLaunchAgentRunning } = await import("./launchd.ts");
|
|
1179
|
+
if (isLaunchAgentRunning()) {
|
|
1180
|
+
logger.ok("LaunchAgent is running");
|
|
1181
|
+
serviceInstalled = true;
|
|
1182
|
+
} else {
|
|
1183
|
+
logger.warn("service.state", "LaunchAgent loaded but not yet running");
|
|
1184
|
+
}
|
|
1185
|
+
} catch {
|
|
1186
|
+
logger.warn("service.state", "Could not verify LaunchAgent state");
|
|
1187
|
+
}
|
|
1188
|
+
} else {
|
|
1189
|
+
try {
|
|
1190
|
+
const { execFileSync } = await import("node:child_process");
|
|
1191
|
+
const state = execFileSync("systemctl", ["is-active", "roundhouse"], { encoding: "utf8" }).trim();
|
|
1192
|
+
if (state === "active") {
|
|
1193
|
+
logger.ok("Service is active");
|
|
1194
|
+
serviceInstalled = true;
|
|
1195
|
+
} else {
|
|
1196
|
+
logger.warn("service.state", `Service state: ${state}`);
|
|
1197
|
+
}
|
|
1198
|
+
} catch {
|
|
1199
|
+
logger.warn("service.state", "Could not verify service state");
|
|
1154
1200
|
}
|
|
1155
|
-
} catch {
|
|
1156
|
-
logger.warn("service.state", "Could not verify service state");
|
|
1157
1201
|
}
|
|
1158
1202
|
}
|
|
1159
1203
|
|
|
@@ -1162,7 +1206,7 @@ async function runHeadlessTelegramSetup(opts: SetupOptions): Promise<void> {
|
|
|
1162
1206
|
botUsername: botInfo.username,
|
|
1163
1207
|
pairingLink,
|
|
1164
1208
|
pairingStatus: "pending",
|
|
1165
|
-
serviceInstalled
|
|
1209
|
+
serviceInstalled,
|
|
1166
1210
|
});
|
|
1167
1211
|
log("");
|
|
1168
1212
|
log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|