@messenger-agent/client 0.24.0-alpha.2 → 0.24.0-alpha.4

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.js CHANGED
@@ -1,303 +1,5 @@
1
- import cac from "cac";
2
- import { defaultConfigPath, defaultDataDir, resolvePath } from "./paths.js";
3
- import { normalizeReleaseChannel } from "./runtime.js";
4
- const serverUrlEnvName = "CODING_AGENT_TUNNEL_SERVER_URL";
5
- const publicCommands = [
6
- "install",
7
- "upgrade",
8
- "sessions",
9
- "maintenance",
10
- "status",
11
- "start",
12
- "restart",
13
- "stop",
14
- "uninstall",
15
- ];
16
- const allCommands = [...publicCommands, "run-service"];
17
- export class UsageError extends Error {
18
- }
19
- export function parseCliArgs(argv, env = process.env) {
20
- if (argv[0] === "--version" || argv[0] === "-v") {
21
- if (argv.length > 1)
22
- throw new UsageError(`Unknown option for version: ${argv[1]}`);
23
- return { command: "version" };
24
- }
25
- const command = argv[0];
26
- if (!command || command === "--help" || command === "-h") {
27
- throw new UsageError(usage());
28
- }
29
- if (argv.includes("--")) {
30
- throw new UsageError(`Unknown option for ${command}: --`);
31
- }
32
- assertKnownOptions(argv);
33
- let parsedArgs;
34
- const cli = createCli({
35
- env,
36
- includeInternalCommands: true,
37
- onParsed(args) {
38
- parsedArgs = args;
39
- },
40
- });
41
- try {
42
- cli.parse(["node", "coding-agent", ...argv]);
43
- }
44
- catch (err) {
45
- const message = err instanceof Error ? err.message : String(err);
46
- throw new UsageError(normalizeCacError(message, argv));
47
- }
48
- if (!parsedArgs) {
49
- throw new UsageError(`Unknown command: ${command}`);
50
- }
51
- return parsedArgs;
52
- }
53
- export function usage() {
54
- const cli = createCli({ includeInternalCommands: false });
55
- cli.help();
56
- return `${captureConsoleInfo(() => cli.outputHelp())}\n\nGlobal Options:\n -v, --version Show the installed client version`;
57
- }
58
- function createCli(options) {
59
- const cli = cac("coding-agent");
60
- const env = options.env ?? process.env;
61
- const onParsed = options.onParsed ?? (() => undefined);
62
- cli
63
- .command("install", "Install and configure the local coding-agent service")
64
- .option("--tunnel-id <id>", "Tunnel id")
65
- .option("--token <token>", "Tunnel token")
66
- .option("--workspace <path>", "Workspace path (default: ~/messenger-workspace)")
67
- .option("--server-url <wss-url>", "Tunnel server URL")
68
- .option("--config <path>", "Config file path", { default: defaultConfigPath })
69
- .option("--data-dir <path>", "Data directory", { default: defaultDataDir })
70
- .option("--workspace-id <id>", "Workspace id")
71
- .option("--workspace-name <name>", "Workspace display name")
72
- .option("--channel <channel>", "Auto-upgrade release channel: latest, beta, or alpha")
73
- .action((commandOptions) => {
74
- const tunnelId = optionalString(commandOptions.tunnelId);
75
- const token = optionalString(commandOptions.token);
76
- const workspace = optionalString(commandOptions.workspace);
77
- onParsed({
78
- command: "install",
79
- tunnelId,
80
- token,
81
- workspace: workspace ? resolvePath(workspace) : undefined,
82
- serverUrl: optionalString(commandOptions.serverUrl) ?? env[serverUrlEnvName],
83
- configPath: resolvePath(requiredString(commandOptions.config, "--config")),
84
- dataDir: resolvePath(requiredString(commandOptions.dataDir, "--data-dir")),
85
- workspaceId: optionalString(commandOptions.workspaceId),
86
- workspaceName: optionalString(commandOptions.workspaceName),
87
- channel: parseChannelOption(commandOptions.channel),
88
- });
89
- });
90
- cli
91
- .command("upgrade", "Upgrade the local coding-agent runtime")
92
- .option("--version <version>", "Runtime version or release channel", { default: "latest" })
93
- .option("--channel <channel>", "Auto-upgrade release channel: latest, beta, or alpha")
94
- .option("--config <path>", "Config file path", { default: defaultConfigPath })
95
- .option("--delay <seconds>", "Schedule the upgrade after a delay")
96
- .option("--allow-waiting", "Allow upgrade while sessions wait for user input")
97
- .action((commandOptions) => {
98
- const delaySeconds = optionalDelay(commandOptions.delay);
99
- const allowWaiting = commandOptions.allowWaiting === true;
100
- assertAllowWaitingHasDelay(allowWaiting, delaySeconds);
101
- onParsed({
102
- command: "upgrade",
103
- version: requiredString(commandOptions.version, "--version"),
104
- configPath: resolvePath(requiredString(commandOptions.config, "--config")),
105
- delaySeconds,
106
- allowWaiting,
107
- channel: parseChannelOption(commandOptions.channel),
108
- });
109
- });
110
- cli
111
- .command("sessions", "Show active and waiting client sessions")
112
- .option("--json", "Print machine-readable JSON")
113
- .option("--config <path>", "Config file path", { default: defaultConfigPath })
114
- .action((commandOptions) => {
115
- onParsed({
116
- command: "sessions",
117
- json: commandOptions.json === true,
118
- configPath: resolvePath(requiredString(commandOptions.config, "--config")),
119
- });
120
- });
121
- cli
122
- .command("maintenance <action>", "Inspect or cancel scheduled maintenance")
123
- .option("--json", "Print machine-readable JSON")
124
- .option("--config <path>", "Config file path", { default: defaultConfigPath })
125
- .action((action, commandOptions) => {
126
- if (action !== "status" && action !== "cancel") {
127
- throw new UsageError(`Unknown maintenance action: ${action}`);
128
- }
129
- onParsed({
130
- command: "maintenance",
131
- action,
132
- json: commandOptions.json === true,
133
- configPath: resolvePath(requiredString(commandOptions.config, "--config")),
134
- });
135
- });
136
- for (const command of ["status", "start"]) {
137
- cli.command(command, `${capitalize(command)} the local coding-agent service`).action(() => {
138
- onParsed({ command });
139
- });
140
- }
141
- cli
142
- .command("restart [agent]", "Restart the local service or a specific agent")
143
- .option("--delay <seconds>", "Schedule the restart after a delay")
144
- .option("--allow-waiting", "Allow restart while sessions wait for user input")
145
- .action((agent, commandOptions) => {
146
- if (agent !== undefined && agent !== "codex" && agent !== "claude") {
147
- throw new UsageError(`Unknown agent for restart: ${agent}`);
148
- }
149
- const delaySeconds = optionalDelay(commandOptions.delay);
150
- const allowWaiting = commandOptions.allowWaiting === true;
151
- assertAllowWaitingHasDelay(allowWaiting, delaySeconds);
152
- onParsed({ command: "restart", agent, delaySeconds, allowWaiting });
153
- });
154
- for (const command of ["stop", "uninstall"]) {
155
- cli
156
- .command(command, `${capitalize(command)} the local coding-agent service`)
157
- .option("--delay <seconds>", `Schedule the ${command} after a delay`)
158
- .option("--allow-waiting", `Allow ${command} while sessions wait for user input`)
159
- .action((commandOptions) => {
160
- const delaySeconds = optionalDelay(commandOptions.delay);
161
- const allowWaiting = commandOptions.allowWaiting === true;
162
- assertAllowWaitingHasDelay(allowWaiting, delaySeconds);
163
- onParsed({ command, delaySeconds, allowWaiting });
164
- });
165
- }
166
- if (options.includeInternalCommands) {
167
- cli
168
- .command("run-service", "Run the managed service supervisor")
169
- .option("--config <path>", "Config file path")
170
- .action((commandOptions) => {
171
- const configPath = optionalString(commandOptions.config);
172
- onParsed({ command: "run-service", configPath: configPath ? resolvePath(configPath) : undefined });
173
- });
174
- }
175
- return cli;
176
- }
177
- function requiredString(value, name) {
178
- const text = optionalString(value);
179
- if (!text)
180
- throw new UsageError(`Missing value for ${name}`);
181
- return text;
182
- }
183
- function optionalString(value) {
184
- return typeof value === "string" ? value : undefined;
185
- }
186
- function parseChannelOption(value) {
187
- const text = optionalString(value);
188
- if (text === undefined)
189
- return undefined;
190
- try {
191
- return normalizeReleaseChannel(text);
192
- }
193
- catch {
194
- throw new UsageError(`Unknown release channel for --channel: ${text} (expected latest, beta, or alpha)`);
195
- }
196
- }
197
- function optionalDelay(value) {
198
- if (value === undefined)
199
- return undefined;
200
- const text = typeof value === "number" ? String(value) : requiredString(value, "--delay");
201
- if (!/^\d+$/.test(text))
202
- throw new UsageError("--delay must be an integer between 1 and 86400 seconds");
203
- const delay = Number.parseInt(text, 10);
204
- if (delay < 1 || delay > 86_400) {
205
- throw new UsageError("--delay must be an integer between 1 and 86400 seconds");
206
- }
207
- return delay;
208
- }
209
- function assertAllowWaitingHasDelay(allowWaiting, delaySeconds) {
210
- if (allowWaiting && delaySeconds === undefined) {
211
- throw new UsageError("--allow-waiting requires --delay");
212
- }
213
- }
214
- function normalizeCacError(message, argv) {
215
- if (message.startsWith("Unknown option `")) {
216
- const rawOption = findFirstUnknownOption(argv);
217
- if (rawOption)
218
- return `Unknown option for ${argv[0]}: ${rawOption}`;
219
- }
220
- if (message.startsWith("option `")) {
221
- const match = /^option `([^` ]+)/.exec(message);
222
- if (match)
223
- return `Missing value for ${match[1]}`;
224
- }
225
- if (message.startsWith("Unused args:")) {
226
- const firstUnusedArg = argv.slice(1).find((arg) => !arg.startsWith("-"));
227
- if (firstUnusedArg)
228
- return `Unknown option for ${argv[0]}: ${firstUnusedArg}`;
229
- }
230
- return message;
231
- }
232
- function findFirstUnknownOption(argv) {
233
- const command = argv[0];
234
- if (!command || !isKnownCommand(command))
235
- return argv.find((arg) => arg.startsWith("-"));
236
- const knownOptions = knownOptionsForCommand(command);
237
- for (const arg of argv.slice(1)) {
238
- if (!arg.startsWith("-"))
239
- continue;
240
- const optionName = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
241
- if (!knownOptions.has(optionName))
242
- return optionName;
243
- }
244
- return undefined;
245
- }
246
- function assertKnownOptions(argv) {
247
- const command = argv[0];
248
- if (!command || !isKnownCommand(command))
249
- return;
250
- const unknownOption = findFirstUnknownOption(argv);
251
- if (unknownOption) {
252
- throw new UsageError(`Unknown option for ${command}: ${unknownOption}`);
253
- }
254
- }
255
- function knownOptionsForCommand(command) {
256
- switch (command) {
257
- case "install":
258
- return new Set([
259
- "--tunnel-id",
260
- "--token",
261
- "--workspace",
262
- "--server-url",
263
- "--config",
264
- "--data-dir",
265
- "--workspace-id",
266
- "--workspace-name",
267
- "--channel",
268
- ]);
269
- case "upgrade":
270
- return new Set(["--version", "--channel", "--config", "--delay", "--allow-waiting"]);
271
- case "sessions":
272
- case "maintenance":
273
- return new Set(["--json", "--config"]);
274
- case "restart":
275
- case "stop":
276
- case "uninstall":
277
- return new Set(["--delay", "--allow-waiting"]);
278
- case "run-service":
279
- return new Set(["--config"]);
280
- default:
281
- return new Set();
282
- }
283
- }
284
- function isKnownCommand(command) {
285
- return allCommands.includes(command);
286
- }
287
- function captureConsoleInfo(callback) {
288
- const originalInfo = console.info;
289
- const output = [];
290
- console.info = (...args) => {
291
- output.push(args.map(String).join(" "));
292
- };
293
- try {
294
- callback();
295
- }
296
- finally {
297
- console.info = originalInfo;
298
- }
299
- return output.join("\n");
300
- }
301
- function capitalize(value) {
302
- return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
303
- }
1
+ import S from"cac";import{defaultConfigPath as u,defaultDataDir as U,resolvePath as d}from"./paths.js";import{normalizeReleaseChannel as $}from"./runtime.js";const C="CODING_AGENT_TUNNEL_SERVER_URL",v=["install","upgrade","sessions","maintenance","status","start","restart","stop","uninstall"],W=[...v,"run-service"];class r extends Error{}function D(n,t=process.env){if(n[0]==="--version"||n[0]==="-v"){if(n.length>1)throw new r(`Unknown option for version: ${n[1]}`);return{command:"version"}}const o=n[0];if(!o||o==="--help"||o==="-h")throw new r(b());if(n.includes("--"))throw new r(`Unknown option for ${o}: --`);P(n);let i;const e=h({env:t,includeInternalCommands:!0,onParsed(a){i=a}});try{e.parse(["node","coding-agent",...n])}catch(a){const s=a instanceof Error?a.message:String(a);throw new r(I(s,n))}if(!i)throw new r(`Unknown command: ${o}`);return i}function b(){const n=h({includeInternalCommands:!1});return n.help(),`${A(()=>n.outputHelp())}
2
+
3
+ Global Options:
4
+ -v, --version Show the installed client version`}function h(n){const t=S("coding-agent"),o=n.env??process.env,i=n.onParsed??(()=>{});t.command("install","Install and configure the local coding-agent service").option("--tunnel-id <id>","Tunnel id").option("--token <token>","Tunnel token").option("--workspace <path>","Workspace path (default: ~/messenger-workspace)").option("--server-url <wss-url>","Tunnel server URL").option("--config <path>","Config file path",{default:u}).option("--data-dir <path>","Data directory",{default:U}).option("--workspace-id <id>","Workspace id").option("--workspace-name <name>","Workspace display name").option("--channel <channel>","Auto-upgrade release channel: latest, beta, or alpha").action(e=>{const a=c(e.tunnelId),s=c(e.token),l=c(e.workspace);i({command:"install",tunnelId:a,token:s,workspace:l?d(l):void 0,serverUrl:c(e.serverUrl)??o[C],configPath:d(f(e.config,"--config")),dataDir:d(f(e.dataDir,"--data-dir")),workspaceId:c(e.workspaceId),workspaceName:c(e.workspaceName),channel:g(e.channel)})}),t.command("upgrade","Upgrade the local coding-agent runtime").option("--version <version>","Runtime version or release channel",{default:"latest"}).option("--channel <channel>","Auto-upgrade release channel: latest, beta, or alpha").option("--config <path>","Config file path",{default:u}).option("--delay <seconds>","Schedule the upgrade after a delay").option("--allow-waiting","Allow upgrade while sessions wait for user input").action(e=>{const a=p(e.delay),s=e.allowWaiting===!0;w(s,a),i({command:"upgrade",version:f(e.version,"--version"),configPath:d(f(e.config,"--config")),delaySeconds:a,allowWaiting:s,channel:g(e.channel)})}),t.command("sessions","Show active and waiting client sessions").option("--json","Print machine-readable JSON").option("--config <path>","Config file path",{default:u}).action(e=>{i({command:"sessions",json:e.json===!0,configPath:d(f(e.config,"--config"))})}),t.command("maintenance <action>","Inspect or cancel scheduled maintenance").option("--json","Print machine-readable JSON").option("--config <path>","Config file path",{default:u}).action((e,a)=>{if(e!=="status"&&e!=="cancel")throw new r(`Unknown maintenance action: ${e}`);i({command:"maintenance",action:e,json:a.json===!0,configPath:d(f(a.config,"--config"))})});for(const e of["status","start"])t.command(e,`${y(e)} the local coding-agent service`).action(()=>{i({command:e})});t.command("restart [agent]","Restart the local service or a specific agent").option("--delay <seconds>","Schedule the restart after a delay").option("--allow-waiting","Allow restart while sessions wait for user input").action((e,a)=>{if(e!==void 0&&e!=="codex"&&e!=="claude")throw new r(`Unknown agent for restart: ${e}`);const s=p(a.delay),l=a.allowWaiting===!0;w(l,s),i({command:"restart",agent:e,delaySeconds:s,allowWaiting:l})});for(const e of["stop","uninstall"])t.command(e,`${y(e)} the local coding-agent service`).option("--delay <seconds>",`Schedule the ${e} after a delay`).option("--allow-waiting",`Allow ${e} while sessions wait for user input`).action(a=>{const s=p(a.delay),l=a.allowWaiting===!0;w(l,s),i({command:e,delaySeconds:s,allowWaiting:l})});return n.includeInternalCommands&&t.command("run-service","Run the managed service supervisor").option("--config <path>","Config file path").action(e=>{const a=c(e.config);i({command:"run-service",configPath:a?d(a):void 0})}),t}function f(n,t){const o=c(n);if(!o)throw new r(`Missing value for ${t}`);return o}function c(n){return typeof n=="string"?n:void 0}function g(n){const t=c(n);if(t!==void 0)try{return $(t)}catch{throw new r(`Unknown release channel for --channel: ${t} (expected latest, beta, or alpha)`)}}function p(n){if(n===void 0)return;const t=typeof n=="number"?String(n):f(n,"--delay");if(!/^\d+$/.test(t))throw new r("--delay must be an integer between 1 and 86400 seconds");const o=Number.parseInt(t,10);if(o<1||o>86400)throw new r("--delay must be an integer between 1 and 86400 seconds");return o}function w(n,t){if(n&&t===void 0)throw new r("--allow-waiting requires --delay")}function I(n,t){if(n.startsWith("Unknown option `")){const o=m(t);if(o)return`Unknown option for ${t[0]}: ${o}`}if(n.startsWith("option `")){const o=/^option `([^` ]+)/.exec(n);if(o)return`Missing value for ${o[1]}`}if(n.startsWith("Unused args:")){const o=t.slice(1).find(i=>!i.startsWith("-"));if(o)return`Unknown option for ${t[0]}: ${o}`}return n}function m(n){const t=n[0];if(!t||!k(t))return n.find(i=>i.startsWith("-"));const o=x(t);for(const i of n.slice(1)){if(!i.startsWith("-"))continue;const e=i.includes("=")?i.slice(0,i.indexOf("=")):i;if(!o.has(e))return e}}function P(n){const t=n[0];if(!t||!k(t))return;const o=m(n);if(o)throw new r(`Unknown option for ${t}: ${o}`)}function x(n){switch(n){case"install":return new Set(["--tunnel-id","--token","--workspace","--server-url","--config","--data-dir","--workspace-id","--workspace-name","--channel"]);case"upgrade":return new Set(["--version","--channel","--config","--delay","--allow-waiting"]);case"sessions":case"maintenance":return new Set(["--json","--config"]);case"restart":case"stop":case"uninstall":return new Set(["--delay","--allow-waiting"]);case"run-service":return new Set(["--config"]);default:return new Set}}function k(n){return W.includes(n)}function A(n){const t=console.info,o=[];console.info=(...i)=>{o.push(i.map(String).join(" "))};try{n()}finally{console.info=t}return o.join(`
5
+ `)}function y(n){return`${n.charAt(0).toUpperCase()}${n.slice(1)}`}export{r as UsageError,D as parseCliArgs,b as usage};
@@ -1,184 +1,2 @@
1
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
3
- import { compareSemverVersions, currentPackageVersion, isPrereleaseVersion, resolveChannelPackageVersion, } from "./runtime.js";
4
- const activeMaintenanceStatuses = new Set(["scheduled", "waiting", "running", "restarting", "stopping"]);
5
- export class AutoUpgradeScheduler {
6
- options;
7
- state;
8
- timer;
9
- checkIntervalMs;
10
- pendingRetryMs;
11
- now;
12
- getCurrentVersion;
13
- getLatestVersion;
14
- getChannel;
15
- constructor(options) {
16
- this.options = options;
17
- this.checkIntervalMs = options.checkIntervalMs ?? 2 * 60 * 60 * 1000;
18
- this.pendingRetryMs = options.pendingRetryMs ?? 30_000;
19
- this.now = options.now ?? Date.now;
20
- this.getCurrentVersion = options.getCurrentVersion ?? currentPackageVersion;
21
- this.getLatestVersion = options.getLatestVersion ?? resolveChannelPackageVersion;
22
- this.getChannel = options.getChannel ?? (() => Promise.resolve("latest"));
23
- }
24
- async start() {
25
- this.state = await this.readState();
26
- if (this.state?.status === "pending" || this.state?.status === "scheduled") {
27
- this.arm(this.pendingRetryMs);
28
- return;
29
- }
30
- const lastCheckedAt = this.state?.lastCheckedAt ? Date.parse(this.state.lastCheckedAt) : undefined;
31
- const untilNextCheck = lastCheckedAt === undefined ? this.checkIntervalMs : this.checkIntervalMs - (this.now() - lastCheckedAt);
32
- this.arm(Math.max(0, untilNextCheck));
33
- }
34
- stop() {
35
- if (this.timer)
36
- clearTimeout(this.timer);
37
- this.timer = undefined;
38
- }
39
- getState() {
40
- return this.state ? structuredClone(this.state) : undefined;
41
- }
42
- async checkNow() {
43
- if (this.timer)
44
- clearTimeout(this.timer);
45
- this.timer = undefined;
46
- await this.tick();
47
- }
48
- arm(delayMs) {
49
- if (this.timer)
50
- clearTimeout(this.timer);
51
- this.timer = setTimeout(() => void this.tick(), delayMs);
52
- this.timer.unref();
53
- }
54
- async tick() {
55
- this.timer = undefined;
56
- try {
57
- const channel = await this.getChannel();
58
- const currentVersion = await this.getCurrentVersion();
59
- let latestVersion = this.state?.status === "pending" || this.state?.status === "scheduled" ? this.state.latestVersion : undefined;
60
- let lastCheckedAt = this.state?.lastCheckedAt;
61
- if (!latestVersion) {
62
- latestVersion = await this.getLatestVersion(channel);
63
- lastCheckedAt = this.isoNow();
64
- }
65
- if (compareSemverVersions(latestVersion, currentVersion) <= 0) {
66
- await this.updateState({
67
- status: "up-to-date",
68
- currentVersion,
69
- channel,
70
- latestVersion,
71
- lastCheckedAt,
72
- message: `Runtime ${currentVersion} is up to date on the ${channel} channel`,
73
- });
74
- this.arm(this.checkIntervalMs);
75
- return;
76
- }
77
- if (channel === "latest" && isPrereleaseVersion(currentVersion) && !isPrereleaseVersion(latestVersion)) {
78
- await this.updateState({
79
- status: "up-to-date",
80
- currentVersion,
81
- channel,
82
- latestVersion,
83
- lastCheckedAt,
84
- message: `Runtime ${currentVersion} is a pre-release ahead of the latest stable ${latestVersion}`,
85
- });
86
- this.arm(this.checkIntervalMs);
87
- return;
88
- }
89
- const task = this.options.maintenance.getTask();
90
- if (isMatchingAutomaticUpgrade(task, latestVersion)) {
91
- await this.updateState({
92
- status: "scheduled",
93
- currentVersion,
94
- channel,
95
- latestVersion,
96
- lastCheckedAt,
97
- message: `Waiting to upgrade to ${latestVersion}`,
98
- });
99
- this.arm(this.pendingRetryMs);
100
- return;
101
- }
102
- if (task && activeMaintenanceStatuses.has(task.status)) {
103
- await this.updateState({
104
- status: "pending",
105
- currentVersion,
106
- channel,
107
- latestVersion,
108
- lastCheckedAt,
109
- message: `Waiting for maintenance task ${task.id}`,
110
- });
111
- this.arm(this.pendingRetryMs);
112
- return;
113
- }
114
- if (task?.source === "automatic" && (task.status === "failed" || task.status === "cancelled")) {
115
- await this.updateState({
116
- status: "failed",
117
- currentVersion,
118
- channel,
119
- latestVersion,
120
- lastCheckedAt,
121
- message: task.message ??
122
- (task.status === "cancelled"
123
- ? `Automatic upgrade to ${latestVersion} was cancelled`
124
- : `Automatic upgrade to ${latestVersion} failed`),
125
- });
126
- this.arm(this.checkIntervalMs);
127
- return;
128
- }
129
- await this.options.maintenance.schedule({
130
- operation: { type: "upgrade", version: latestVersion },
131
- delaySeconds: 1,
132
- allowWaiting: true,
133
- source: "automatic",
134
- });
135
- await this.updateState({
136
- status: "scheduled",
137
- currentVersion,
138
- channel,
139
- latestVersion,
140
- lastCheckedAt,
141
- message: `Waiting to upgrade to ${latestVersion}`,
142
- });
143
- this.arm(this.pendingRetryMs);
144
- }
145
- catch (err) {
146
- const currentVersion = await this.getCurrentVersion().catch(() => "unknown");
147
- await this.updateState({
148
- status: "failed",
149
- currentVersion,
150
- channel: this.state?.channel ?? "latest",
151
- latestVersion: this.state?.latestVersion,
152
- lastCheckedAt: this.isoNow(),
153
- message: err instanceof Error ? err.message : String(err),
154
- });
155
- this.arm(this.checkIntervalMs);
156
- }
157
- }
158
- async updateState(state) {
159
- this.state = { ...state, updatedAt: this.isoNow() };
160
- await mkdir(dirname(this.options.statePath), { recursive: true, mode: 0o700 });
161
- const temporaryPath = `${this.options.statePath}.tmp`;
162
- await writeFile(temporaryPath, `${JSON.stringify(this.state, null, 2)}\n`, { mode: 0o600 });
163
- await rename(temporaryPath, this.options.statePath);
164
- }
165
- async readState() {
166
- try {
167
- return JSON.parse(await readFile(this.options.statePath, "utf8"));
168
- }
169
- catch (err) {
170
- if (err.code === "ENOENT")
171
- return undefined;
172
- throw err;
173
- }
174
- }
175
- isoNow() {
176
- return new Date(this.now()).toISOString();
177
- }
178
- }
179
- function isMatchingAutomaticUpgrade(task, version) {
180
- return (task?.source === "automatic" &&
181
- activeMaintenanceStatuses.has(task.status) &&
182
- task.operation.type === "upgrade" &&
183
- task.operation.version === version);
184
- }
1
+ import{mkdir as o,readFile as c,rename as u,writeFile as d}from"node:fs/promises";import{dirname as l}from"node:path";import{compareSemverVersions as m,currentPackageVersion as g,isPrereleaseVersion as r,resolveChannelPackageVersion as p}from"./runtime.js";const h=new Set(["scheduled","waiting","running","restarting","stopping"]);class y{options;state;timer;checkIntervalMs;pendingRetryMs;now;getCurrentVersion;getLatestVersion;getChannel;constructor(t){this.options=t,this.checkIntervalMs=t.checkIntervalMs??7200*1e3,this.pendingRetryMs=t.pendingRetryMs??3e4,this.now=t.now??Date.now,this.getCurrentVersion=t.getCurrentVersion??g,this.getLatestVersion=t.getLatestVersion??p,this.getChannel=t.getChannel??(()=>Promise.resolve("latest"))}async start(){if(this.state=await this.readState(),this.state?.status==="pending"||this.state?.status==="scheduled"){this.arm(this.pendingRetryMs);return}const t=this.state?.lastCheckedAt?Date.parse(this.state.lastCheckedAt):void 0,e=t===void 0?this.checkIntervalMs:this.checkIntervalMs-(this.now()-t);this.arm(Math.max(0,e))}stop(){this.timer&&clearTimeout(this.timer),this.timer=void 0}getState(){return this.state?structuredClone(this.state):void 0}async checkNow(){this.timer&&clearTimeout(this.timer),this.timer=void 0,await this.tick()}arm(t){this.timer&&clearTimeout(this.timer),this.timer=setTimeout(()=>{this.tick()},t),this.timer.unref()}async tick(){this.timer=void 0;try{const t=await this.getChannel(),e=await this.getCurrentVersion();let s=this.state?.status==="pending"||this.state?.status==="scheduled"?this.state.latestVersion:void 0,i=this.state?.lastCheckedAt;if(s||(s=await this.getLatestVersion(t),i=this.isoNow()),m(s,e)<=0){await this.updateState({status:"up-to-date",currentVersion:e,channel:t,latestVersion:s,lastCheckedAt:i,message:`Runtime ${e} is up to date on the ${t} channel`}),this.arm(this.checkIntervalMs);return}if(t==="latest"&&r(e)&&!r(s)){await this.updateState({status:"up-to-date",currentVersion:e,channel:t,latestVersion:s,lastCheckedAt:i,message:`Runtime ${e} is a pre-release ahead of the latest stable ${s}`}),this.arm(this.checkIntervalMs);return}const a=this.options.maintenance.getTask();if(w(a,s)){await this.updateState({status:"scheduled",currentVersion:e,channel:t,latestVersion:s,lastCheckedAt:i,message:`Waiting to upgrade to ${s}`}),this.arm(this.pendingRetryMs);return}if(a&&h.has(a.status)){await this.updateState({status:"pending",currentVersion:e,channel:t,latestVersion:s,lastCheckedAt:i,message:`Waiting for maintenance task ${a.id}`}),this.arm(this.pendingRetryMs);return}if(a?.source==="automatic"&&(a.status==="failed"||a.status==="cancelled")){await this.updateState({status:"failed",currentVersion:e,channel:t,latestVersion:s,lastCheckedAt:i,message:a.message??(a.status==="cancelled"?`Automatic upgrade to ${s} was cancelled`:`Automatic upgrade to ${s} failed`)}),this.arm(this.checkIntervalMs);return}await this.options.maintenance.schedule({operation:{type:"upgrade",version:s},delaySeconds:1,allowWaiting:!0,source:"automatic"}),await this.updateState({status:"scheduled",currentVersion:e,channel:t,latestVersion:s,lastCheckedAt:i,message:`Waiting to upgrade to ${s}`}),this.arm(this.pendingRetryMs)}catch(t){const e=await this.getCurrentVersion().catch(()=>"unknown");await this.updateState({status:"failed",currentVersion:e,channel:this.state?.channel??"latest",latestVersion:this.state?.latestVersion,lastCheckedAt:this.isoNow(),message:t instanceof Error?t.message:String(t)}),this.arm(this.checkIntervalMs)}}async updateState(t){this.state={...t,updatedAt:this.isoNow()},await o(l(this.options.statePath),{recursive:!0,mode:448});const e=`${this.options.statePath}.tmp`;await d(e,`${JSON.stringify(this.state,null,2)}
2
+ `,{mode:384}),await u(e,this.options.statePath)}async readState(){try{return JSON.parse(await c(this.options.statePath,"utf8"))}catch(t){if(t.code==="ENOENT")return;throw t}}isoNow(){return new Date(this.now()).toISOString()}}function w(n,t){return n?.source==="automatic"&&h.has(n.status)&&n.operation.type==="upgrade"&&n.operation.version===t}export{y as AutoUpgradeScheduler};
@@ -1,100 +1 @@
1
- import { constants } from "node:fs";
2
- import { access, chmod, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
4
- import { parse, stringify } from "yaml";
5
- import { normalizeReleaseChannel } from "./runtime.js";
6
- function isRecord(value) {
7
- return typeof value === "object" && value !== null && !Array.isArray(value);
8
- }
9
- function objectAt(config, key) {
10
- const existing = config[key];
11
- if (isRecord(existing))
12
- return existing;
13
- const next = {};
14
- config[key] = next;
15
- return next;
16
- }
17
- export async function ensureClientDirectories(dataDir, configPath) {
18
- await mkdir(dirname(configPath), { recursive: true, mode: 0o700 });
19
- await chmod(dirname(configPath), 0o700);
20
- await mkdir(dataDir, { recursive: true, mode: 0o700 });
21
- await mkdir(join(dataDir, "logs"), { recursive: true, mode: 0o700 });
22
- await mkdir(join(dataDir, "uploads"), { recursive: true, mode: 0o700 });
23
- await mkdir(join(dataDir, "runtime"), { recursive: true, mode: 0o700 });
24
- }
25
- async function fileExists(path) {
26
- try {
27
- await access(path, constants.F_OK);
28
- return true;
29
- }
30
- catch {
31
- return false;
32
- }
33
- }
34
- export async function readConfigYaml(configPath) {
35
- if (!(await fileExists(configPath)))
36
- return {};
37
- const parsed = parse(await readFile(configPath, "utf8"));
38
- if (parsed === null || parsed === undefined)
39
- return {};
40
- if (!isRecord(parsed)) {
41
- throw new Error(`Config file must contain a YAML object: ${configPath}`);
42
- }
43
- return parsed;
44
- }
45
- export function mergeClientConfig(existing, options) {
46
- const config = { ...existing };
47
- config.log_level ??= "info";
48
- if (!options.preserveWorkspaces) {
49
- config.workspaces = [
50
- {
51
- id: options.workspaceId,
52
- name: options.workspaceName ?? `${options.tunnelId} Default`,
53
- path: options.workspacePath,
54
- },
55
- ];
56
- }
57
- config.data_dir = options.dataDir;
58
- const fileUploads = objectAt(config, "file_uploads");
59
- fileUploads.temp_dir = join(options.dataDir, "uploads");
60
- const tunnel = objectAt(config, "tunnel");
61
- tunnel.enabled = true;
62
- if (options.serverUrl)
63
- tunnel.server_url = options.serverUrl;
64
- tunnel.tunnel_id = options.tunnelId;
65
- tunnel.token = options.token;
66
- return config;
67
- }
68
- export async function readUpgradeChannel(configPath) {
69
- const config = await readConfigYaml(configPath);
70
- const autoUpgrade = config.auto_upgrade;
71
- const value = isRecord(autoUpgrade) ? autoUpgrade.channel : undefined;
72
- return normalizeReleaseChannel(value) ?? "latest";
73
- }
74
- export async function writeUpgradeChannel(configPath, channel) {
75
- const config = await readConfigYaml(configPath);
76
- const autoUpgrade = objectAt(config, "auto_upgrade");
77
- autoUpgrade.channel = channel;
78
- await writeConfigYaml(configPath, config);
79
- }
80
- export async function writeConfigYaml(configPath, config) {
81
- await mkdir(dirname(configPath), { recursive: true, mode: 0o700 });
82
- const tempPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
83
- await writeFile(tempPath, stringify(config), { mode: 0o600 });
84
- await chmod(tempPath, 0o600);
85
- await rename(tempPath, configPath);
86
- await chmod(configPath, 0o600);
87
- }
88
- export async function updateClientConfig(options) {
89
- await ensureClientDirectories(options.dataDir, options.configPath);
90
- const existing = await readConfigYaml(options.configPath);
91
- const next = mergeClientConfig(existing, options);
92
- await writeConfigYaml(options.configPath, next);
93
- return next;
94
- }
95
- export async function assertPrivateConfig(configPath) {
96
- const mode = (await stat(configPath)).mode & 0o777;
97
- if ((mode & 0o077) !== 0) {
98
- throw new Error(`Config file permissions are too broad: ${configPath}`);
99
- }
100
- }
1
+ import{constants as m}from"node:fs";import{access as w,chmod as u,mkdir as n,readFile as p,rename as g,stat as x,writeFile as C}from"node:fs/promises";import{dirname as c,join as o}from"node:path";import{parse as y,stringify as v}from"yaml";import{normalizeReleaseChannel as _}from"./runtime.js";function s(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function d(e,r){const t=e[r];if(s(t))return t;const a={};return e[r]=a,a}async function h(e,r){await n(c(r),{recursive:!0,mode:448}),await u(c(r),448),await n(e,{recursive:!0,mode:448}),await n(o(e,"logs"),{recursive:!0,mode:448}),await n(o(e,"uploads"),{recursive:!0,mode:448}),await n(o(e,"runtime"),{recursive:!0,mode:448})}async function k(e){try{return await w(e,m.F_OK),!0}catch{return!1}}async function l(e){if(!await k(e))return{};const r=y(await p(e,"utf8"));if(r==null)return{};if(!s(r))throw new Error(`Config file must contain a YAML object: ${e}`);return r}function U(e,r){const t={...e};t.log_level??="info",r.preserveWorkspaces||(t.workspaces=[{id:r.workspaceId,name:r.workspaceName??`${r.tunnelId} Default`,path:r.workspacePath}]),t.data_dir=r.dataDir;const a=d(t,"file_uploads");a.temp_dir=o(r.dataDir,"uploads");const i=d(t,"tunnel");return i.enabled=!0,r.serverUrl&&(i.server_url=r.serverUrl),i.tunnel_id=r.tunnelId,i.token=r.token,t}async function E(e){const t=(await l(e)).auto_upgrade,a=s(t)?t.channel:void 0;return _(a)??"latest"}async function F(e,r){const t=await l(e),a=d(t,"auto_upgrade");a.channel=r,await f(e,t)}async function f(e,r){await n(c(e),{recursive:!0,mode:448});const t=`${e}.${process.pid}.${Date.now()}.tmp`;await C(t,v(r),{mode:384}),await u(t,384),await g(t,e),await u(e,384)}async function I(e){await h(e.dataDir,e.configPath);const r=await l(e.configPath),t=U(r,e);return await f(e.configPath,t),t}async function Y(e){if(((await x(e)).mode&511&63)!==0)throw new Error(`Config file permissions are too broad: ${e}`)}export{Y as assertPrivateConfig,h as ensureClientDirectories,U as mergeClientConfig,l as readConfigYaml,E as readUpgradeChannel,I as updateClientConfig,f as writeConfigYaml,F as writeUpgradeChannel};