@oddessentials/agent-guild 0.5.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 +1 -1
- package/package.json +1 -1
- package/src/manager/changelog.mjs +195 -0
- package/src/manager/main.mjs +3 -0
- package/src/manager/news.mjs +5 -5
- package/src/manager/server.mjs +27 -8
- package/web/app.js +353 -8
- 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 +35 -11
- package/web/styles.css +168 -71
- 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/README.md
CHANGED
|
@@ -44,7 +44,7 @@ coding tool, and see at a glance which sessions and agents are working.
|
|
|
44
44
|
can watch. Sessions keep running; once they are done, **Restart to use
|
|
45
45
|
vX.Y.Z** in the top bar switches to the new version.
|
|
46
46
|
* **Light or dark.** The page follows your system theme and the top-bar
|
|
47
|
-
toggle switches it.
|
|
47
|
+
toggle switches it.
|
|
48
48
|
|
|
49
49
|
## Requirements
|
|
50
50
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oddessentials/agent-guild",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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": {
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { compareVersions } from './versions.mjs';
|
|
3
|
+
import { FeedError, USER_AGENT, decodeEntities, failure, readBody, refusal, webUrl } from './news.mjs';
|
|
4
|
+
|
|
5
|
+
const REPOSITORY = 'oddessentials/agent-guild';
|
|
6
|
+
const RELEASES_URL = `https://github.com/${REPOSITORY}/releases`;
|
|
7
|
+
const API_URL = `https://api.github.com/repos/${REPOSITORY}/releases?per_page=30`;
|
|
8
|
+
const TTL_MS = 60 * 60 * 1000;
|
|
9
|
+
const RETRY_MS = 10 * 60 * 1000;
|
|
10
|
+
const MISSING_MS = 2 * 60 * 1000;
|
|
11
|
+
const FETCH_TIMEOUT_MS = 20000;
|
|
12
|
+
const MAX_BYTES = 5 * 1024 * 1024;
|
|
13
|
+
const MAX_CHANGES = 100;
|
|
14
|
+
const MAX_NOTES = 20000;
|
|
15
|
+
const MAX_LINE = 1000;
|
|
16
|
+
const RELEASE_TAG = /^v?(\d+\.\d+\.\d+)$/;
|
|
17
|
+
const VERSION_HEADING = /^v?\d+\.\d+\.\d+(?![\d.])/;
|
|
18
|
+
const INSTALL_STEPS = /^install or update$/i;
|
|
19
|
+
const COMMIT_LINK = /\s*\(\[[0-9a-f]{7,40}\]\([^)\s]*\)\)/gi;
|
|
20
|
+
const INLINE = /\*\*(.+?)\*\*|`([^`]+)`|!\[[^\]]*\]\([^)]*\)|<(https?:\/\/[^>\s]+)>|\[([^\]]+)\]\(([^)\s]+)\)|<\/?[a-z][^>]*>/gi;
|
|
21
|
+
|
|
22
|
+
const iso = (time) => (time ? new Date(time).toISOString() : null);
|
|
23
|
+
|
|
24
|
+
function textRun(text, strong) {
|
|
25
|
+
return strong ? { text: decodeEntities(text), strong: true } : { text: decodeEntities(text) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function inline(source, strong = false) {
|
|
29
|
+
const runs = [];
|
|
30
|
+
let at = 0;
|
|
31
|
+
for (const match of source.matchAll(INLINE)) {
|
|
32
|
+
if (match.index > at) runs.push(textRun(source.slice(at, match.index), strong));
|
|
33
|
+
const [, bold, code, autolink, label, href] = match;
|
|
34
|
+
if (bold !== undefined) {
|
|
35
|
+
runs.push(...inline(bold, true));
|
|
36
|
+
} else if (code !== undefined) {
|
|
37
|
+
runs.push({ ...(strong && { strong: true }), text: code, code: true });
|
|
38
|
+
} else if (autolink !== undefined || label !== undefined) {
|
|
39
|
+
const url = webUrl(autolink ?? href);
|
|
40
|
+
runs.push({ ...textRun(label ?? autolink, strong), ...(url && { url }) });
|
|
41
|
+
}
|
|
42
|
+
at = match.index + match[0].length;
|
|
43
|
+
}
|
|
44
|
+
if (at < source.length) runs.push(textRun(source.slice(at), strong));
|
|
45
|
+
return runs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const sameStyle = (a, b) => !a.url && !b.url && Boolean(a.code) === Boolean(b.code) && Boolean(a.strong) === Boolean(b.strong);
|
|
49
|
+
|
|
50
|
+
function tidy(runs) {
|
|
51
|
+
const tidied = [];
|
|
52
|
+
for (const run of runs) {
|
|
53
|
+
const text = run.text.replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ');
|
|
54
|
+
if (!text) continue;
|
|
55
|
+
const last = tidied.at(-1);
|
|
56
|
+
if (last && sameStyle(last, run)) last.text += text;
|
|
57
|
+
else tidied.push({ ...run, text });
|
|
58
|
+
}
|
|
59
|
+
if (tidied.length) {
|
|
60
|
+
tidied[0].text = tidied[0].text.trimStart();
|
|
61
|
+
tidied.at(-1).text = tidied.at(-1).text.trimEnd();
|
|
62
|
+
}
|
|
63
|
+
return tidied.filter((run) => run.text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function plain(source) {
|
|
67
|
+
return tidy(inline(source)).map((run) => run.text).join('');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parseNotes(markdown) {
|
|
71
|
+
const sections = [];
|
|
72
|
+
let section = null;
|
|
73
|
+
let change = null;
|
|
74
|
+
let skipping = false;
|
|
75
|
+
let fenced = false;
|
|
76
|
+
let count = 0;
|
|
77
|
+
for (const whole of String(markdown ?? '').slice(0, MAX_NOTES).replace(/<!--[\s\S]*?(?:-->|$)/g, '').split(/\r?\n/)) {
|
|
78
|
+
const line = whole.slice(0, MAX_LINE);
|
|
79
|
+
if (/^\s*(?:```|~~~)/.test(line)) {
|
|
80
|
+
fenced = !fenced;
|
|
81
|
+
change = null;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (fenced) continue;
|
|
85
|
+
const heading = /^\s{0,3}#{1,6}\s+(.*?)(?:\s+#+)?\s*$/.exec(line);
|
|
86
|
+
if (heading) {
|
|
87
|
+
const title = plain(heading[1]);
|
|
88
|
+
change = null;
|
|
89
|
+
section = null;
|
|
90
|
+
skipping = INSTALL_STEPS.test(title);
|
|
91
|
+
if (title && !skipping && !VERSION_HEADING.test(title)) sections.push(section = { title, changes: [] });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (skipping) continue;
|
|
95
|
+
if (/^\s*([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
|
|
96
|
+
change = null;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const bullet = /^\s*(?:[-*+]|\d{1,9}[.)])\s+(.*)$/.exec(line);
|
|
100
|
+
const body = (bullet ? bullet[1] : line.replace(/^\s*(?:>\s?)+/, '')).trim();
|
|
101
|
+
if (!body) {
|
|
102
|
+
change = null;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const runs = tidy(inline(body.replace(COMMIT_LINK, '')));
|
|
106
|
+
if (runs.length === 0) continue;
|
|
107
|
+
if (!bullet && change) {
|
|
108
|
+
change.splice(0, change.length, ...tidy([...change, { text: ' ' }, ...runs]));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (count === MAX_CHANGES) break;
|
|
112
|
+
if (!section) sections.push(section = { title: null, changes: [] });
|
|
113
|
+
section.changes.push(change = runs);
|
|
114
|
+
count++;
|
|
115
|
+
}
|
|
116
|
+
return sections.filter((s) => s.changes.length > 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function parseReleases(json) {
|
|
120
|
+
const list = JSON.parse(json);
|
|
121
|
+
if (!Array.isArray(list)) throw new FeedError('sent no releases');
|
|
122
|
+
const releases = new Map();
|
|
123
|
+
for (const release of list) {
|
|
124
|
+
if (!release || typeof release !== 'object' || release.draft || release.prerelease) continue;
|
|
125
|
+
const tag = String(release.tag_name ?? '');
|
|
126
|
+
const version = RELEASE_TAG.exec(tag)?.[1];
|
|
127
|
+
if (!version || releases.has(version)) continue;
|
|
128
|
+
const published = Date.parse(release.published_at);
|
|
129
|
+
releases.set(version, {
|
|
130
|
+
version,
|
|
131
|
+
url: webUrl(String(release.html_url ?? '')) ?? `${RELEASES_URL}/tag/${encodeURIComponent(tag)}`,
|
|
132
|
+
publishedAt: Number.isFinite(published) ? new Date(published).toISOString() : null,
|
|
133
|
+
sections: parseNotes(typeof release.body === 'string' ? release.body : ''),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return [...releases.values()].sort((a, b) => compareVersions(b.version, a.version));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class Changelog extends EventEmitter {
|
|
140
|
+
constructor({
|
|
141
|
+
latest = () => null, fetchImpl = fetch, url = API_URL,
|
|
142
|
+
ttlMs = TTL_MS, retryMs = RETRY_MS, missingMs = MISSING_MS, timeoutMs = FETCH_TIMEOUT_MS, maxBytes = MAX_BYTES,
|
|
143
|
+
} = {}) {
|
|
144
|
+
super();
|
|
145
|
+
this.latest = latest;
|
|
146
|
+
this.fetchImpl = fetchImpl;
|
|
147
|
+
this.url = url;
|
|
148
|
+
this.ttlMs = ttlMs;
|
|
149
|
+
this.retryMs = retryMs;
|
|
150
|
+
this.missingMs = missingMs;
|
|
151
|
+
this.timeoutMs = timeoutMs;
|
|
152
|
+
this.maxBytes = maxBytes;
|
|
153
|
+
this.releases = [];
|
|
154
|
+
this.etag = null;
|
|
155
|
+
this.checkedAt = 0;
|
|
156
|
+
this.okAt = 0;
|
|
157
|
+
this.error = null;
|
|
158
|
+
this.refreshing = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
snapshot() {
|
|
162
|
+
this._refreshDue();
|
|
163
|
+
return { refreshing: Boolean(this.refreshing), okAt: iso(this.okAt), error: this.error, releases: this.releases };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_refreshDue() {
|
|
167
|
+
if (this.refreshing) return;
|
|
168
|
+
const latest = this.latest();
|
|
169
|
+
const missing = Boolean(latest) && !this.releases.some((release) => release.version === latest);
|
|
170
|
+
const wait = this.error ? this.retryMs : missing ? this.missingMs : this.ttlMs;
|
|
171
|
+
if (Date.now() - this.checkedAt < wait) return;
|
|
172
|
+
this.refreshing = this._refresh().finally(() => {
|
|
173
|
+
this.refreshing = null;
|
|
174
|
+
this.emit('updated');
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async _refresh() {
|
|
179
|
+
this.checkedAt = Date.now();
|
|
180
|
+
try {
|
|
181
|
+
const headers = { 'User-Agent': USER_AGENT, Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' };
|
|
182
|
+
if (this.etag) headers['If-None-Match'] = this.etag;
|
|
183
|
+
const res = await this.fetchImpl(this.url, { headers, signal: AbortSignal.timeout(this.timeoutMs) });
|
|
184
|
+
if (res.status !== 304) {
|
|
185
|
+
if (!res.ok) throw new FeedError(refusal(res));
|
|
186
|
+
this.releases = parseReleases(await readBody(res, this.maxBytes));
|
|
187
|
+
this.etag = res.headers.get('etag');
|
|
188
|
+
}
|
|
189
|
+
this.error = null;
|
|
190
|
+
this.okAt = Date.now();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
this.error = failure(err, this.timeoutMs);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/manager/main.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { UsageMonitor } from './usage.mjs';
|
|
|
10
10
|
import { SessionHistory } from './session-history.mjs';
|
|
11
11
|
import { ModelStats } from './model-stats.mjs';
|
|
12
12
|
import { NewsFeed } from './news.mjs';
|
|
13
|
+
import { Changelog } from './changelog.mjs';
|
|
13
14
|
import { createManagerServer } from './server.mjs';
|
|
14
15
|
import { SelfUpdate } from './self-update.mjs';
|
|
15
16
|
import { resolveBaseEnv, pathReader } from './shell-env.mjs';
|
|
@@ -64,6 +65,7 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
|
|
|
64
65
|
const history = new SessionHistory({ registry, env: baseEnv });
|
|
65
66
|
const modelStats = new ModelStats({ registry });
|
|
66
67
|
const news = new NewsFeed({ registry });
|
|
68
|
+
const changelog = new Changelog({ latest: () => selfUpdate.latest });
|
|
67
69
|
let closing = null;
|
|
68
70
|
|
|
69
71
|
const refreshVersions = () => {
|
|
@@ -113,6 +115,7 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
|
|
|
113
115
|
history,
|
|
114
116
|
modelStats,
|
|
115
117
|
news,
|
|
118
|
+
changelog,
|
|
116
119
|
token,
|
|
117
120
|
host,
|
|
118
121
|
port,
|
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';
|
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
|
};
|
|
@@ -103,6 +106,7 @@ export function createManagerServer({
|
|
|
103
106
|
history,
|
|
104
107
|
modelStats,
|
|
105
108
|
news = null,
|
|
109
|
+
changelog = null,
|
|
106
110
|
token,
|
|
107
111
|
host = '127.0.0.1',
|
|
108
112
|
port = 0,
|
|
@@ -148,16 +152,27 @@ export function createManagerServer({
|
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
|
|
151
|
-
function serveFile(res, file, { cache = false } = {}) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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 = {
|
|
155
161
|
...SECURITY_HEADERS,
|
|
156
162
|
'Content-Type': MIME[path.extname(file).toLowerCase()] || 'application/octet-stream',
|
|
157
163
|
'Cache-Control': cache ? 'public, max-age=3600' : 'no-cache',
|
|
158
|
-
|
|
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);
|
|
159
175
|
});
|
|
160
|
-
res.end(data);
|
|
161
176
|
});
|
|
162
177
|
}
|
|
163
178
|
|
|
@@ -165,7 +180,7 @@ export function createManagerServer({
|
|
|
165
180
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
166
181
|
throw new HttpError(405, 'method not allowed', 'method_not_allowed');
|
|
167
182
|
}
|
|
168
|
-
if (vendor[pathname]) return serveFile(res, vendor[pathname], { cache: true });
|
|
183
|
+
if (vendor[pathname]) return serveFile(req, res, vendor[pathname], { cache: true });
|
|
169
184
|
let rel;
|
|
170
185
|
try {
|
|
171
186
|
rel = pathname === '/' ? 'index.html' : decodeURIComponent(pathname).replace(/^\/+/, '');
|
|
@@ -177,7 +192,7 @@ export function createManagerServer({
|
|
|
177
192
|
if (file !== webDir && !file.startsWith(webDir + path.sep)) {
|
|
178
193
|
throw new HttpError(404, 'not found', 'not_found');
|
|
179
194
|
}
|
|
180
|
-
return serveFile(res, file);
|
|
195
|
+
return serveFile(req, res, file);
|
|
181
196
|
}
|
|
182
197
|
|
|
183
198
|
async function handleApi(req, res, url) {
|
|
@@ -241,6 +256,9 @@ export function createManagerServer({
|
|
|
241
256
|
if (route === '/news' && method === 'GET' && news) {
|
|
242
257
|
return sendJson(res, 200, news.snapshot());
|
|
243
258
|
}
|
|
259
|
+
if (route === '/changelog' && method === 'GET' && changelog) {
|
|
260
|
+
return sendJson(res, 200, changelog.snapshot());
|
|
261
|
+
}
|
|
244
262
|
const historyMatch = route.match(/^\/providers\/([a-z0-9][a-z0-9_-]{0,31})\/history$/);
|
|
245
263
|
if (historyMatch && method === 'GET') {
|
|
246
264
|
const provider = registry.get(historyMatch[1]);
|
|
@@ -364,6 +382,7 @@ export function createManagerServer({
|
|
|
364
382
|
registry.on('updated', () => broadcast({ type: 'providers.updated', providers: registry.list() }));
|
|
365
383
|
selfUpdate?.on('updated', () => broadcast({ type: 'manager.upgrade', upgrade: upgradeInfo() }));
|
|
366
384
|
news?.on('updated', () => broadcast({ type: 'news.updated' }));
|
|
385
|
+
changelog?.on('updated', () => broadcast({ type: 'changelog.updated' }));
|
|
367
386
|
|
|
368
387
|
function handleEvents(ws) {
|
|
369
388
|
eventClients.add(ws);
|