@messenger-agent/client 0.24.0-alpha.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/dist/args.d.ts +66 -0
- package/dist/args.js +303 -0
- package/dist/assets/skills/manage-coding-agent-client/SKILL.md +114 -0
- package/dist/auto-upgrade.d.ts +43 -0
- package/dist/auto-upgrade.js +184 -0
- package/dist/config-file.d.ts +22 -0
- package/dist/config-file.js +100 -0
- package/dist/control.d.ts +17 -0
- package/dist/control.js +152 -0
- package/dist/exec.d.ts +10 -0
- package/dist/exec.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +158 -0
- package/dist/install.d.ts +26 -0
- package/dist/install.js +142 -0
- package/dist/maintenance.d.ts +63 -0
- package/dist/maintenance.js +194 -0
- package/dist/paths.d.ts +8 -0
- package/dist/paths.js +20 -0
- package/dist/runtime.d.ts +34 -0
- package/dist/runtime.js +241 -0
- package/dist/service.d.ts +41 -0
- package/dist/service.js +208 -0
- package/dist/supervisor.d.ts +45 -0
- package/dist/supervisor.js +282 -0
- package/package.json +31 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { access, cp, lstat, mkdir, readFile, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, delimiter, dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { runCommand } from "./exec.js";
|
|
6
|
+
import { defaultBinDir, defaultRuntimeDir } from "./paths.js";
|
|
7
|
+
const packageName = "@messenger-agent/client";
|
|
8
|
+
export const releaseChannels = ["latest", "beta", "alpha"];
|
|
9
|
+
export function normalizeReleaseChannel(value) {
|
|
10
|
+
if (value === undefined || value === null || value === "")
|
|
11
|
+
return undefined;
|
|
12
|
+
if (value === "stable")
|
|
13
|
+
return "latest";
|
|
14
|
+
if (value === "latest" || value === "beta" || value === "alpha")
|
|
15
|
+
return value;
|
|
16
|
+
throw new Error(`Unknown release channel: ${String(value)} (expected latest, beta, or alpha)`);
|
|
17
|
+
}
|
|
18
|
+
export function parseReleaseChannel(value) {
|
|
19
|
+
return value === "stable" ? "latest" : value === "latest" || value === "beta" || value === "alpha" ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function normalizeUpgradeVersion(version, explicitChannel) {
|
|
22
|
+
const versionChannel = parseReleaseChannel(version);
|
|
23
|
+
const channel = explicitChannel ?? versionChannel;
|
|
24
|
+
return { version: versionChannel ? (channel ?? versionChannel) : version, channel };
|
|
25
|
+
}
|
|
26
|
+
export function isPrereleaseVersion(version) {
|
|
27
|
+
return /^\d+\.\d+\.\d+-/.test(version);
|
|
28
|
+
}
|
|
29
|
+
export function compareSemverVersions(left, right) {
|
|
30
|
+
const [leftVersion] = left.split("+", 1);
|
|
31
|
+
const [rightVersion] = right.split("+", 1);
|
|
32
|
+
const leftMatch = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(leftVersion ?? "");
|
|
33
|
+
const rightMatch = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(rightVersion ?? "");
|
|
34
|
+
if (!leftMatch || !rightMatch) {
|
|
35
|
+
throw new Error(`Automatic upgrades require semantic versions, received: ${left} and ${right}`);
|
|
36
|
+
}
|
|
37
|
+
for (let index = 1; index <= 3; index += 1) {
|
|
38
|
+
const difference = Number(leftMatch[index]) - Number(rightMatch[index]);
|
|
39
|
+
if (difference !== 0)
|
|
40
|
+
return Math.sign(difference);
|
|
41
|
+
}
|
|
42
|
+
return comparePrereleaseIdentifiers(leftMatch[4], rightMatch[4]);
|
|
43
|
+
}
|
|
44
|
+
function comparePrereleaseIdentifiers(left, right) {
|
|
45
|
+
if (!left && !right)
|
|
46
|
+
return 0;
|
|
47
|
+
if (!left)
|
|
48
|
+
return 1;
|
|
49
|
+
if (!right)
|
|
50
|
+
return -1;
|
|
51
|
+
const leftParts = left.split(".");
|
|
52
|
+
const rightParts = right.split(".");
|
|
53
|
+
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
54
|
+
const leftPart = leftParts[index];
|
|
55
|
+
const rightPart = rightParts[index];
|
|
56
|
+
if (leftPart === undefined)
|
|
57
|
+
return -1;
|
|
58
|
+
if (rightPart === undefined)
|
|
59
|
+
return 1;
|
|
60
|
+
const leftNumeric = /^\d+$/.test(leftPart);
|
|
61
|
+
const rightNumeric = /^\d+$/.test(rightPart);
|
|
62
|
+
if (leftNumeric && rightNumeric) {
|
|
63
|
+
const difference = Number(leftPart) - Number(rightPart);
|
|
64
|
+
if (difference !== 0)
|
|
65
|
+
return Math.sign(difference);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (leftNumeric)
|
|
69
|
+
return -1;
|
|
70
|
+
if (rightNumeric)
|
|
71
|
+
return 1;
|
|
72
|
+
if (leftPart !== rightPart)
|
|
73
|
+
return leftPart < rightPart ? -1 : 1;
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
export async function currentPackageVersion() {
|
|
78
|
+
const packagePath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
79
|
+
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
|
80
|
+
return packageJson.version ?? "latest";
|
|
81
|
+
}
|
|
82
|
+
export function npmExecutablePath(nodeExecutable = process.execPath) {
|
|
83
|
+
return join(dirname(nodeExecutable), "npm");
|
|
84
|
+
}
|
|
85
|
+
export function npmProcessPath(nodeExecutable = process.execPath, currentPath = process.env.PATH) {
|
|
86
|
+
const nodeDirectory = dirname(nodeExecutable);
|
|
87
|
+
return currentPath ? `${nodeDirectory}${delimiter}${currentPath}` : nodeDirectory;
|
|
88
|
+
}
|
|
89
|
+
export function currentBundledSkillsDir(moduleUrl = import.meta.url) {
|
|
90
|
+
const moduleDir = dirname(fileURLToPath(moduleUrl));
|
|
91
|
+
return basename(moduleDir) === "src" ? join(moduleDir, "..", "assets", "skills") : join(moduleDir, "assets", "skills");
|
|
92
|
+
}
|
|
93
|
+
export function defaultAgentHomes() {
|
|
94
|
+
return [
|
|
95
|
+
process.env.CODEX_HOME ?? join(homedir(), ".codex"),
|
|
96
|
+
process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"),
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
async function removePath(path) {
|
|
100
|
+
await rm(path, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
async function replaceSymlink(target, linkPath) {
|
|
103
|
+
const temporaryLink = `${linkPath}.tmp-${process.pid}`;
|
|
104
|
+
await removePath(temporaryLink);
|
|
105
|
+
await symlink(target, temporaryLink, "dir");
|
|
106
|
+
await rename(temporaryLink, linkPath);
|
|
107
|
+
}
|
|
108
|
+
async function isSymlink(path) {
|
|
109
|
+
try {
|
|
110
|
+
return (await lstat(path)).isSymbolicLink();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function installRuntime(options) {
|
|
117
|
+
const runtimeDir = options.runtimeDir ?? defaultRuntimeDir;
|
|
118
|
+
const binDir = options.binDir ?? defaultBinDir;
|
|
119
|
+
const channel = parseReleaseChannel(options.version);
|
|
120
|
+
const version = options.version === "current"
|
|
121
|
+
? await currentPackageVersion()
|
|
122
|
+
: channel
|
|
123
|
+
? await resolveChannelPackageVersion(channel)
|
|
124
|
+
: options.version;
|
|
125
|
+
const releaseName = version.replaceAll("/", "_").replaceAll(":", "_");
|
|
126
|
+
const releasesDir = join(runtimeDir, "releases");
|
|
127
|
+
const releaseDir = join(releasesDir, releaseName);
|
|
128
|
+
const currentLink = join(runtimeDir, "current");
|
|
129
|
+
const packageEntry = join(releaseDir, "node_modules", "@messenger-agent", "client", "dist", "index.js");
|
|
130
|
+
const currentPackageEntry = join(currentLink, "node_modules", "@messenger-agent", "client", "dist", "index.js");
|
|
131
|
+
const skillsSourceDir = options.skillsSourceDir ??
|
|
132
|
+
join(releaseDir, "node_modules", "@messenger-agent", "client", "dist", "assets", "skills");
|
|
133
|
+
const wrapperPath = join(binDir, "coding-agent-client-service");
|
|
134
|
+
const cliWrapperPath = join(binDir, "coding-agent");
|
|
135
|
+
await mkdir(releasesDir, { recursive: true, mode: 0o700 });
|
|
136
|
+
await mkdir(binDir, { recursive: true, mode: 0o700 });
|
|
137
|
+
if (!(await pathExists(packageEntry))) {
|
|
138
|
+
const temporaryReleaseDir = `${releaseDir}.tmp-${process.pid}`;
|
|
139
|
+
await removePath(temporaryReleaseDir);
|
|
140
|
+
await mkdir(temporaryReleaseDir, { recursive: true, mode: 0o700 });
|
|
141
|
+
try {
|
|
142
|
+
await runCommand(npmExecutablePath(), ["install", "--prefix", temporaryReleaseDir, "--omit=dev", "--verbose", `${packageName}@${version}`], { env: { PATH: npmProcessPath() } });
|
|
143
|
+
await removePath(releaseDir);
|
|
144
|
+
await rename(temporaryReleaseDir, releaseDir);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
await removePath(temporaryReleaseDir);
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const [defaultCodexHome, defaultClaudeHome] = defaultAgentHomes();
|
|
152
|
+
await syncBundledSkills(skillsSourceDir, [
|
|
153
|
+
options.codexHome ?? defaultCodexHome,
|
|
154
|
+
options.claudeHome ?? defaultClaudeHome,
|
|
155
|
+
]);
|
|
156
|
+
if (!(await isSymlink(currentLink))) {
|
|
157
|
+
await removePath(currentLink);
|
|
158
|
+
}
|
|
159
|
+
await replaceSymlink(releaseDir, currentLink);
|
|
160
|
+
await writeFile(wrapperPath, [
|
|
161
|
+
"#!/bin/sh",
|
|
162
|
+
"set -eu",
|
|
163
|
+
`export AGENT_CONFIG_PATH=${shellSingleQuote(options.configPath)}`,
|
|
164
|
+
`exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(packageEntry)} run-service --config ${shellSingleQuote(options.configPath)}`,
|
|
165
|
+
"",
|
|
166
|
+
].join("\n"), { mode: 0o700 });
|
|
167
|
+
await writeFile(cliWrapperPath, [
|
|
168
|
+
"#!/bin/sh",
|
|
169
|
+
"set -eu",
|
|
170
|
+
`export AGENT_CONFIG_PATH=${shellSingleQuote(options.configPath)}`,
|
|
171
|
+
`exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(currentPackageEntry)} "$@"`,
|
|
172
|
+
"",
|
|
173
|
+
].join("\n"), { mode: 0o700 });
|
|
174
|
+
return { releaseDir, currentLink, wrapperPath, cliWrapperPath, version };
|
|
175
|
+
}
|
|
176
|
+
export async function resolveChannelPackageVersion(channel) {
|
|
177
|
+
const result = await runCommand(npmExecutablePath(), ["view", `${packageName}@${channel}`, "version", "--json"], {
|
|
178
|
+
allowFailure: true,
|
|
179
|
+
env: { PATH: npmProcessPath() },
|
|
180
|
+
});
|
|
181
|
+
if (result.status !== 0) {
|
|
182
|
+
throw new Error(`Unable to resolve the ${channel} coding-agent version: ${result.stderr || result.stdout}`);
|
|
183
|
+
}
|
|
184
|
+
const value = JSON.parse(result.stdout);
|
|
185
|
+
if (typeof value !== "string" || !value)
|
|
186
|
+
throw new Error("Registry returned an invalid coding-agent version");
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
async function pathExists(path) {
|
|
190
|
+
try {
|
|
191
|
+
await access(path);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
export async function syncBundledSkills(skillsSourceDir, agentHomes) {
|
|
199
|
+
const skillName = "manage-coding-agent-client";
|
|
200
|
+
const source = join(skillsSourceDir, skillName);
|
|
201
|
+
const sourceExists = await isDirectory(source);
|
|
202
|
+
for (const home of new Set(agentHomes)) {
|
|
203
|
+
const skillsDir = join(home, "skills");
|
|
204
|
+
const target = join(skillsDir, skillName);
|
|
205
|
+
const temporary = join(skillsDir, `.${skillName}.tmp-${process.pid}`);
|
|
206
|
+
const backup = join(skillsDir, `.${skillName}.old-${process.pid}`);
|
|
207
|
+
await rm(temporary, { recursive: true, force: true });
|
|
208
|
+
await rm(backup, { recursive: true, force: true });
|
|
209
|
+
if (!sourceExists) {
|
|
210
|
+
await rm(target, { recursive: true, force: true });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
await mkdir(skillsDir, { recursive: true, mode: 0o700 });
|
|
214
|
+
await cp(source, temporary, { recursive: true });
|
|
215
|
+
await rename(target, backup).catch((err) => {
|
|
216
|
+
if (err.code !== "ENOENT")
|
|
217
|
+
throw err;
|
|
218
|
+
});
|
|
219
|
+
try {
|
|
220
|
+
await rename(temporary, target);
|
|
221
|
+
await rm(backup, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
await rename(backup, target).catch(() => undefined);
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function isDirectory(path) {
|
|
230
|
+
try {
|
|
231
|
+
return (await lstat(path)).isDirectory();
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (err.code === "ENOENT")
|
|
235
|
+
return false;
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function shellSingleQuote(value) {
|
|
240
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
241
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type CommandResult } from "./exec.js";
|
|
2
|
+
export declare const serviceName = "coding-agent-client";
|
|
3
|
+
export declare const launchdLabel = "vip.elevo.coding-agent.client";
|
|
4
|
+
export type ServiceInstallOptions = {
|
|
5
|
+
configPath: string;
|
|
6
|
+
dataDir: string;
|
|
7
|
+
workspacePath: string;
|
|
8
|
+
serviceCommandPath: string;
|
|
9
|
+
platform?: NodeJS.Platform;
|
|
10
|
+
uid?: number;
|
|
11
|
+
user?: string;
|
|
12
|
+
};
|
|
13
|
+
export type ServiceCommands = {
|
|
14
|
+
status: string;
|
|
15
|
+
logs: string;
|
|
16
|
+
start: string;
|
|
17
|
+
restart: string;
|
|
18
|
+
stop: string;
|
|
19
|
+
uninstall: string;
|
|
20
|
+
};
|
|
21
|
+
export type ServiceInstallResult = {
|
|
22
|
+
servicePath: string;
|
|
23
|
+
commands: ServiceCommands;
|
|
24
|
+
warnings: string[];
|
|
25
|
+
};
|
|
26
|
+
export declare function shellEscape(value: string): string;
|
|
27
|
+
export declare function createSystemdService(options: ServiceInstallOptions): string;
|
|
28
|
+
export declare function createLaunchdPlist(options: ServiceInstallOptions): string;
|
|
29
|
+
type LingerDependencies = {
|
|
30
|
+
commandExists: (command: string) => Promise<boolean>;
|
|
31
|
+
runCommand: (command: string, args: string[], options?: {
|
|
32
|
+
allowFailure?: boolean;
|
|
33
|
+
interactive?: boolean;
|
|
34
|
+
}) => Promise<CommandResult>;
|
|
35
|
+
};
|
|
36
|
+
export declare function ensureSystemdUserLinger(user: string, dependencies?: LingerDependencies): Promise<void>;
|
|
37
|
+
export declare function installService(options: ServiceInstallOptions): Promise<ServiceInstallResult>;
|
|
38
|
+
export declare function linuxCommands(): ServiceCommands;
|
|
39
|
+
export declare function darwinCommands(): ServiceCommands;
|
|
40
|
+
export declare function uninstallService(platform?: NodeJS.Platform): Promise<void>;
|
|
41
|
+
export {};
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { access, chmod, mkdir, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { userInfo } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { runCommand } from "./exec.js";
|
|
5
|
+
export const serviceName = "coding-agent-client";
|
|
6
|
+
export const launchdLabel = "vip.elevo.coding-agent.client";
|
|
7
|
+
export function shellEscape(value) {
|
|
8
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
9
|
+
}
|
|
10
|
+
export function createSystemdService(options) {
|
|
11
|
+
return [
|
|
12
|
+
"[Unit]",
|
|
13
|
+
"Description=Coding Agent Client",
|
|
14
|
+
"After=network-online.target",
|
|
15
|
+
"",
|
|
16
|
+
"[Service]",
|
|
17
|
+
"Type=simple",
|
|
18
|
+
`ExecStart=${systemdEscape(options.serviceCommandPath)}`,
|
|
19
|
+
"Restart=always",
|
|
20
|
+
"RestartSec=5",
|
|
21
|
+
`WorkingDirectory=${systemdEscape(options.workspacePath)}`,
|
|
22
|
+
`Environment=${systemdEnv("AGENT_CONFIG_PATH", options.configPath)}`,
|
|
23
|
+
"",
|
|
24
|
+
"[Install]",
|
|
25
|
+
"WantedBy=default.target",
|
|
26
|
+
"",
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
function systemdEscape(value) {
|
|
30
|
+
return value.replaceAll("\\", "\\\\").replaceAll(" ", "\\x20");
|
|
31
|
+
}
|
|
32
|
+
function systemdEnv(name, value) {
|
|
33
|
+
return `${name}=${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}`;
|
|
34
|
+
}
|
|
35
|
+
function xmlEscape(value) {
|
|
36
|
+
return value
|
|
37
|
+
.replaceAll("&", "&")
|
|
38
|
+
.replaceAll("<", "<")
|
|
39
|
+
.replaceAll(">", ">")
|
|
40
|
+
.replaceAll('"', """)
|
|
41
|
+
.replaceAll("'", "'");
|
|
42
|
+
}
|
|
43
|
+
export function createLaunchdPlist(options) {
|
|
44
|
+
const stdoutPath = join(options.dataDir, "logs", "client-service.out.log");
|
|
45
|
+
const stderrPath = join(options.dataDir, "logs", "client-service.err.log");
|
|
46
|
+
return [
|
|
47
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
48
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
49
|
+
'<plist version="1.0">',
|
|
50
|
+
"<dict>",
|
|
51
|
+
" <key>Label</key>",
|
|
52
|
+
` <string>${xmlEscape(launchdLabel)}</string>`,
|
|
53
|
+
" <key>ProgramArguments</key>",
|
|
54
|
+
" <array>",
|
|
55
|
+
` <string>${xmlEscape(options.serviceCommandPath)}</string>`,
|
|
56
|
+
" </array>",
|
|
57
|
+
" <key>WorkingDirectory</key>",
|
|
58
|
+
` <string>${xmlEscape(options.workspacePath)}</string>`,
|
|
59
|
+
" <key>EnvironmentVariables</key>",
|
|
60
|
+
" <dict>",
|
|
61
|
+
" <key>AGENT_CONFIG_PATH</key>",
|
|
62
|
+
` <string>${xmlEscape(options.configPath)}</string>`,
|
|
63
|
+
" </dict>",
|
|
64
|
+
" <key>RunAtLoad</key>",
|
|
65
|
+
" <true/>",
|
|
66
|
+
" <key>KeepAlive</key>",
|
|
67
|
+
" <true/>",
|
|
68
|
+
" <key>StandardOutPath</key>",
|
|
69
|
+
` <string>${xmlEscape(stdoutPath)}</string>`,
|
|
70
|
+
" <key>StandardErrorPath</key>",
|
|
71
|
+
` <string>${xmlEscape(stderrPath)}</string>`,
|
|
72
|
+
"</dict>",
|
|
73
|
+
"</plist>",
|
|
74
|
+
"",
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
async function commandExists(command) {
|
|
78
|
+
const pathDirs = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
79
|
+
for (const pathDir of pathDirs) {
|
|
80
|
+
try {
|
|
81
|
+
await access(join(pathDir, command));
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// continue
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const defaultLingerDependencies = { commandExists, runCommand };
|
|
91
|
+
export async function ensureSystemdUserLinger(user, dependencies = defaultLingerDependencies) {
|
|
92
|
+
if (!(await dependencies.commandExists("loginctl"))) {
|
|
93
|
+
throw new Error(`loginctl is required to keep the user service running after logout. Enable linger for ${user} and retry installation.`);
|
|
94
|
+
}
|
|
95
|
+
if (await isLingerEnabled(user, dependencies.runCommand))
|
|
96
|
+
return;
|
|
97
|
+
console.log(`Enabling login persistence for ${user}`);
|
|
98
|
+
const enable = await dependencies.runCommand("loginctl", ["enable-linger", user], {
|
|
99
|
+
allowFailure: true,
|
|
100
|
+
interactive: true,
|
|
101
|
+
});
|
|
102
|
+
if (enable.status !== 0) {
|
|
103
|
+
throw new Error(`Unable to enable login persistence for ${user}. Run 'sudo loginctl enable-linger ${user}' and retry installation.`);
|
|
104
|
+
}
|
|
105
|
+
if (!(await isLingerEnabled(user, dependencies.runCommand))) {
|
|
106
|
+
throw new Error(`Login persistence is still disabled for ${user}. Run 'sudo loginctl enable-linger ${user}' and retry installation.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function isLingerEnabled(user, command) {
|
|
110
|
+
const result = await command("loginctl", ["show-user", user, "-p", "Linger"], { allowFailure: true });
|
|
111
|
+
return result.status === 0 && result.stdout.trim() === "Linger=yes";
|
|
112
|
+
}
|
|
113
|
+
export async function installService(options) {
|
|
114
|
+
const platform = options.platform ?? process.platform;
|
|
115
|
+
if (platform === "linux")
|
|
116
|
+
return installSystemdService(options);
|
|
117
|
+
if (platform === "darwin")
|
|
118
|
+
return installLaunchdService(options);
|
|
119
|
+
throw new Error("Only Linux and macOS are supported");
|
|
120
|
+
}
|
|
121
|
+
async function installSystemdService(options) {
|
|
122
|
+
if (!(await commandExists("systemctl"))) {
|
|
123
|
+
throw new Error("systemctl is required to install the user service");
|
|
124
|
+
}
|
|
125
|
+
const user = options.user ?? process.env.USER ?? userInfo().username;
|
|
126
|
+
await ensureSystemdUserLinger(user);
|
|
127
|
+
const servicePath = join(process.env.HOME ?? "", ".config", "systemd", "user", `${serviceName}.service`);
|
|
128
|
+
await mkdir(dirname(servicePath), { recursive: true, mode: 0o700 });
|
|
129
|
+
await writeFile(servicePath, createSystemdService(options), { mode: 0o600 });
|
|
130
|
+
await chmod(servicePath, 0o600);
|
|
131
|
+
await runCommand("systemctl", ["--user", "daemon-reload"]);
|
|
132
|
+
await runCommand("systemctl", ["--user", "enable", "--now", `${serviceName}.service`]);
|
|
133
|
+
await runCommand("systemctl", ["--user", "restart", `${serviceName}.service`]);
|
|
134
|
+
return {
|
|
135
|
+
servicePath,
|
|
136
|
+
commands: linuxCommands(),
|
|
137
|
+
warnings: [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function installLaunchdService(options) {
|
|
141
|
+
if (!(await commandExists("launchctl"))) {
|
|
142
|
+
throw new Error("launchctl is required to install the LaunchAgent");
|
|
143
|
+
}
|
|
144
|
+
const servicePath = join(process.env.HOME ?? "", "Library", "LaunchAgents", `${launchdLabel}.plist`);
|
|
145
|
+
await mkdir(dirname(servicePath), { recursive: true, mode: 0o700 });
|
|
146
|
+
await writeFile(servicePath, createLaunchdPlist(options), { mode: 0o600 });
|
|
147
|
+
await chmod(servicePath, 0o600);
|
|
148
|
+
const uid = options.uid ?? process.getuid?.();
|
|
149
|
+
if (uid === undefined) {
|
|
150
|
+
throw new Error("Unable to determine current uid for launchctl");
|
|
151
|
+
}
|
|
152
|
+
const domain = `gui/${uid}`;
|
|
153
|
+
await runCommand("launchctl", ["bootout", domain, servicePath], { allowFailure: true });
|
|
154
|
+
await runCommand("launchctl", ["bootstrap", domain, servicePath]);
|
|
155
|
+
await runCommand("launchctl", ["kickstart", "-k", `${domain}/${launchdLabel}`]);
|
|
156
|
+
return {
|
|
157
|
+
servicePath,
|
|
158
|
+
commands: darwinCommands(),
|
|
159
|
+
warnings: [],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export function linuxCommands() {
|
|
163
|
+
return {
|
|
164
|
+
status: `systemctl --user status ${serviceName}.service`,
|
|
165
|
+
logs: `journalctl --user -u ${serviceName}.service -n 100 -f`,
|
|
166
|
+
start: `systemctl --user start ${serviceName}.service`,
|
|
167
|
+
restart: `systemctl --user restart ${serviceName}.service`,
|
|
168
|
+
stop: `systemctl --user stop ${serviceName}.service`,
|
|
169
|
+
uninstall: `systemctl --user disable --now ${serviceName}.service && rm -f ~/.config/systemd/user/${serviceName}.service && systemctl --user daemon-reload`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export function darwinCommands() {
|
|
173
|
+
const domain = "gui/$(id -u)";
|
|
174
|
+
const servicePath = `~/Library/LaunchAgents/${launchdLabel}.plist`;
|
|
175
|
+
return {
|
|
176
|
+
status: `launchctl print ${domain}/${launchdLabel}`,
|
|
177
|
+
logs: "tail -f ~/.coding-agent/data/logs/client-service.out.log ~/.coding-agent/data/logs/client-service.err.log",
|
|
178
|
+
start: `launchctl bootstrap ${domain} ${servicePath}`,
|
|
179
|
+
restart: `launchctl bootout ${domain} ${servicePath} 2>/dev/null || true; launchctl bootstrap ${domain} ${servicePath}`,
|
|
180
|
+
stop: `launchctl bootout ${domain} ${servicePath}`,
|
|
181
|
+
uninstall: `launchctl bootout ${domain} ${servicePath}; rm -f ${servicePath}`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export async function uninstallService(platform = process.platform) {
|
|
185
|
+
if (platform === "linux") {
|
|
186
|
+
await runCommand("systemctl", ["--user", "disable", "--now", `${serviceName}.service`], { allowFailure: true });
|
|
187
|
+
await unlink(join(process.env.HOME ?? "", ".config", "systemd", "user", `${serviceName}.service`)).catch((err) => {
|
|
188
|
+
if (err.code !== "ENOENT")
|
|
189
|
+
throw err;
|
|
190
|
+
});
|
|
191
|
+
await runCommand("systemctl", ["--user", "daemon-reload"], { allowFailure: true });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (platform === "darwin") {
|
|
195
|
+
const uid = process.getuid?.();
|
|
196
|
+
if (uid === undefined) {
|
|
197
|
+
throw new Error("Unable to determine current uid for launchctl");
|
|
198
|
+
}
|
|
199
|
+
const servicePath = join(process.env.HOME ?? "", "Library", "LaunchAgents", `${launchdLabel}.plist`);
|
|
200
|
+
await runCommand("launchctl", ["bootout", `gui/${uid}`, servicePath], { allowFailure: true });
|
|
201
|
+
await unlink(servicePath).catch((err) => {
|
|
202
|
+
if (err.code !== "ENOENT")
|
|
203
|
+
throw err;
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
throw new Error("Only Linux and macOS are supported");
|
|
208
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { type AgentActivitySnapshot, type ManagedAgentName } from "@messenger-agent/shared/agent-activity";
|
|
3
|
+
export type { ManagedAgentName } from "@messenger-agent/shared/agent-activity";
|
|
4
|
+
export type AgentActivityStatus = ({
|
|
5
|
+
available: true;
|
|
6
|
+
} & AgentActivitySnapshot) | {
|
|
7
|
+
available: false;
|
|
8
|
+
error: string;
|
|
9
|
+
};
|
|
10
|
+
export type ClientActivityStatus = {
|
|
11
|
+
active: number;
|
|
12
|
+
waiting: number;
|
|
13
|
+
agents: Record<ManagedAgentName, AgentActivityStatus>;
|
|
14
|
+
};
|
|
15
|
+
export type ManagedAgent = {
|
|
16
|
+
name: ManagedProcessName;
|
|
17
|
+
entry: string;
|
|
18
|
+
};
|
|
19
|
+
export type ManagedProcessName = ManagedAgentName | "workspace";
|
|
20
|
+
export type SupervisorOptions = {
|
|
21
|
+
configPath: string;
|
|
22
|
+
workspacePath: string;
|
|
23
|
+
agents?: ManagedAgent[];
|
|
24
|
+
restartBaseMs?: number;
|
|
25
|
+
restartMaxMs?: number;
|
|
26
|
+
spawnProcess?: typeof spawn;
|
|
27
|
+
};
|
|
28
|
+
export declare function resolveAgentEntries(): ManagedAgent[];
|
|
29
|
+
export declare function readWorkspacePathFromConfig(configPath: string): Promise<string>;
|
|
30
|
+
export declare class AgentSupervisor {
|
|
31
|
+
private readonly options;
|
|
32
|
+
private stopping;
|
|
33
|
+
private readonly running;
|
|
34
|
+
private readonly restartBaseMs;
|
|
35
|
+
private readonly restartMaxMs;
|
|
36
|
+
constructor(options: SupervisorOptions);
|
|
37
|
+
start(): void;
|
|
38
|
+
stop(): Promise<void>;
|
|
39
|
+
restartAgent(name: ManagedAgentName): Promise<void>;
|
|
40
|
+
activityStatus(timeoutMs?: number): Promise<ClientActivityStatus>;
|
|
41
|
+
private agentActivityStatus;
|
|
42
|
+
private restartAgentProcess;
|
|
43
|
+
private startAgent;
|
|
44
|
+
}
|
|
45
|
+
export declare function runSupervisor(configPath: string): Promise<void>;
|