agent-relay-server 0.4.25 → 0.4.27
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 +14 -4
- package/package.json +1 -1
- package/src/cli.ts +82 -0
- package/src/upgrade.ts +378 -0
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ Launch with `AGENT_RELAY_PROFILE=backend-tester codex` or
|
|
|
115
115
|
`AGENT_RELAY_PROFILE=backend-tester claude`. Explicit env vars override the
|
|
116
116
|
profile where they overlap.
|
|
117
117
|
|
|
118
|
-
Codex has additional tuning vars documented in [codex/README.md](codex/README.md).
|
|
118
|
+
Codex has additional tuning vars documented in [codex/README.md](codex/README.md). The interactive `codex-relay` launcher starts `codex app-server` and connects the TUI with `codex --remote`; `codex-relay --headless` starts a relay-only session and prints a `codex resume --remote` attach command.
|
|
119
119
|
|
|
120
120
|
Agent IDs are deterministic: `{hostname}-{project}-{session-hash}`.
|
|
121
121
|
|
|
@@ -270,11 +270,21 @@ Lifecycle: `agent-relay daemon status|logs|restart|stop|start|enable|disable|uni
|
|
|
270
270
|
|
|
271
271
|
### Version compatibility
|
|
272
272
|
|
|
273
|
-
The
|
|
273
|
+
The provider integrations check the server version on startup and warn if they diverge. Server, Claude plugin, and Codex packages share version numbers. Upgrade a host with:
|
|
274
274
|
|
|
275
275
|
```bash
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
agent-relay upgrade --dry-run
|
|
277
|
+
agent-relay upgrade --yes
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`agent-relay upgrade` detects optional providers. It refreshes Codex when `codex-relay` is installed, updates the Claude plugin when `claude` has `agent-relay@agent-relay` installed, and restarts the managed `agent-relay.service` unless `--no-restart` is passed.
|
|
281
|
+
|
|
282
|
+
Provider-specific aliases are available when you know what you want:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
agent-relay upgrade --providers codex
|
|
286
|
+
agent-relay upgrade --providers claude
|
|
287
|
+
agent-relay-codex upgrade --dry-run
|
|
278
288
|
```
|
|
279
289
|
|
|
280
290
|
## API Reference
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -16,6 +16,13 @@ import {
|
|
|
16
16
|
formatSetupPlan,
|
|
17
17
|
pathExists,
|
|
18
18
|
} from "./setup";
|
|
19
|
+
import {
|
|
20
|
+
createUpgradePlan,
|
|
21
|
+
detectUpgradeSnapshot,
|
|
22
|
+
executeUpgradePlan,
|
|
23
|
+
formatUpgradePlan,
|
|
24
|
+
type UpgradeProvider,
|
|
25
|
+
} from "./upgrade";
|
|
19
26
|
import { VERSION } from "./config";
|
|
20
27
|
|
|
21
28
|
const HELP = `
|
|
@@ -24,6 +31,8 @@ agent-relay ${VERSION}
|
|
|
24
31
|
Usage:
|
|
25
32
|
agent-relay [start]
|
|
26
33
|
agent-relay setup [--yes] [--dry-run] [--force] [--env-file PATH] [--host HOST] [--port PORT] [--db-path PATH] [--token TOKEN|--no-token]
|
|
34
|
+
agent-relay upgrade [--dry-run] [--version VERSION] [--providers auto|all|codex|claude] [--no-restart] [--yes]
|
|
35
|
+
agent-relay setup upgrade [same options as upgrade]
|
|
27
36
|
agent-relay daemon <install|uninstall|start|stop|restart|enable|disable|status|logs> [options]
|
|
28
37
|
agent-relay pair <target|create|status|accept|reject|hangup|send> [options]
|
|
29
38
|
agent-relay message <target> <body> [options]
|
|
@@ -62,6 +71,13 @@ Daemon options:
|
|
|
62
71
|
--yes Skip confirmation prompts
|
|
63
72
|
--force Overwrite/remove managed-file guardrails
|
|
64
73
|
--json Print structured output
|
|
74
|
+
|
|
75
|
+
Upgrade options:
|
|
76
|
+
--version VERSION Target version (default: latest published server version)
|
|
77
|
+
--providers LIST Provider integrations to upgrade: auto, all, codex, claude
|
|
78
|
+
--no-restart Do not restart agent-relay.service
|
|
79
|
+
--dry-run Print detected state and planned commands
|
|
80
|
+
--yes Skip confirmation prompts
|
|
65
81
|
`.trim();
|
|
66
82
|
|
|
67
83
|
const DAEMON_ACTIONS = new Set<DaemonAction>([
|
|
@@ -87,6 +103,10 @@ export async function handleCli(args: string[]): Promise<"start" | "handled"> {
|
|
|
87
103
|
console.log(VERSION);
|
|
88
104
|
return "handled";
|
|
89
105
|
}
|
|
106
|
+
if (command === "upgrade" || (command === "setup" && args[1] === "upgrade")) {
|
|
107
|
+
await handleUpgradeCommand(command === "setup" ? args.slice(2) : args.slice(1));
|
|
108
|
+
return "handled";
|
|
109
|
+
}
|
|
90
110
|
if (command === "setup" || command === "init") {
|
|
91
111
|
await handleSetupCommand(args.slice(1));
|
|
92
112
|
return "handled";
|
|
@@ -122,6 +142,68 @@ export async function handleCli(args: string[]): Promise<"start" | "handled"> {
|
|
|
122
142
|
throw new Error(`Unknown command "${command}". Run agent-relay --help.`);
|
|
123
143
|
}
|
|
124
144
|
|
|
145
|
+
async function handleUpgradeCommand(args: string[]): Promise<void> {
|
|
146
|
+
let targetVersion: string | undefined;
|
|
147
|
+
let dryRun = false;
|
|
148
|
+
let noRestart = false;
|
|
149
|
+
let yes = false;
|
|
150
|
+
let json = false;
|
|
151
|
+
const providers: UpgradeProvider[] = [];
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < args.length; i++) {
|
|
154
|
+
const arg = args[i];
|
|
155
|
+
if (arg === "--version" && i + 1 < args.length) targetVersion = args[++i];
|
|
156
|
+
else if (arg === "--providers" && i + 1 < args.length) {
|
|
157
|
+
for (const provider of args[++i]!.split(",")) providers.push(parseUpgradeProvider(provider));
|
|
158
|
+
} else if (arg === "--provider" && i + 1 < args.length) providers.push(parseUpgradeProvider(args[++i]!));
|
|
159
|
+
else if (arg === "--codex") providers.push("codex");
|
|
160
|
+
else if (arg === "--claude") providers.push("claude");
|
|
161
|
+
else if (arg === "--all") providers.push("all");
|
|
162
|
+
else if (arg === "--dry-run") dryRun = true;
|
|
163
|
+
else if (arg === "--no-restart") noRestart = true;
|
|
164
|
+
else if (arg === "--yes" || arg === "-y") yes = true;
|
|
165
|
+
else if (arg === "--json") json = true;
|
|
166
|
+
else throw new Error(`Unknown upgrade option "${arg}"`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const snapshot = await detectUpgradeSnapshot({
|
|
170
|
+
...(targetVersion ? { targetVersion } : {}),
|
|
171
|
+
providers,
|
|
172
|
+
noRestart,
|
|
173
|
+
});
|
|
174
|
+
const plan = createUpgradePlan(snapshot, {
|
|
175
|
+
...(targetVersion ? { targetVersion } : {}),
|
|
176
|
+
providers,
|
|
177
|
+
noRestart,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (json) {
|
|
181
|
+
console.log(JSON.stringify({ plan }, null, 2));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (dryRun) {
|
|
186
|
+
console.log(formatUpgradePlan(plan, { dryRun: true }));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!yes) {
|
|
191
|
+
console.log(formatUpgradePlan(plan));
|
|
192
|
+
const ok = await confirm("Run this upgrade plan?");
|
|
193
|
+
if (!ok) {
|
|
194
|
+
console.log("Upgrade cancelled.");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(await executeUpgradePlan(plan));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parseUpgradeProvider(value: string): UpgradeProvider {
|
|
203
|
+
if (value === "auto" || value === "all" || value === "codex" || value === "claude") return value;
|
|
204
|
+
throw new Error(`Unknown upgrade provider "${value}". Expected auto, all, codex, or claude.`);
|
|
205
|
+
}
|
|
206
|
+
|
|
125
207
|
async function handleSlashOrPairCommand(command: string, args: string[]): Promise<void> {
|
|
126
208
|
if (command === "/disconnect") {
|
|
127
209
|
await handlePairCommand(["hangup", ...args]);
|
package/src/upgrade.ts
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { VERSION } from "./config";
|
|
5
|
+
|
|
6
|
+
export type UpgradeProvider = "auto" | "all" | "codex" | "claude";
|
|
7
|
+
|
|
8
|
+
export type UpgradeOptions = {
|
|
9
|
+
targetVersion?: string;
|
|
10
|
+
providers?: UpgradeProvider[];
|
|
11
|
+
noRestart?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type InstalledPackage = {
|
|
15
|
+
version?: string;
|
|
16
|
+
source: "bun" | "npm" | "copied" | "claude-plugin";
|
|
17
|
+
path?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ClaudePluginInstall = {
|
|
21
|
+
id: string;
|
|
22
|
+
version?: string;
|
|
23
|
+
scope?: string;
|
|
24
|
+
installPath?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type UpgradeSnapshot = {
|
|
28
|
+
targetVersion: string;
|
|
29
|
+
serverPackage?: InstalledPackage;
|
|
30
|
+
codexPackage?: InstalledPackage;
|
|
31
|
+
codexCopiedPackage?: InstalledPackage;
|
|
32
|
+
claudePluginInstalls: ClaudePluginInstall[];
|
|
33
|
+
hasCodexCommand: boolean;
|
|
34
|
+
hasClaudeCommand: boolean;
|
|
35
|
+
hasSystemdUserService: boolean;
|
|
36
|
+
runningServerVersion?: string;
|
|
37
|
+
packageManager: "bun" | "npm" | "none";
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type UpgradeAction = {
|
|
41
|
+
label: string;
|
|
42
|
+
command: string[];
|
|
43
|
+
reason: string;
|
|
44
|
+
mutates: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type UpgradePlan = {
|
|
48
|
+
targetVersion: string;
|
|
49
|
+
providers: { codex: boolean; claude: boolean };
|
|
50
|
+
snapshot: UpgradeSnapshot;
|
|
51
|
+
actions: UpgradeAction[];
|
|
52
|
+
warnings: string[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type CommandResult = {
|
|
56
|
+
exitCode: number;
|
|
57
|
+
stdout: string;
|
|
58
|
+
stderr: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type Runner = (command: string[]) => CommandResult;
|
|
62
|
+
|
|
63
|
+
export async function detectUpgradeSnapshot(options: UpgradeOptions = {}): Promise<UpgradeSnapshot> {
|
|
64
|
+
const targetVersion = options.targetVersion ?? await npmViewVersion("agent-relay-server@latest") ?? VERSION;
|
|
65
|
+
const bunPackages = commandExists("bun") ? bunGlobalPackages() : new Map<string, string>();
|
|
66
|
+
const npmPackages = commandExists("npm") ? npmGlobalPackages() : new Map<string, string>();
|
|
67
|
+
const codexCopiedPackage = readPackageVersion(join(homeDir(), ".agent-relay", "codex", "package", "package.json"));
|
|
68
|
+
const claudePluginInstalls = readClaudePluginInstalls(join(homeDir(), ".claude", "plugins", "installed_plugins.json"));
|
|
69
|
+
|
|
70
|
+
const packageManager = bunPackages.has("agent-relay-server") || bunPackages.has("agent-relay-codex")
|
|
71
|
+
? "bun"
|
|
72
|
+
: npmPackages.has("agent-relay-server") || npmPackages.has("agent-relay-codex")
|
|
73
|
+
? "npm"
|
|
74
|
+
: commandExists("bun")
|
|
75
|
+
? "bun"
|
|
76
|
+
: commandExists("npm")
|
|
77
|
+
? "npm"
|
|
78
|
+
: "none";
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
targetVersion,
|
|
82
|
+
serverPackage: installedPackage("agent-relay-server", bunPackages, npmPackages),
|
|
83
|
+
codexPackage: installedPackage("agent-relay-codex", bunPackages, npmPackages),
|
|
84
|
+
codexCopiedPackage: codexCopiedPackage
|
|
85
|
+
? { version: codexCopiedPackage, source: "copied", path: join(homeDir(), ".agent-relay", "codex", "package") }
|
|
86
|
+
: undefined,
|
|
87
|
+
claudePluginInstalls,
|
|
88
|
+
hasCodexCommand: commandExists("agent-relay-codex") || commandExists("codex-relay") || existsSync(join(homeDir(), ".agent-relay", "codex", "package")),
|
|
89
|
+
hasClaudeCommand: commandExists("claude"),
|
|
90
|
+
hasSystemdUserService: hasSystemdUserService("agent-relay.service"),
|
|
91
|
+
runningServerVersion: await runningServerVersion(),
|
|
92
|
+
packageManager,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createUpgradePlan(snapshot: UpgradeSnapshot, options: UpgradeOptions = {}): UpgradePlan {
|
|
97
|
+
const targetVersion = options.targetVersion ?? snapshot.targetVersion;
|
|
98
|
+
const requestedProviders = options.providers?.length ? options.providers : ["auto"];
|
|
99
|
+
const providerSet = new Set(requestedProviders);
|
|
100
|
+
const codexRequested = providerSet.has("all") || providerSet.has("codex") || (providerSet.has("auto") && isCodexDetected(snapshot));
|
|
101
|
+
const claudeRequested = providerSet.has("all") || providerSet.has("claude") || (providerSet.has("auto") && isClaudeRelayDetected(snapshot));
|
|
102
|
+
const actions: UpgradeAction[] = [];
|
|
103
|
+
const warnings: string[] = [];
|
|
104
|
+
|
|
105
|
+
if (snapshot.packageManager === "none") {
|
|
106
|
+
warnings.push("No supported global package manager found. Install Bun or npm, then rerun `agent-relay upgrade`.");
|
|
107
|
+
} else {
|
|
108
|
+
const packages = [`agent-relay-server@${targetVersion}`];
|
|
109
|
+
if (codexRequested) packages.push(`agent-relay-codex@${targetVersion}`);
|
|
110
|
+
const command = snapshot.packageManager === "bun"
|
|
111
|
+
? ["bun", "add", "-g", ...packages]
|
|
112
|
+
: ["npm", "install", "-g", ...packages];
|
|
113
|
+
actions.push({
|
|
114
|
+
label: "Upgrade global packages",
|
|
115
|
+
command,
|
|
116
|
+
reason: `Update server${codexRequested ? " and Codex integration" : ""} packages to ${targetVersion}.`,
|
|
117
|
+
mutates: true,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (codexRequested) {
|
|
122
|
+
actions.push({
|
|
123
|
+
label: "Refresh Codex relay install",
|
|
124
|
+
command: ["agent-relay-codex", "install", "--alias"],
|
|
125
|
+
reason: "Refresh copied Codex package, launcher shims, hooks, and plugin marketplace files.",
|
|
126
|
+
mutates: true,
|
|
127
|
+
});
|
|
128
|
+
} else if (providerSet.has("auto") && !isCodexDetected(snapshot)) {
|
|
129
|
+
warnings.push("Codex provider not detected; skipping Codex integration upgrade.");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (claudeRequested) {
|
|
133
|
+
if (snapshot.hasClaudeCommand && snapshot.claudePluginInstalls.length > 0) {
|
|
134
|
+
for (const scope of uniqueStrings(snapshot.claudePluginInstalls.map((install) => install.scope || "user"))) {
|
|
135
|
+
actions.push({
|
|
136
|
+
label: `Update Claude plugin (${scope})`,
|
|
137
|
+
command: ["claude", "plugin", "update", "agent-relay@agent-relay", "--scope", scope],
|
|
138
|
+
reason: "Update installed Claude Code Agent Relay plugin.",
|
|
139
|
+
mutates: true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
} else if (!snapshot.hasClaudeCommand) {
|
|
143
|
+
warnings.push("Claude Code command not detected; skipping Claude plugin upgrade.");
|
|
144
|
+
} else {
|
|
145
|
+
warnings.push("Claude Code detected, but agent-relay@agent-relay plugin is not installed; skipping Claude plugin upgrade.");
|
|
146
|
+
}
|
|
147
|
+
} else if (providerSet.has("auto") && !isClaudeRelayDetected(snapshot)) {
|
|
148
|
+
warnings.push("Claude Agent Relay plugin not detected; skipping Claude plugin upgrade.");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (snapshot.hasSystemdUserService) {
|
|
152
|
+
if (options.noRestart) {
|
|
153
|
+
warnings.push("agent-relay.service detected but --no-restart was set; restart manually to run the upgraded server.");
|
|
154
|
+
} else {
|
|
155
|
+
actions.push({
|
|
156
|
+
label: "Restart Agent Relay service",
|
|
157
|
+
command: ["systemctl", "--user", "restart", "agent-relay.service"],
|
|
158
|
+
reason: "Restart managed server so /api/stats reports the upgraded version.",
|
|
159
|
+
mutates: true,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
warnings.push("No systemd user service detected; restart any manually running Agent Relay server yourself.");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
targetVersion,
|
|
168
|
+
providers: { codex: codexRequested, claude: claudeRequested },
|
|
169
|
+
snapshot: { ...snapshot, targetVersion },
|
|
170
|
+
actions,
|
|
171
|
+
warnings,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function executeUpgradePlan(plan: UpgradePlan, options: { dryRun?: boolean; runner?: Runner } = {}): Promise<string> {
|
|
176
|
+
if (options.dryRun) return formatUpgradePlan(plan, { dryRun: true });
|
|
177
|
+
const runner = options.runner ?? runCommand;
|
|
178
|
+
const lines = [`Upgrading Agent Relay to ${plan.targetVersion}`];
|
|
179
|
+
for (const action of plan.actions) {
|
|
180
|
+
lines.push(`\n${action.label}`);
|
|
181
|
+
lines.push(`$ ${action.command.map(shellQuote).join(" ")}`);
|
|
182
|
+
const result = runner(action.command);
|
|
183
|
+
if (result.stdout.trim()) lines.push(result.stdout.trim());
|
|
184
|
+
if (result.stderr.trim()) lines.push(result.stderr.trim());
|
|
185
|
+
if (result.exitCode !== 0) {
|
|
186
|
+
throw new Error(`${action.label} failed with exit code ${result.exitCode}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (plan.actions.some((action) => action.command.join(" ") === "systemctl --user restart agent-relay.service")) {
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
191
|
+
const serverVersion = await runningServerVersion();
|
|
192
|
+
if (serverVersion && serverVersion !== plan.targetVersion) {
|
|
193
|
+
throw new Error(`agent-relay.service restarted but /api/stats reports ${serverVersion}, expected ${plan.targetVersion}`);
|
|
194
|
+
}
|
|
195
|
+
if (serverVersion) lines.push(`Running server: ${serverVersion}`);
|
|
196
|
+
}
|
|
197
|
+
lines.push("\nUpgrade commands completed.");
|
|
198
|
+
if (plan.warnings.length > 0) {
|
|
199
|
+
lines.push("\nWarnings:");
|
|
200
|
+
for (const warning of plan.warnings) lines.push(`- ${warning}`);
|
|
201
|
+
}
|
|
202
|
+
return lines.join("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function formatUpgradePlan(plan: UpgradePlan, options: { dryRun?: boolean } = {}): string {
|
|
206
|
+
const lines = [
|
|
207
|
+
`${options.dryRun ? "Upgrade dry run" : "Upgrade plan"}: Agent Relay ${plan.targetVersion}`,
|
|
208
|
+
"",
|
|
209
|
+
"Detected:",
|
|
210
|
+
`- server package: ${formatPackage(plan.snapshot.serverPackage)}`,
|
|
211
|
+
`- running server: ${plan.snapshot.runningServerVersion ?? "unknown"}`,
|
|
212
|
+
`- codex package: ${formatPackage(plan.snapshot.codexPackage)}`,
|
|
213
|
+
`- codex copied package: ${formatPackage(plan.snapshot.codexCopiedPackage)}`,
|
|
214
|
+
`- claude command: ${plan.snapshot.hasClaudeCommand ? "yes" : "no"}`,
|
|
215
|
+
`- claude agent-relay plugin: ${formatClaudePlugins(plan.snapshot.claudePluginInstalls)}`,
|
|
216
|
+
`- systemd user service: ${plan.snapshot.hasSystemdUserService ? "yes" : "no"}`,
|
|
217
|
+
"",
|
|
218
|
+
`Providers: codex=${plan.providers.codex ? "yes" : "no"}, claude=${plan.providers.claude ? "yes" : "no"}`,
|
|
219
|
+
"",
|
|
220
|
+
"Actions:",
|
|
221
|
+
];
|
|
222
|
+
if (plan.actions.length === 0) lines.push("- none");
|
|
223
|
+
for (const action of plan.actions) {
|
|
224
|
+
lines.push(`- ${action.label}: ${action.reason}`);
|
|
225
|
+
lines.push(` ${action.command.map(shellQuote).join(" ")}`);
|
|
226
|
+
}
|
|
227
|
+
if (plan.warnings.length > 0) {
|
|
228
|
+
lines.push("", "Warnings:");
|
|
229
|
+
for (const warning of plan.warnings) lines.push(`- ${warning}`);
|
|
230
|
+
}
|
|
231
|
+
return lines.join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isCodexDetected(snapshot: UpgradeSnapshot): boolean {
|
|
235
|
+
return Boolean(snapshot.codexPackage || snapshot.codexCopiedPackage || snapshot.hasCodexCommand);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isClaudeRelayDetected(snapshot: UpgradeSnapshot): boolean {
|
|
239
|
+
return snapshot.claudePluginInstalls.length > 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function installedPackage(name: string, bunPackages: Map<string, string>, npmPackages: Map<string, string>): InstalledPackage | undefined {
|
|
243
|
+
const bunVersion = bunPackages.get(name);
|
|
244
|
+
if (bunVersion) return { version: bunVersion, source: "bun" };
|
|
245
|
+
const npmVersion = npmPackages.get(name);
|
|
246
|
+
if (npmVersion) return { version: npmVersion, source: "npm" };
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function bunGlobalPackages(): Map<string, string> {
|
|
251
|
+
const result = runCommand(["bun", "pm", "ls", "-g"]);
|
|
252
|
+
const packages = new Map<string, string>();
|
|
253
|
+
for (const line of result.stdout.split("\n")) {
|
|
254
|
+
const match = line.match(/(?:├──|└──)\s+(@?[^@\s]+(?:\/[^@\s]+)?)@([^\s]+)/);
|
|
255
|
+
if (match?.[1] && match[2]) packages.set(match[1], match[2]);
|
|
256
|
+
}
|
|
257
|
+
return packages;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function npmGlobalPackages(): Map<string, string> {
|
|
261
|
+
const result = runCommand(["npm", "list", "-g", "--depth=0", "--json"]);
|
|
262
|
+
const packages = new Map<string, string>();
|
|
263
|
+
if (result.exitCode !== 0 && !result.stdout.trim()) return packages;
|
|
264
|
+
try {
|
|
265
|
+
const parsed = JSON.parse(result.stdout) as { dependencies?: Record<string, { version?: string }> };
|
|
266
|
+
for (const [name, dep] of Object.entries(parsed.dependencies ?? {})) {
|
|
267
|
+
if (dep.version) packages.set(name, dep.version);
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// Ignore unparsable npm output; the plan can still use other signals.
|
|
271
|
+
}
|
|
272
|
+
return packages;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function npmViewVersion(spec: string): Promise<string | undefined> {
|
|
276
|
+
if (!commandExists("npm")) return undefined;
|
|
277
|
+
const result = runCommand(["npm", "view", spec, "version"]);
|
|
278
|
+
return result.exitCode === 0 ? result.stdout.trim() || undefined : undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function runningServerVersion(): Promise<string | undefined> {
|
|
282
|
+
try {
|
|
283
|
+
const headers: Record<string, string> = {};
|
|
284
|
+
if (process.env.AGENT_RELAY_TOKEN) headers["X-Agent-Relay-Token"] = process.env.AGENT_RELAY_TOKEN;
|
|
285
|
+
const relayUrl = (process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850").replace(/\/+$/, "");
|
|
286
|
+
const response = await fetch(`${relayUrl}/api/stats`, { headers });
|
|
287
|
+
if (!response.ok) return undefined;
|
|
288
|
+
const payload = await response.json() as { version?: string };
|
|
289
|
+
return payload.version;
|
|
290
|
+
} catch {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function hasSystemdUserService(name: string): boolean {
|
|
296
|
+
if (!commandExists("systemctl")) return false;
|
|
297
|
+
const result = runCommand(["systemctl", "--user", "status", name, "--no-pager"]);
|
|
298
|
+
return result.exitCode === 0 || result.stdout.includes("Loaded: loaded") || result.stderr.includes("Loaded: loaded");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function readPackageVersion(path: string): string | undefined {
|
|
302
|
+
if (!existsSync(path)) return undefined;
|
|
303
|
+
try {
|
|
304
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as { version?: string };
|
|
305
|
+
return parsed.version;
|
|
306
|
+
} catch {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function readClaudePluginInstalls(path: string): ClaudePluginInstall[] {
|
|
312
|
+
if (!existsSync(path)) return [];
|
|
313
|
+
try {
|
|
314
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as {
|
|
315
|
+
plugins?: Record<string, Array<{ scope?: string; installPath?: string; version?: string }>>;
|
|
316
|
+
};
|
|
317
|
+
return (parsed.plugins?.["agent-relay@agent-relay"] ?? []).map((install) => ({
|
|
318
|
+
id: "agent-relay@agent-relay",
|
|
319
|
+
version: install.version,
|
|
320
|
+
scope: install.scope,
|
|
321
|
+
installPath: install.installPath,
|
|
322
|
+
}));
|
|
323
|
+
} catch {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function commandExists(command: string): boolean {
|
|
329
|
+
const result = runCommand(["which", command]);
|
|
330
|
+
if (result.exitCode === 0) return true;
|
|
331
|
+
return [
|
|
332
|
+
join(homeDir(), ".local", "bin", command),
|
|
333
|
+
join(homeDir(), ".bun", "bin", command),
|
|
334
|
+
join(homeDir(), ".npm-global", "bin", command),
|
|
335
|
+
join(homeDir(), ".agent-relay", "codex", "bin", command),
|
|
336
|
+
].some((path) => existsSync(path));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function runCommand(command: string[]): CommandResult {
|
|
340
|
+
try {
|
|
341
|
+
const result = Bun.spawnSync(command, { stdout: "pipe", stderr: "pipe" });
|
|
342
|
+
return {
|
|
343
|
+
exitCode: result.exitCode,
|
|
344
|
+
stdout: result.stdout?.toString() ?? "",
|
|
345
|
+
stderr: result.stderr?.toString() ?? "",
|
|
346
|
+
};
|
|
347
|
+
} catch (error) {
|
|
348
|
+
return {
|
|
349
|
+
exitCode: 127,
|
|
350
|
+
stdout: "",
|
|
351
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function homeDir(): string {
|
|
357
|
+
return process.env.HOME || process.env.USERPROFILE || homedir();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function uniqueStrings(values: string[]): string[] {
|
|
361
|
+
return [...new Set(values.filter(Boolean))];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function formatPackage(pkg: InstalledPackage | undefined): string {
|
|
365
|
+
if (!pkg) return "not detected";
|
|
366
|
+
const path = pkg.path ? ` at ${pkg.path}` : "";
|
|
367
|
+
return `${pkg.version ?? "unknown"} (${pkg.source}${path})`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function formatClaudePlugins(installs: ClaudePluginInstall[]): string {
|
|
371
|
+
if (installs.length === 0) return "not detected";
|
|
372
|
+
return installs.map((install) => `${install.version ?? "unknown"} (${install.scope ?? "user"})`).join(", ");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function shellQuote(value: string): string {
|
|
376
|
+
if (/^[a-zA-Z0-9_@%+=:,./-]+$/.test(value)) return value;
|
|
377
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
378
|
+
}
|