@oddessentials/agent-guild 0.33.2 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -9
- package/bin/agent-guild.mjs +29 -3
- package/package.json +1 -1
- package/src/manager/autostart.mjs +104 -20
- package/src/manager/environment-pins.mjs +289 -0
- package/src/manager/environment-probe.mjs +11 -0
- package/src/manager/environment.mjs +320 -15
- package/src/manager/github-views.mjs +34 -2
- package/src/manager/github.mjs +3 -1
- package/src/manager/launch.mjs +93 -1
- package/src/manager/main.mjs +25 -12
- package/src/manager/server.mjs +37 -6
- package/src/manager/systemd-service.mjs +312 -0
- package/web/app.js +129 -8
- package/web/environment.js +239 -34
- package/web/index.html +23 -7
- package/web/styles.css +28 -0
package/README.md
CHANGED
|
@@ -87,12 +87,16 @@ shell. Pick the default chip to clear a saved choice and follow the default
|
|
|
87
87
|
again. The choice applies to new sessions.
|
|
88
88
|
|
|
89
89
|
**See the manager environment.** The Shell card summarizes detected Node.js,
|
|
90
|
-
Python, Go, .NET SDK, R and Rust versions. **Environment details**
|
|
91
|
-
resolved paths, detection results, .NET runtimes and
|
|
92
|
-
Windows, vfox, uv and pnpm launchers. Tools are listed
|
|
93
|
-
activation is not inferred.
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
Python, Go, .NET SDK, R and Rust versions. **Environment details** opens on
|
|
91
|
+
the manager result: resolved paths, detection results, .NET runtimes and
|
|
92
|
+
detected nvm/NVM for Windows, vfox, uv and pnpm launchers. Tools are listed
|
|
93
|
+
by presence; their activation is not inferred. The same dialog can show three
|
|
94
|
+
other facts, each refreshed on its own: pin files in the working folder
|
|
95
|
+
(configured text, not an installed version), the PATH a Guild session was
|
|
96
|
+
given when it started, and the PATH a new process receives before the shell
|
|
97
|
+
runs its startup files. tmux and herdr sessions have no spawn record to show.
|
|
98
|
+
The card itself stays the manager summary. This describes the computer running
|
|
99
|
+
Agent Guild, including when viewed remotely.
|
|
96
100
|
|
|
97
101
|
Python always uses `python` when it is found on the manager's PATH; otherwise
|
|
98
102
|
it uses `python3`. A broken `python` remains the primary result, with a
|
|
@@ -105,8 +109,10 @@ Discovery runs on first viewing and again on **Refresh**, in an isolated
|
|
|
105
109
|
helper with a neutral temporary working directory. It does not load shell
|
|
106
110
|
profiles, activate tools, download runtimes or modify installations. Unknown
|
|
107
111
|
script/shim launchers and Windows execution aliases are left unverified.
|
|
108
|
-
Refresh checks the
|
|
109
|
-
|
|
112
|
+
Refresh checks the fact selected in the dialog and leaves the others as they
|
|
113
|
+
are. The manager fact reads the manager's current environment and does not
|
|
114
|
+
reload its PATH from a shell. Existing results for that fact remain visible
|
|
115
|
+
during refresh. Go reports
|
|
110
116
|
the local bundled toolchain with automatic switching disabled, and the Node
|
|
111
117
|
version running Agent Guild is also shown in details.
|
|
112
118
|
|
|
@@ -233,6 +239,23 @@ does not start even while **Launch at sign-in** is checked. While it is on,
|
|
|
233
239
|
the setting says when it last ran at sign-in and whether the session manager
|
|
234
240
|
started then.
|
|
235
241
|
|
|
242
|
+
On Linux the setting is one choice: **Only when I start it**, **When I sign
|
|
243
|
+
in** (the desktop entry above) or **When the computer starts**. The last
|
|
244
|
+
runs the session manager as a systemd user service, `agent-guild.service`,
|
|
245
|
+
so it is running before you connect to a machine with no desktop, and
|
|
246
|
+
systemd starts it again if it fails. Only one of the two starters is on at
|
|
247
|
+
a time, and changing the choice never stops the running session manager: one
|
|
248
|
+
started by hand hands over to systemd at its next restart. While the service
|
|
249
|
+
is on, `agent-guild open` and `agent-guild restart` start the session manager
|
|
250
|
+
through systemd, `agent-guild stop` stops it until the next start, and tmux
|
|
251
|
+
cards survive restarts as they do without it. To start before anyone signs
|
|
252
|
+
in, systemd needs lingering, which only an administrator can always turn
|
|
253
|
+
on; the setting shows the command, `sudo loginctl enable-linger <user>`,
|
|
254
|
+
until it is on. It also shows what systemd reports, such as a port another
|
|
255
|
+
session manager was using, and for a failure the command that shows its
|
|
256
|
+
log, `journalctl --user -u agent-guild.service`. `agent-guild status` prints
|
|
257
|
+
the same state, read from systemd when no session manager answers.
|
|
258
|
+
|
|
236
259
|
The browser tab icon adds a rotating green ring whenever any session card says
|
|
237
260
|
**Working**, including Shell and task cards. It mirrors the cards' terminal
|
|
238
261
|
activity, not assistant completion. The ring remains visible with reduced
|
|
@@ -255,7 +278,7 @@ There is no added polling while connected and no additional monitoring process.
|
|
|
255
278
|
| Command | What it does |
|
|
256
279
|
| --- | --- |
|
|
257
280
|
| `agent-guild` or `agent-guild open` | Start the manager if needed and open the page. `--no-browser` prints the URL instead. |
|
|
258
|
-
| `agent-guild status` | Show whether the manager is running and list its sessions. |
|
|
281
|
+
| `agent-guild status` | Show whether the manager is running, how it starts, and list its sessions. |
|
|
259
282
|
| `agent-guild stop` | Stop the manager, ending every session. |
|
|
260
283
|
| `agent-guild restart` | Stop the manager and start it again on the version installed on disk, ending every session. |
|
|
261
284
|
| `agent-guild url` | Print the page URL with its access token. |
|
|
@@ -343,6 +366,14 @@ Any other tool or script can report agents and its model too. See
|
|
|
343
366
|
when started with `AGENT_GUILD_PORT=0`. Enabled entries are updated when
|
|
344
367
|
the manager starts. A macOS entry disabled through `launchctl` stays off
|
|
345
368
|
until you explicitly turn **Launch at sign-in** back on.
|
|
369
|
+
* **When the computer starts**, on Linux, writes
|
|
370
|
+
`~/.config/systemd/user/agent-guild.service` and enables it, which links
|
|
371
|
+
it from `default.target.wants`; the unit runs `boot.sh` from the data
|
|
372
|
+
folder, which runs `agent-guild start` in the foreground. Choosing another
|
|
373
|
+
option disables and removes both. A unit whose Agent Guild package has
|
|
374
|
+
been uninstalled or moved disables and removes itself at the next start.
|
|
375
|
+
Lingering, which lets it start before anyone signs in, is a system
|
|
376
|
+
setting that Agent Guild reads but never changes.
|
|
346
377
|
|
|
347
378
|
The manager makes these outbound requests, and none of them carry your code
|
|
348
379
|
or prompts:
|
package/bin/agent-guild.mjs
CHANGED
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
readRuntimeFile,
|
|
20
20
|
resolvePort,
|
|
21
21
|
} from '../src/manager/config.mjs';
|
|
22
|
-
import { spawnManager } from '../src/manager/launch.mjs';
|
|
22
|
+
import { defaultBoot, serviceFor, spawnManager, startService } from '../src/manager/launch.mjs';
|
|
23
23
|
import { recordSignIn } from '../src/manager/autostart.mjs';
|
|
24
|
+
import { EXIT_PORT_IN_USE, describeStartup, startupState, startupSummary, unitPort } from '../src/manager/systemd-service.mjs';
|
|
24
25
|
|
|
25
26
|
function usage() {
|
|
26
27
|
console.log(`Usage: agent-guild [command] [--no-browser]
|
|
@@ -105,6 +106,11 @@ async function ensureManager({ port = null } = {}) {
|
|
|
105
106
|
const running = await health(knownUrl);
|
|
106
107
|
if (running) return { url: knownUrl, started: false, version: running.version };
|
|
107
108
|
|
|
109
|
+
// While the boot service is on for this port, systemd is the one that starts a manager.
|
|
110
|
+
const { boot, note } = await serviceFor(listenPort);
|
|
111
|
+
if (boot) return startService({ boot, port: listenPort, url: expectedUrl, health });
|
|
112
|
+
if (note) console.log(note);
|
|
113
|
+
|
|
108
114
|
const child = spawnManager({ env: { ...process.env, AGENT_GUILD_PORT: String(listenPort) } });
|
|
109
115
|
|
|
110
116
|
let exited = false;
|
|
@@ -223,15 +229,26 @@ async function cmdStatus() {
|
|
|
223
229
|
const h = await health(url);
|
|
224
230
|
if (!h) {
|
|
225
231
|
console.log('Session manager is not running.');
|
|
232
|
+
// With no manager to ask, the boot service is read from systemd directly.
|
|
233
|
+
const boot = defaultBoot();
|
|
234
|
+
const read = boot && await boot.read().catch(() => null);
|
|
235
|
+
if (read?.enabled) {
|
|
236
|
+
const port = unitPort(await boot.text().catch(() => null));
|
|
237
|
+
console.log(describeStartup(startupState(read.show, { port }), { linger: read.linger, user: boot.user }));
|
|
238
|
+
}
|
|
226
239
|
process.exitCode = 3;
|
|
227
240
|
return;
|
|
228
241
|
}
|
|
229
|
-
const
|
|
242
|
+
const headers = { Authorization: `Bearer ${loadOrCreateToken()}` };
|
|
243
|
+
const res = await fetch(`${url}/api/v1/sessions`, { headers });
|
|
230
244
|
const { sessions = [] } = res.ok ? await res.json() : {};
|
|
231
245
|
const running = sessions.filter((s) => s.status === 'running').length;
|
|
232
246
|
console.log(`Session manager ${h.version} running at ${url} (pid ${h.pid}).`);
|
|
233
247
|
const notice = versionNotice(h.version);
|
|
234
248
|
if (notice) console.log(notice);
|
|
249
|
+
const startup = await fetch(`${url}/api/v1/autostart`, { headers }).then((r) => (r.ok ? r.json() : {}), () => ({}));
|
|
250
|
+
const summary = startupSummary(startup.autostart);
|
|
251
|
+
if (summary) console.log(summary);
|
|
235
252
|
console.log(`${sessions.length} session(s), ${running} running.`);
|
|
236
253
|
for (const s of sessions) {
|
|
237
254
|
const model = s.model ? ` [${s.model.displayName || s.model.name}]` : '';
|
|
@@ -255,10 +272,19 @@ async function main() {
|
|
|
255
272
|
switch (command) {
|
|
256
273
|
case 'open': return cmdOpen({ browser: !flags.has('--no-browser'), signIn: flags.has('--sign-in') });
|
|
257
274
|
case 'start': {
|
|
275
|
+
// `--service`: run by the Linux boot service, which systemd supervises.
|
|
276
|
+
const supervised = flags.has('--service');
|
|
258
277
|
await import('../src/manager/main.mjs').then(async (m) => {
|
|
259
278
|
process.on('uncaughtException', (err) => console.error('[manager] unexpected error:', err));
|
|
260
279
|
process.on('unhandledRejection', (err) => console.error('[manager] unhandled rejection:', err));
|
|
261
|
-
const { shutdown } = await m.startManager()
|
|
280
|
+
const { shutdown } = await m.startManager({ supervised }).catch((err) => {
|
|
281
|
+
// Another manager has the port: an exit systemd does not retry, and the page reports.
|
|
282
|
+
if (supervised && err.code === 'EADDRINUSE') {
|
|
283
|
+
console.error(`agent-guild: ${err.message}`);
|
|
284
|
+
process.exit(EXIT_PORT_IN_USE);
|
|
285
|
+
}
|
|
286
|
+
throw err;
|
|
287
|
+
});
|
|
262
288
|
console.log(`Open: ${pageUrl(readRuntimeFile()?.url ?? baseUrl(), loadOrCreateToken())}`);
|
|
263
289
|
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
264
290
|
process.on(sig, () => shutdown(sig).then(() => process.exit(0)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oddessentials/agent-guild",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Launch and watch AI coding-assistant terminal sessions from one local web page. A bundled session manager owns the terminals so the page can close and reconnect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
// runs a script in the data folder, as on macOS: desktops read Exec lines
|
|
15
15
|
// by different rules (systemd's autostart generator keeps a backslash that
|
|
16
16
|
// GLib removes), so the line holds only paths that read the same in all.
|
|
17
|
+
// Linux also offers a systemd user service that starts the manager when
|
|
18
|
+
// the computer starts (systemd-service.mjs). One starter at a time: the
|
|
19
|
+
// setting is a mode, off, sign-in or boot, and choosing one removes the
|
|
20
|
+
// other, so a sign-in never races the service for the port.
|
|
17
21
|
//
|
|
18
22
|
// Each launcher touches a file in the data folder before it starts Node.js,
|
|
19
23
|
// and `open --sign-in` records whether a manager then answered, so the page
|
|
@@ -35,6 +39,7 @@ import os from 'node:os';
|
|
|
35
39
|
import path from 'node:path';
|
|
36
40
|
import { execFile } from 'node:child_process';
|
|
37
41
|
import { DEFAULT_PORT } from './config.mjs';
|
|
42
|
+
import { BOOT_NOTE, UNIT_UNSAFE, bootSupported, createBootService, journalCommand, lingerCommand, startupState, systemdRunner, unitPort, userName } from './systemd-service.mjs';
|
|
38
43
|
|
|
39
44
|
export const ARGS = ['open', '--no-browser', '--sign-in'];
|
|
40
45
|
export const WINDOWS_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run';
|
|
@@ -61,8 +66,8 @@ export const MAC_NOTE = 'Starts the session manager in the background when you s
|
|
|
61
66
|
export const LINUX_NOTE = 'Starts the session manager in the background when you sign in to a desktop session on this computer. The page does not open. A computer with no desktop session, such as a server reached over SSH, does not start it. Desktop startup settings list it as Agent Guild.';
|
|
62
67
|
/** Added to LINUX_NOTE when the manager itself was not started from a desktop session. */
|
|
63
68
|
export const LINUX_NO_DESKTOP = 'This manager was not started from a desktop session, so this computer may not have one.';
|
|
64
|
-
/**
|
|
65
|
-
const
|
|
69
|
+
/** Linux startup modes. */
|
|
70
|
+
export const MODES = ['off', 'sign-in', 'boot'];
|
|
66
71
|
|
|
67
72
|
const failure = (message) => Object.assign(new Error(message), { status: 500, code: 'autostart_failed' });
|
|
68
73
|
|
|
@@ -318,6 +323,9 @@ async function clearSignIns(dataDir) {
|
|
|
318
323
|
* @param {string|null} [opts.unavailable] a reason autostart cannot be offered, which turns it off here
|
|
319
324
|
* @param {(args: string[]) => Promise<{ status: number, stdout: string, stderr: string }>} [opts.reg] reg.exe, replaceable in tests
|
|
320
325
|
* @param {(args: string[]) => Promise<{ status: number, stdout: string, stderr: string }>} [opts.launchctl] launchctl, replaceable in tests
|
|
326
|
+
* @param {(command: string, args: string[]) => Promise<object>} [opts.systemd] systemctl and journalctl on Linux, replaceable in tests
|
|
327
|
+
* @param {string} [opts.lingerDir] where systemd records lingering users, replaceable in tests
|
|
328
|
+
* @param {number} [opts.pid] this manager, which the service state compares with systemd's
|
|
321
329
|
*/
|
|
322
330
|
export function createAutostart({
|
|
323
331
|
platform = process.platform,
|
|
@@ -333,6 +341,10 @@ export function createAutostart({
|
|
|
333
341
|
reg = runCommand(path.win32.join(env.SystemRoot || env.SYSTEMROOT || env.WINDIR || 'C:\\Windows', 'System32', 'reg.exe')),
|
|
334
342
|
uid = process.getuid?.(),
|
|
335
343
|
launchctl = runCommand('/bin/launchctl'),
|
|
344
|
+
user = userName(uid),
|
|
345
|
+
systemd = systemdRunner({ uid, env }),
|
|
346
|
+
lingerDir,
|
|
347
|
+
pid = process.pid,
|
|
336
348
|
}) {
|
|
337
349
|
const entryExecPath = platform === 'win32' ? execPath : stableExecPath(execPath);
|
|
338
350
|
const attempt = path.join(dataDir, SIGN_IN_ATTEMPT);
|
|
@@ -340,6 +352,9 @@ export function createAutostart({
|
|
|
340
352
|
let note = null;
|
|
341
353
|
let unusable = null;
|
|
342
354
|
let refreshError = null;
|
|
355
|
+
/** The systemd user service, on a Linux that can offer it. */
|
|
356
|
+
let boot = null;
|
|
357
|
+
let bootRefreshError = null;
|
|
343
358
|
let queue = Promise.resolve();
|
|
344
359
|
/** One change or check at a time, so two clicks cannot interleave their writes. */
|
|
345
360
|
const serial = (fn) => {
|
|
@@ -424,13 +439,15 @@ export function createAutostart({
|
|
|
424
439
|
await fs.promises.rm(launcher, { force: true });
|
|
425
440
|
},
|
|
426
441
|
};
|
|
427
|
-
} else if (platform
|
|
442
|
+
} else if (bootSupported({ platform, env, release })) {
|
|
428
443
|
const file = path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'autostart', 'agent-guild.desktop');
|
|
429
444
|
const launcher = path.join(dataDir, LINUX_LAUNCHER);
|
|
430
|
-
const
|
|
445
|
+
const service = createBootService({ env, home, user, uid, dataDir, run: systemd, ...(lingerDir && { lingerDir }) });
|
|
446
|
+
const unsafe = [entryExecPath, script, file, launcher, log, attempt, ...service.paths].find((p) => UNIT_UNSAFE.test(p));
|
|
431
447
|
if (unsafe) {
|
|
432
|
-
unusable = `Not available because this path holds a character (", \`,
|
|
448
|
+
unusable = `Not available because this path holds a character (", \`, $, \\ or a line break) that desktops and systemd read differently in a startup entry: ${unsafe}`;
|
|
433
449
|
} else {
|
|
450
|
+
boot = service;
|
|
434
451
|
const desktop = env.XDG_CURRENT_DESKTOP || env.WAYLAND_DISPLAY || env.DISPLAY;
|
|
435
452
|
note = desktop ? LINUX_NOTE : `${LINUX_NOTE} ${LINUX_NO_DESKTOP}`;
|
|
436
453
|
target = {
|
|
@@ -455,10 +472,25 @@ export function createAutostart({
|
|
|
455
472
|
: unavailable || unusable || (platform === 'linux' ? 'Not available in WSL. Start Agent Guild from Windows to launch it at sign-in.'
|
|
456
473
|
: 'Not available on this operating system.');
|
|
457
474
|
|
|
475
|
+
const FAILED = new Set(['failed', 'port-in-use']);
|
|
476
|
+
/** The service's side of a Linux description: its state, and the commands that are the next step. */
|
|
477
|
+
const describeBoot = async () => {
|
|
478
|
+
const read = await boot.read();
|
|
479
|
+
const base = { note: BOOT_NOTE, user, linger: read.linger };
|
|
480
|
+
if (!read.reachable) return { ...base, available: false, enabled: false, reason: read.reason, state: null, commands: [] };
|
|
481
|
+
const state = startupState(read.show, { pid, port: unitPort(await boot.text()) });
|
|
482
|
+
const commands = [];
|
|
483
|
+
if (read.enabled && FAILED.has(state.kind)) commands.push(journalCommand());
|
|
484
|
+
if (read.enabled && !read.linger) commands.push(lingerCommand(user));
|
|
485
|
+
return { ...base, available: true, enabled: read.enabled, reason: read.enabled ? bootRefreshError : null, state, commands };
|
|
486
|
+
};
|
|
487
|
+
const modeOf = (signIn, bootOn) => (signIn && bootOn ? 'both' : bootOn ? 'boot' : signIn ? 'sign-in' : 'off');
|
|
488
|
+
|
|
458
489
|
const describe = async () => {
|
|
459
490
|
if (!target) return { available: false, enabled: false, reason };
|
|
460
491
|
try {
|
|
461
492
|
const enabled = await target.enabled();
|
|
493
|
+
const linux = boot ? await describeBoot() : null;
|
|
462
494
|
return {
|
|
463
495
|
available: true,
|
|
464
496
|
enabled,
|
|
@@ -467,38 +499,90 @@ export function createAutostart({
|
|
|
467
499
|
// An unreadable record never keeps the setting from being turned off.
|
|
468
500
|
lastRun: enabled ? await lastSignIn(dataDir).catch(() => null) : null,
|
|
469
501
|
log,
|
|
502
|
+
...(linux && { mode: modeOf(enabled, linux.enabled), boot: linux }),
|
|
470
503
|
};
|
|
471
504
|
} catch (err) {
|
|
472
505
|
return { available: false, enabled: false, reason: `Could not read the startup setting: ${err.message}` };
|
|
473
506
|
}
|
|
474
507
|
};
|
|
475
508
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
509
|
+
/**
|
|
510
|
+
* Makes `mode` the one starter. The new one is added and confirmed before
|
|
511
|
+
* the old one goes, and nothing here starts or stops a manager: a service
|
|
512
|
+
* takes over at the manager's next restart or the computer's next start.
|
|
513
|
+
*/
|
|
514
|
+
const apply = async (mode) => {
|
|
515
|
+
const signedIn = await target.enabled().catch(() => false);
|
|
516
|
+
// A fresh start says it has not run yet rather than report an earlier time it was on.
|
|
517
|
+
if (mode !== 'sign-in' || !signedIn) await clearSignIns(dataDir);
|
|
518
|
+
if (mode === 'boot') {
|
|
519
|
+
const read = await boot.read();
|
|
520
|
+
if (!read.reachable) throw Object.assign(new Error(read.reason), { status: 409, code: 'autostart_unavailable' });
|
|
481
521
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
await
|
|
485
|
-
|
|
486
|
-
const state = await describe();
|
|
487
|
-
if (!state.available || state.enabled !== enabled) throw failure(state.reason || 'Could not verify the startup setting.');
|
|
488
|
-
return state;
|
|
522
|
+
await boot.write(command());
|
|
523
|
+
await boot.reload();
|
|
524
|
+
await boot.enable();
|
|
525
|
+
if (!(await boot.read()).enabled) throw failure('systemd did not enable the unit.');
|
|
489
526
|
} catch (err) {
|
|
490
|
-
|
|
527
|
+
// Leave no half-made unit behind; the sign-in entry, if any, is still in place.
|
|
528
|
+
if (!read.enabled) await boot.remove().catch(() => {});
|
|
529
|
+
throw err;
|
|
491
530
|
}
|
|
531
|
+
await target.disable();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
await (mode === 'sign-in' ? target.enable() : target.disable());
|
|
535
|
+
if (boot) await boot.remove();
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
/** Applies the mode `pick` resolves to, inside the queue, and checks the result against what the OS reports. */
|
|
539
|
+
const change = (pick) => serial(async () => {
|
|
540
|
+
if (!target) throw Object.assign(new Error(reason), { status: 409, code: 'autostart_unavailable' });
|
|
541
|
+
const mode = await pick();
|
|
542
|
+
if (mode === 'boot' && !boot) throw Object.assign(new Error('Starting when the computer starts is offered only on Linux with systemd.'), { status: 409, code: 'autostart_unavailable' });
|
|
543
|
+
try {
|
|
544
|
+
await apply(mode);
|
|
545
|
+
refreshError = null;
|
|
546
|
+
bootRefreshError = null;
|
|
547
|
+
const state = await describe();
|
|
548
|
+
const done = boot ? state.mode === mode : state.enabled === (mode === 'sign-in');
|
|
549
|
+
if (!state.available || !done) throw failure(state.reason || 'Could not verify the startup setting.');
|
|
550
|
+
return state;
|
|
551
|
+
} catch (err) {
|
|
552
|
+
throw err.code === 'autostart_failed' || err.code === 'autostart_unavailable' ? err : failure(`Could not change the startup setting: ${err.message}`);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
describe: () => serial(describe),
|
|
558
|
+
/**
|
|
559
|
+
* Turns the sign-in entry on or off and resolves to the new description.
|
|
560
|
+
* On Linux, on means the sign-in mode, and off keeps a service that is on.
|
|
561
|
+
*/
|
|
562
|
+
set: (enabled) => change(async () => {
|
|
563
|
+
if (enabled) return 'sign-in';
|
|
564
|
+
return boot && ['boot', 'both'].includes((await describe()).mode) ? 'boot' : 'off';
|
|
492
565
|
}),
|
|
493
|
-
/**
|
|
566
|
+
/** Linux: makes `mode` (off, sign-in or boot) the one way the manager starts. Elsewhere boot is unavailable. */
|
|
567
|
+
setMode: (mode) => change(async () => mode),
|
|
568
|
+
/** Points what is on at this manager's Node.js, package and port. */
|
|
494
569
|
refresh: () => serial(async () => {
|
|
570
|
+
let first = null;
|
|
495
571
|
try {
|
|
496
572
|
if (target && await target.enabled()) await target.write();
|
|
497
573
|
refreshError = null;
|
|
498
574
|
} catch (err) {
|
|
499
575
|
refreshError = `Could not update the sign-in entry: ${err.message}`;
|
|
500
|
-
|
|
576
|
+
first = err;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
if (boot && (await boot.read()).enabled && await boot.write(command())) await boot.reload();
|
|
580
|
+
bootRefreshError = null;
|
|
581
|
+
} catch (err) {
|
|
582
|
+
bootRefreshError = `Could not update the systemd unit: ${err.message}`;
|
|
583
|
+
first ??= err;
|
|
501
584
|
}
|
|
585
|
+
if (first) throw first;
|
|
502
586
|
}),
|
|
503
587
|
};
|
|
504
588
|
}
|