@oddessentials/agent-guild 0.4.0 → 0.6.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 +13 -8
- package/bin/agent-guild-report.mjs +14 -1
- package/bin/agent-guild.mjs +85 -46
- package/config/providers.default.json +4 -0
- package/package.json +1 -1
- package/src/manager/changelog.mjs +195 -0
- package/src/manager/launch.mjs +64 -0
- package/src/manager/main.mjs +27 -4
- package/src/manager/news.mjs +5 -5
- package/src/manager/providers.mjs +11 -0
- package/src/manager/server.mjs +52 -16
- package/src/manager/session-history.mjs +496 -0
- package/src/manager/session-manager.mjs +4 -0
- package/src/manager/session.mjs +16 -0
- package/src/report/hooks.mjs +4 -0
- package/web/app.js +754 -56
- package/web/art/art.css +372 -159
- package/web/art/characters/anthropic/idle.avif +0 -0
- package/web/art/characters/anthropic/idle.webp +0 -0
- package/web/art/characters/anthropic/locked.avif +0 -0
- package/web/art/characters/anthropic/locked.webp +0 -0
- package/web/art/characters/anthropic/working.avif +0 -0
- package/web/art/characters/anthropic/working.webp +0 -0
- package/web/art/characters/google/idle.avif +0 -0
- package/web/art/characters/google/idle.webp +0 -0
- package/web/art/characters/google/locked.avif +0 -0
- package/web/art/characters/google/locked.webp +0 -0
- package/web/art/characters/google/working.avif +0 -0
- package/web/art/characters/google/working.webp +0 -0
- package/web/art/characters/openai/idle.avif +0 -0
- package/web/art/characters/openai/idle.webp +0 -0
- package/web/art/characters/openai/locked.avif +0 -0
- package/web/art/characters/openai/locked.webp +0 -0
- package/web/art/characters/openai/working.avif +0 -0
- package/web/art/characters/openai/working.webp +0 -0
- package/web/art/characters/shell/idle.avif +0 -0
- package/web/art/characters/shell/idle.webp +0 -0
- package/web/art/characters/shell/locked.avif +0 -0
- package/web/art/characters/shell/locked.webp +0 -0
- package/web/art/characters/shell/working.avif +0 -0
- package/web/art/characters/shell/working.webp +0 -0
- package/web/art/characters/xai/idle.avif +0 -0
- package/web/art/characters/xai/idle.webp +0 -0
- package/web/art/characters/xai/locked.avif +0 -0
- package/web/art/characters/xai/locked.webp +0 -0
- package/web/art/characters/xai/working.avif +0 -0
- package/web/art/characters/xai/working.webp +0 -0
- package/web/art/icons/anthropic.avif +0 -0
- package/web/art/icons/anthropic.webp +0 -0
- package/web/art/icons/google.avif +0 -0
- package/web/art/icons/google.webp +0 -0
- package/web/art/icons/openai.avif +0 -0
- package/web/art/icons/openai.webp +0 -0
- package/web/art/icons/shell.avif +0 -0
- package/web/art/icons/shell.webp +0 -0
- package/web/art/icons/xai.avif +0 -0
- package/web/art/icons/xai.webp +0 -0
- package/web/art/page-light.avif +0 -0
- package/web/art/page-light.webp +0 -0
- package/web/art/page.avif +0 -0
- package/web/art/page.webp +0 -0
- package/web/art/ui/empty-state.avif +0 -0
- package/web/art/ui/empty-state.webp +0 -0
- package/web/art/ui/favicon.png +0 -0
- package/web/art/ui/frame-ornate.png +0 -0
- package/web/art/ui/guild-crest.png +0 -0
- package/web/fonts/OFL.txt +93 -0
- package/web/fonts/cinzel.woff2 +0 -0
- package/web/index.html +82 -14
- package/web/styles.css +208 -68
- package/web/art/characters/anthropic/idle.png +0 -0
- package/web/art/characters/anthropic/locked.png +0 -0
- package/web/art/characters/anthropic/working.png +0 -0
- package/web/art/characters/google/idle.png +0 -0
- package/web/art/characters/google/locked.png +0 -0
- package/web/art/characters/google/working.png +0 -0
- package/web/art/characters/openai/idle.png +0 -0
- package/web/art/characters/openai/locked.png +0 -0
- package/web/art/characters/openai/working.png +0 -0
- package/web/art/characters/shell/idle.png +0 -0
- package/web/art/characters/shell/locked.png +0 -0
- package/web/art/characters/shell/working.png +0 -0
- package/web/art/characters/xai/idle.png +0 -0
- package/web/art/characters/xai/locked.png +0 -0
- package/web/art/characters/xai/working.png +0 -0
- package/web/art/icons/anthropic.png +0 -0
- package/web/art/icons/google.png +0 -0
- package/web/art/icons/openai.png +0 -0
- package/web/art/icons/shell.png +0 -0
- package/web/art/icons/xai.png +0 -0
- package/web/art/page.png +0 -0
package/src/manager/main.mjs
CHANGED
|
@@ -7,12 +7,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { ProviderRegistry } from './providers.mjs';
|
|
8
8
|
import { SessionManager } from './session-manager.mjs';
|
|
9
9
|
import { UsageMonitor } from './usage.mjs';
|
|
10
|
+
import { SessionHistory } from './session-history.mjs';
|
|
10
11
|
import { ModelStats } from './model-stats.mjs';
|
|
11
12
|
import { NewsFeed } from './news.mjs';
|
|
13
|
+
import { Changelog } from './changelog.mjs';
|
|
12
14
|
import { createManagerServer } from './server.mjs';
|
|
13
15
|
import { SelfUpdate } from './self-update.mjs';
|
|
14
16
|
import { resolveBaseEnv, pathReader } from './shell-env.mjs';
|
|
15
17
|
import { writeReportShims } from './report-shims.mjs';
|
|
18
|
+
import { launcherPath, spawnManager } from './launch.mjs';
|
|
16
19
|
import {
|
|
17
20
|
DEFAULT_HOST,
|
|
18
21
|
PACKAGE_FILE,
|
|
@@ -59,8 +62,10 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
|
|
|
59
62
|
const selfUpdate = new SelfUpdate({ pkg: PACKAGE_NAME, version, packageFile, registry });
|
|
60
63
|
const manager = new SessionManager({ registry, baseEnv, getApiUrl: () => api.url, sessionDefaults, shimDir, selfUpdate });
|
|
61
64
|
const usage = new UsageMonitor({ registry, env: baseEnv });
|
|
65
|
+
const history = new SessionHistory({ registry, env: baseEnv });
|
|
62
66
|
const modelStats = new ModelStats({ registry });
|
|
63
67
|
const news = new NewsFeed({ registry });
|
|
68
|
+
const changelog = new Changelog({ latest: () => selfUpdate.latest });
|
|
64
69
|
let closing = null;
|
|
65
70
|
|
|
66
71
|
const refreshVersions = () => {
|
|
@@ -70,9 +75,14 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
|
|
|
70
75
|
const versionTimer = setInterval(refreshVersions, VERSION_REFRESH_MS);
|
|
71
76
|
versionTimer.unref();
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
/**
|
|
79
|
+
* End every session and close the API. With `restart`, a new manager is
|
|
80
|
+
* then started from the package on disk, so it comes up on the version
|
|
81
|
+
* an upgrade installed; clients reconnect to it by themselves.
|
|
82
|
+
*/
|
|
83
|
+
const shutdown = (reason = 'shutdown', { restart = false } = {}) => {
|
|
74
84
|
if (closing) return closing;
|
|
75
|
-
console.log(`[manager] stopping (${reason}); ending ${manager.sessions.size} session(s)`);
|
|
85
|
+
console.log(`[manager] ${restart ? 'restarting' : 'stopping'} (${reason}); ending ${manager.sessions.size} session(s)`);
|
|
76
86
|
clearInterval(versionTimer);
|
|
77
87
|
removeRuntimeFile();
|
|
78
88
|
// Sessions end before the API closes, and the last event says whether
|
|
@@ -80,7 +90,17 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
|
|
|
80
90
|
// from a timeout. The manager refuses new sessions meanwhile.
|
|
81
91
|
closing = manager.shutdown().then(({ remaining }) => {
|
|
82
92
|
if (remaining > 0) console.warn(`[manager] ${remaining} session process(es) did not confirm exiting in time`);
|
|
83
|
-
return api.close({ notice: { type: 'manager.stopped', remaining } });
|
|
93
|
+
return api.close({ notice: { type: 'manager.stopped', remaining, restart } });
|
|
94
|
+
}).then(() => {
|
|
95
|
+
// Only once the port is released: the successor listens on the same one,
|
|
96
|
+
// the bound one rather than a configured 0, so clients find it again.
|
|
97
|
+
if (!restart) return;
|
|
98
|
+
try {
|
|
99
|
+
const child = spawnManager({ note: 'restarting manager', env: { ...process.env, AGENT_GUILD_PORT: String(api.port) } });
|
|
100
|
+
console.log(`[manager] started the next manager (pid ${child.pid})`);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(`[manager] could not start the next manager: ${err.message}`);
|
|
103
|
+
}
|
|
84
104
|
});
|
|
85
105
|
return closing;
|
|
86
106
|
};
|
|
@@ -92,8 +112,10 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
|
|
|
92
112
|
manager,
|
|
93
113
|
registry,
|
|
94
114
|
usage,
|
|
115
|
+
history,
|
|
95
116
|
modelStats,
|
|
96
117
|
news,
|
|
118
|
+
changelog,
|
|
97
119
|
token,
|
|
98
120
|
host,
|
|
99
121
|
port,
|
|
@@ -101,7 +123,8 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
|
|
|
101
123
|
version,
|
|
102
124
|
selfUpdate,
|
|
103
125
|
extraOrigins,
|
|
104
|
-
|
|
126
|
+
launcher: launcherPath(),
|
|
127
|
+
onShutdownRequest: ({ restart = false } = {}) => shutdown('requested via API', { restart }).then(() => process.exit(0)),
|
|
105
128
|
});
|
|
106
129
|
|
|
107
130
|
try {
|
package/src/manager/news.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const LIMITS = { news: 20, releases: 5, research: 10 };
|
|
|
14
14
|
const TITLE_MAX = 300;
|
|
15
15
|
const SUMMARY_MAX = 240;
|
|
16
16
|
const RAW_MAX = 8000;
|
|
17
|
-
const USER_AGENT = `agent-guild/${VERSION} (+https://github.com/oddessentials/agent-guild)`;
|
|
17
|
+
export const USER_AGENT = `agent-guild/${VERSION} (+https://github.com/oddessentials/agent-guild)`;
|
|
18
18
|
const FEED_ACCEPT = 'application/rss+xml, application/atom+xml, application/xml;q=0.9, text/xml;q=0.8, */*;q=0.5';
|
|
19
19
|
|
|
20
20
|
const latestRelease = (id, name, repo, provider) => ({
|
|
@@ -64,7 +64,7 @@ const NAMED = {
|
|
|
64
64
|
uacute: 'ú', uuml: 'ü', ccedil: 'ç', ntilde: 'ñ', szlig: 'ß', Auml: 'Ä', Ouml: 'Ö', Uuml: 'Ü',
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
class FeedError extends Error {}
|
|
67
|
+
export class FeedError extends Error {}
|
|
68
68
|
|
|
69
69
|
const fields = new Map();
|
|
70
70
|
const attrs = new Map();
|
|
@@ -329,7 +329,7 @@ function merge(previous, fresh, limit, now) {
|
|
|
329
329
|
return [...byKey.values()].sort((a, b) => b.time - a.time).slice(0, limit);
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
async function readBody(res, maxBytes) {
|
|
332
|
+
export async function readBody(res, maxBytes) {
|
|
333
333
|
const tooLarge = () => new FeedError(`sent more than ${maxBytes / 1024 / 1024} MB`);
|
|
334
334
|
if (Number(res.headers.get('content-length')) > maxBytes) throw tooLarge();
|
|
335
335
|
const chunks = [];
|
|
@@ -349,12 +349,12 @@ async function readBody(res, maxBytes) {
|
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
function refusal(res) {
|
|
352
|
+
export function refusal(res) {
|
|
353
353
|
if ((res.status === 403 || res.status === 429) && res.headers.get('x-ratelimit-remaining') === '0') return 'GitHub API rate limit exceeded';
|
|
354
354
|
return `HTTP ${res.status}`;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
function failure(err, timeoutMs) {
|
|
357
|
+
export function failure(err, timeoutMs) {
|
|
358
358
|
if (err instanceof FeedError) return err.message;
|
|
359
359
|
if (err?.name === 'TimeoutError') return `did not answer within ${timeoutMs / 1000} seconds`;
|
|
360
360
|
if (err instanceof SyntaxError) return 'sent data that could not be read';
|
|
@@ -60,6 +60,15 @@ function normalizeUsage(usage) {
|
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/** "claude", "codex", "gemini", "grok", a { command, args } that prints past sessions as JSON, or null. */
|
|
64
|
+
function normalizeHistory(history) {
|
|
65
|
+
if (['claude', 'codex', 'gemini', 'grok'].includes(history)) return history;
|
|
66
|
+
if (history && typeof history === 'object' && typeof history.command === 'string' && history.command) {
|
|
67
|
+
return { command: history.command, args: Array.isArray(history.args) ? history.args.map(String) : [] };
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
function stringList(value) {
|
|
64
73
|
return Array.isArray(value) ? value.map(String).filter(Boolean) : [];
|
|
65
74
|
}
|
|
@@ -153,6 +162,7 @@ function normalize(raw, platform, warnings) {
|
|
|
153
162
|
package: merged.package ? String(merged.package) : null,
|
|
154
163
|
versionArgs: Array.isArray(merged.versionArgs) && merged.versionArgs.length ? merged.versionArgs.map(String) : null,
|
|
155
164
|
usage: normalizeUsage(merged.usage),
|
|
165
|
+
history: normalizeHistory(merged.history),
|
|
156
166
|
modelPattern: merged.modelPattern ? String(merged.modelPattern) : null,
|
|
157
167
|
args: Array.isArray(merged.args) ? merged.args.map(String) : [],
|
|
158
168
|
resumeArgs: Array.isArray(merged.resumeArgs) ? merged.resumeArgs.map(String) : [],
|
|
@@ -612,6 +622,7 @@ export class ProviderRegistry extends EventEmitter {
|
|
|
612
622
|
warnings: this.installWarnings(provider, installs),
|
|
613
623
|
npmNote: provider.npmNote,
|
|
614
624
|
usageSource: provider.usage === null ? null : typeof provider.usage === 'string' ? provider.usage : 'command',
|
|
625
|
+
historySource: provider.history === null ? null : typeof provider.history === 'string' ? provider.history : 'command',
|
|
615
626
|
accounts: provider.accounts.map(({ id, label }) => ({ id, label })),
|
|
616
627
|
modelPattern: provider.modelPattern,
|
|
617
628
|
color: provider.color,
|
package/src/manager/server.mjs
CHANGED
|
@@ -21,6 +21,9 @@ const MIME = {
|
|
|
21
21
|
'.css': 'text/css; charset=utf-8',
|
|
22
22
|
'.svg': 'image/svg+xml',
|
|
23
23
|
'.png': 'image/png',
|
|
24
|
+
'.avif': 'image/avif',
|
|
25
|
+
'.webp': 'image/webp',
|
|
26
|
+
'.woff2': 'font/woff2',
|
|
24
27
|
'.ico': 'image/x-icon',
|
|
25
28
|
'.json': 'application/json; charset=utf-8',
|
|
26
29
|
};
|
|
@@ -100,8 +103,10 @@ export function createManagerServer({
|
|
|
100
103
|
manager,
|
|
101
104
|
registry,
|
|
102
105
|
usage,
|
|
106
|
+
history,
|
|
103
107
|
modelStats,
|
|
104
108
|
news = null,
|
|
109
|
+
changelog = null,
|
|
105
110
|
token,
|
|
106
111
|
host = '127.0.0.1',
|
|
107
112
|
port = 0,
|
|
@@ -109,6 +114,9 @@ export function createManagerServer({
|
|
|
109
114
|
version = '0.0.0',
|
|
110
115
|
selfUpdate = null,
|
|
111
116
|
extraOrigins = [],
|
|
117
|
+
/** The double-click launcher file for this platform, or null when the package carries none. */
|
|
118
|
+
launcher = null,
|
|
119
|
+
/** @type {(opts: { restart: boolean }) => void} */
|
|
112
120
|
onShutdownRequest = () => {},
|
|
113
121
|
}) {
|
|
114
122
|
const upgradeInfo = () => (selfUpdate ? selfUpdate.describe() : null);
|
|
@@ -144,16 +152,27 @@ export function createManagerServer({
|
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
function serveFile(res, file, { cache = false } = {}) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
function serveFile(req, res, file, { cache = false } = {}) {
|
|
156
|
+
const notFound = () => sendJson(res, 404, { error: { code: 'not_found', message: 'not found' } });
|
|
157
|
+
fs.stat(file, (statErr, stat) => {
|
|
158
|
+
if (statErr || !stat.isFile()) return notFound();
|
|
159
|
+
const etag = `W/"${stat.size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`;
|
|
160
|
+
const headers = {
|
|
151
161
|
...SECURITY_HEADERS,
|
|
152
162
|
'Content-Type': MIME[path.extname(file).toLowerCase()] || 'application/octet-stream',
|
|
153
163
|
'Cache-Control': cache ? 'public, max-age=3600' : 'no-cache',
|
|
154
|
-
|
|
164
|
+
ETag: etag,
|
|
165
|
+
};
|
|
166
|
+
const known = String(req.headers['if-none-match'] || '').split(',').map((tag) => tag.trim());
|
|
167
|
+
if (known.includes(etag) || known.includes('*')) {
|
|
168
|
+
res.writeHead(304, headers);
|
|
169
|
+
return res.end();
|
|
170
|
+
}
|
|
171
|
+
fs.readFile(file, (err, data) => {
|
|
172
|
+
if (err) return notFound();
|
|
173
|
+
res.writeHead(200, { ...headers, 'Content-Length': data.length });
|
|
174
|
+
res.end(data);
|
|
155
175
|
});
|
|
156
|
-
res.end(data);
|
|
157
176
|
});
|
|
158
177
|
}
|
|
159
178
|
|
|
@@ -161,7 +180,7 @@ export function createManagerServer({
|
|
|
161
180
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
162
181
|
throw new HttpError(405, 'method not allowed', 'method_not_allowed');
|
|
163
182
|
}
|
|
164
|
-
if (vendor[pathname]) return serveFile(res, vendor[pathname], { cache: true });
|
|
183
|
+
if (vendor[pathname]) return serveFile(req, res, vendor[pathname], { cache: true });
|
|
165
184
|
let rel;
|
|
166
185
|
try {
|
|
167
186
|
rel = pathname === '/' ? 'index.html' : decodeURIComponent(pathname).replace(/^\/+/, '');
|
|
@@ -173,7 +192,7 @@ export function createManagerServer({
|
|
|
173
192
|
if (file !== webDir && !file.startsWith(webDir + path.sep)) {
|
|
174
193
|
throw new HttpError(404, 'not found', 'not_found');
|
|
175
194
|
}
|
|
176
|
-
return serveFile(res, file);
|
|
195
|
+
return serveFile(req, res, file);
|
|
177
196
|
}
|
|
178
197
|
|
|
179
198
|
async function handleApi(req, res, url) {
|
|
@@ -184,9 +203,10 @@ export function createManagerServer({
|
|
|
184
203
|
return sendJson(res, 200, { ok: true, name: 'agent-guild', version, pid: process.pid });
|
|
185
204
|
}
|
|
186
205
|
|
|
187
|
-
// Agent and
|
|
188
|
-
// token that the manager injects into each tool's
|
|
189
|
-
|
|
206
|
+
// Agent, model and tool-session reports may authenticate with the
|
|
207
|
+
// per-session report token that the manager injects into each tool's
|
|
208
|
+
// environment.
|
|
209
|
+
const reportMatch = route.match(/^\/sessions\/([a-f0-9]+)\/(agents|model|tool-session)$/);
|
|
190
210
|
if (reportMatch && method === 'POST') {
|
|
191
211
|
const [, id, kind] = reportMatch;
|
|
192
212
|
const body = await readJsonBody(req);
|
|
@@ -195,7 +215,8 @@ export function createManagerServer({
|
|
|
195
215
|
reportToken: req.headers['x-agent-guild-report-token'],
|
|
196
216
|
};
|
|
197
217
|
if (kind === 'agents') return sendJson(res, 200, { agent: manager.reportAgent(id, body, auth) });
|
|
198
|
-
return sendJson(res, 200, { model: manager.reportModel(id, body, auth) });
|
|
218
|
+
if (kind === 'model') return sendJson(res, 200, { model: manager.reportModel(id, body, auth) });
|
|
219
|
+
return sendJson(res, 200, { toolSessionId: manager.reportToolSession(id, body, auth) });
|
|
199
220
|
}
|
|
200
221
|
|
|
201
222
|
requireAuth(req, url);
|
|
@@ -210,6 +231,7 @@ export function createManagerServer({
|
|
|
210
231
|
startedAt,
|
|
211
232
|
warnings: registry.warnings,
|
|
212
233
|
upgrade: upgradeInfo(),
|
|
234
|
+
launcher,
|
|
213
235
|
});
|
|
214
236
|
}
|
|
215
237
|
if (route === '/upgrade' && method === 'POST') {
|
|
@@ -234,6 +256,17 @@ export function createManagerServer({
|
|
|
234
256
|
if (route === '/news' && method === 'GET' && news) {
|
|
235
257
|
return sendJson(res, 200, news.snapshot());
|
|
236
258
|
}
|
|
259
|
+
if (route === '/changelog' && method === 'GET' && changelog) {
|
|
260
|
+
return sendJson(res, 200, changelog.snapshot());
|
|
261
|
+
}
|
|
262
|
+
const historyMatch = route.match(/^\/providers\/([a-z0-9][a-z0-9_-]{0,31})\/history$/);
|
|
263
|
+
if (historyMatch && method === 'GET') {
|
|
264
|
+
const provider = registry.get(historyMatch[1]);
|
|
265
|
+
if (!provider) throw new HttpError(404, `unknown provider "${historyMatch[1]}"`, 'unknown_provider');
|
|
266
|
+
if (!provider.history) throw new HttpError(400, `${provider.tool} has no history source configured`, 'history_unsupported');
|
|
267
|
+
const account = registry.account(provider, url.searchParams.get('account'));
|
|
268
|
+
return sendJson(res, 200, { history: await history.list(provider, account, { limit: url.searchParams.get('limit') }) });
|
|
269
|
+
}
|
|
237
270
|
const installMatch = route.match(/^\/providers\/([a-z0-9][a-z0-9_-]{0,31})\/install$/);
|
|
238
271
|
if (installMatch && method === 'POST') {
|
|
239
272
|
const body = await readJsonBody(req);
|
|
@@ -258,14 +291,16 @@ export function createManagerServer({
|
|
|
258
291
|
err.running = running;
|
|
259
292
|
throw err;
|
|
260
293
|
}
|
|
294
|
+
// With `restart`, a new manager is started once this one has closed.
|
|
295
|
+
const restart = body.restart === true;
|
|
261
296
|
// Refuse new sessions from this moment, before the shutdown itself
|
|
262
297
|
// runs: a session accepted in between would be ended without warning.
|
|
263
298
|
manager.closing = true;
|
|
264
|
-
sendJson(res, 202, { ok: true, running });
|
|
299
|
+
sendJson(res, 202, { ok: true, running, restart });
|
|
265
300
|
// Tell every client first, so a second page shows "stopped" rather
|
|
266
301
|
// than "not reachable" when its socket drops.
|
|
267
|
-
broadcast({ type: 'manager.stopping', running });
|
|
268
|
-
setImmediate(onShutdownRequest);
|
|
302
|
+
broadcast({ type: 'manager.stopping', running, restart });
|
|
303
|
+
setImmediate(() => onShutdownRequest({ restart }));
|
|
269
304
|
return undefined;
|
|
270
305
|
}
|
|
271
306
|
|
|
@@ -347,10 +382,11 @@ export function createManagerServer({
|
|
|
347
382
|
registry.on('updated', () => broadcast({ type: 'providers.updated', providers: registry.list() }));
|
|
348
383
|
selfUpdate?.on('updated', () => broadcast({ type: 'manager.upgrade', upgrade: upgradeInfo() }));
|
|
349
384
|
news?.on('updated', () => broadcast({ type: 'news.updated' }));
|
|
385
|
+
changelog?.on('updated', () => broadcast({ type: 'changelog.updated' }));
|
|
350
386
|
|
|
351
387
|
function handleEvents(ws) {
|
|
352
388
|
eventClients.add(ws);
|
|
353
|
-
safeSend(ws, { type: 'hello', version, upgrade: upgradeInfo(), sessions: manager.list() });
|
|
389
|
+
safeSend(ws, { type: 'hello', version, pid: process.pid, launcher, upgrade: upgradeInfo(), sessions: manager.list() });
|
|
354
390
|
ws.on('close', () => eventClients.delete(ws));
|
|
355
391
|
ws.on('message', () => { /* events socket is server -> client only */ });
|
|
356
392
|
}
|