compounding-loop 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +93 -3
- package/bin/loop.js +2 -0
- package/dist/cli/dashboard.js +52 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/gh.js +29 -0
- package/dist/cli/gh.js.map +1 -0
- package/dist/cli/index.js +144 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.js +72 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/loops.js +72 -0
- package/dist/cli/loops.js.map +1 -0
- package/dist/cli/new.js +54 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/run.js +81 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/core/blocked.js +42 -0
- package/dist/core/blocked.js.map +1 -0
- package/dist/core/brief.js +91 -0
- package/dist/core/brief.js.map +1 -0
- package/dist/core/gh.js +2 -0
- package/dist/core/gh.js.map +1 -0
- package/dist/core/init.js +59 -0
- package/dist/core/init.js.map +1 -0
- package/dist/core/repo.js +81 -0
- package/dist/core/repo.js.map +1 -0
- package/dist/core/rounds.js +50 -0
- package/dist/core/rounds.js.map +1 -0
- package/dist/core/runPrompt.js +31 -0
- package/dist/core/runPrompt.js.map +1 -0
- package/dist/core/status.js +21 -0
- package/dist/core/status.js.map +1 -0
- package/dist/core/table.js +24 -0
- package/dist/core/table.js.map +1 -0
- package/dist/core/template.js +29 -0
- package/dist/core/template.js.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/server/data.js +36 -0
- package/dist/server/data.js.map +1 -0
- package/dist/server/events.js +105 -0
- package/dist/server/events.js.map +1 -0
- package/dist/server/five-sites.json +3071 -0
- package/dist/server/health.js +133 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.js +134 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/loops.js +142 -0
- package/dist/server/loops.js.map +1 -0
- package/dist/server/settings.js +25 -0
- package/dist/server/settings.js.map +1 -0
- package/dist/server/static-export.js +28 -0
- package/dist/server/static-export.js.map +1 -0
- package/dist/server/static.js +80 -0
- package/dist/server/static.js.map +1 -0
- package/dist/web/assets/index-BDdtnG33.js +49 -0
- package/dist/web/assets/index-D3-N8DN4.css +2 -0
- package/dist/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.svg +1 -0
- package/method/COMPOUNDING_LOOP.md +569 -0
- package/method/example/K-0001.md +16 -0
- package/method/knowledge-index.md +5 -0
- package/method/operating-card.md +40 -0
- package/method/retro.md +10 -0
- package/method/reviewer-prompt.md +13 -0
- package/method/stuck-protocol.md +12 -0
- package/method/task-template.md +41 -0
- package/package.json +71 -4
- package/plugin/.claude-plugin/plugin.json +7 -0
- package/plugin/commands/handoff.md +10 -0
- package/plugin/commands/loop-init.md +13 -0
- package/plugin/commands/retro.md +11 -0
- package/plugin/commands/stuck.md +10 -0
- package/runners/github-actions/loop.yml +51 -0
- package/runners/local/README.md +8 -0
- package/runners/local/crontab.txt +6 -0
- package/runners/local/task-scheduler.xml +32 -0
- package/runners/routine/README.md +19 -0
- package/runners/routine/prompt.md +13 -0
- package/templates/chrome-extension/CLAUDE.md +18 -0
- package/templates/chrome-extension/IDEA.md +22 -0
- package/templates/chrome-extension/extension/background.js +5 -0
- package/templates/chrome-extension/extension/manifest.json +9 -0
- package/templates/chrome-extension/extension/popup.css +3 -0
- package/templates/chrome-extension/extension/popup.html +15 -0
- package/templates/chrome-extension/extension/popup.js +16 -0
- package/templates/chrome-extension/gitignore +6 -0
- package/templates/chrome-extension/package.json +12 -0
- package/templates/chrome-extension/playwright.config.js +22 -0
- package/templates/chrome-extension/scripts/check.mjs +51 -0
- package/templates/chrome-extension/scripts/test.mjs +59 -0
- package/templates/chrome-extension/tests/popup.spec.js +37 -0
- package/templates/static-site/.ai/index.md +5 -0
- package/templates/static-site/.ai/log.md +3 -0
- package/templates/static-site/.ai/loop-prompt.md +10 -0
- package/templates/static-site/.ai/task.md +27 -0
- package/templates/static-site/.gitattributes +1 -0
- package/templates/static-site/CLAUDE.md +113 -0
- package/templates/static-site/IDEA.md +36 -0
- package/templates/static-site/gitignore +6 -0
- package/templates/static-site/netlify.toml +14 -0
- package/templates/static-site/package.json +14 -0
- package/templates/static-site/playwright.config.js +27 -0
- package/templates/static-site/scripts/check.mjs +188 -0
- package/templates/static-site/scripts/serve.mjs +44 -0
- package/templates/static-site/scripts/test.mjs +59 -0
- package/templates/static-site/site/index.html +23 -0
- package/templates/static-site/site/robots.txt +4 -0
- package/templates/static-site/site/sitemap.xml +4 -0
- package/templates/static-site/tests/pages.spec.js +21 -0
- package/templates/static-site/vercel.json +19 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { LOCK_MINUTES } from '../core/status.js';
|
|
4
|
+
const minutesBetween = (later, earlier) => (later.getTime() - earlier.getTime()) / 60_000;
|
|
5
|
+
/** Every runner in `runners/` fires hourly; a loop on another schedule passes its own period. */
|
|
6
|
+
export const DEFAULT_CRON_PERIOD_MINUTES = 60;
|
|
7
|
+
/** A run shorter than this did not do a round of work; it was refused (usually a rate limit). */
|
|
8
|
+
export const SHORT_RUN_SECONDS = 5;
|
|
9
|
+
/** Consecutive short runs before the check fails; one is just a skipped round. */
|
|
10
|
+
export const SHORT_RUN_STREAK = 3;
|
|
11
|
+
/**
|
|
12
|
+
* Leak check: the site must not publish the repo root, because `.ai/`, CLAUDE.md and IDEA.md would be served.
|
|
13
|
+
* `netlifyToml` is the file's text, or undefined when the repo has none.
|
|
14
|
+
*/
|
|
15
|
+
export function leakCheck(netlifyToml) {
|
|
16
|
+
if (netlifyToml === undefined)
|
|
17
|
+
return { kind: 'leak', ok: true, reason: 'no netlify.toml: nothing is published by this repo', proof: 'netlify.toml not found' };
|
|
18
|
+
const publish = /^\s*publish\s*=\s*["']([^"']*)["']/m.exec(netlifyToml)?.[1];
|
|
19
|
+
// Only an explicit root counts: a missing publish may be set in the Netlify UI, so it is unknown, not a leak.
|
|
20
|
+
if (publish === undefined)
|
|
21
|
+
return { kind: 'leak', ok: true, reason: 'netlify.toml sets no publish folder (unknown)', proof: 'publish not set in netlify.toml' };
|
|
22
|
+
const root = ['', '.', './', '/'].includes(publish.trim());
|
|
23
|
+
return root
|
|
24
|
+
? { kind: 'leak', ok: false, reason: 'netlify.toml publishes the repo root, so .ai/ and CLAUDE.md are public', proof: `publish = ${JSON.stringify(publish)}` }
|
|
25
|
+
: { kind: 'leak', ok: true, reason: `only ${publish} is published`, proof: `publish = ${JSON.stringify(publish)}` };
|
|
26
|
+
}
|
|
27
|
+
/** Stale lock: a lock older than the lock window with no commit since it was taken means a session died holding it. */
|
|
28
|
+
export function staleLockCheck(facts, now) {
|
|
29
|
+
const { lockAt, lastCommitAt } = facts;
|
|
30
|
+
// Runs never delete the lock; a loop that stopped on purpose always leaves an old one behind.
|
|
31
|
+
if (facts.hasBlocked || facts.hasDone)
|
|
32
|
+
return { kind: 'stale-lock', ok: true, reason: 'loop is stopped on purpose', proof: facts.hasDone ? 'DONE.md exists' : 'BLOCKED.md exists' };
|
|
33
|
+
if (!lockAt)
|
|
34
|
+
return { kind: 'stale-lock', ok: true, reason: 'no lock held', proof: '.ai/session.lock not found' };
|
|
35
|
+
const age = minutesBetween(now, lockAt);
|
|
36
|
+
const committedSince = lastCommitAt !== undefined && lastCommitAt.getTime() > lockAt.getTime();
|
|
37
|
+
const stale = age >= LOCK_MINUTES && !committedSince;
|
|
38
|
+
return {
|
|
39
|
+
kind: 'stale-lock',
|
|
40
|
+
ok: !stale,
|
|
41
|
+
reason: stale ? `lock is ${Math.round(age)} min old and nothing was committed since` : 'lock is fresh or work followed it',
|
|
42
|
+
proof: `lock at ${lockAt.toISOString()}, window ${LOCK_MINUTES} min`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Parses `.ai/runs.jsonl` (one `{"startedAt","endedAt"}` object per line); bad lines are skipped. */
|
|
46
|
+
export function parseRunLog(text) {
|
|
47
|
+
const runs = [];
|
|
48
|
+
for (const line of text.split(/\r?\n/)) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = JSON.parse(line);
|
|
51
|
+
const startedAt = new Date(String(raw.startedAt));
|
|
52
|
+
const endedAt = new Date(String(raw.endedAt));
|
|
53
|
+
if (!Number.isNaN(startedAt.getTime()) && !Number.isNaN(endedAt.getTime()))
|
|
54
|
+
runs.push({ startedAt, endedAt });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// not JSON: ignore
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return runs.sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
|
|
61
|
+
}
|
|
62
|
+
/** Short runs: the last few runs all ended within seconds, the signature of a rate-limited account. */
|
|
63
|
+
export function shortRunsCheck(runs) {
|
|
64
|
+
if (runs.length === 0)
|
|
65
|
+
return { kind: 'short-runs', ok: true, reason: 'no run log yet', proof: '.ai/runs.jsonl not found or empty (`loop run` writes it)' };
|
|
66
|
+
const recent = runs.slice(-SHORT_RUN_STREAK);
|
|
67
|
+
const seconds = recent.map((r) => (r.endedAt.getTime() - r.startedAt.getTime()) / 1000);
|
|
68
|
+
const failing = recent.length === SHORT_RUN_STREAK && seconds.every((s) => s < SHORT_RUN_SECONDS);
|
|
69
|
+
return {
|
|
70
|
+
kind: 'short-runs',
|
|
71
|
+
ok: !failing,
|
|
72
|
+
reason: failing ? `the last ${SHORT_RUN_STREAK} runs each ended in under ${SHORT_RUN_SECONDS} s (rate limit?)` : 'recent runs did real work',
|
|
73
|
+
proof: `last ${recent.length} run durations: ${seconds.map((s) => `${s.toFixed(1)} s`).join(', ')}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Parses `.ai/last-test.json` (`{"passed": n, "failed": n}`); undefined when missing or malformed. */
|
|
77
|
+
export function parseTestRecord(text) {
|
|
78
|
+
try {
|
|
79
|
+
const raw = JSON.parse(text);
|
|
80
|
+
return typeof raw.passed === 'number' && typeof raw.failed === 'number' ? { passed: raw.passed, failed: raw.failed } : undefined;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function failingTestsCheck(record) {
|
|
87
|
+
if (!record)
|
|
88
|
+
return { kind: 'failing-tests', ok: true, reason: 'no test result recorded yet', proof: '.ai/last-test.json not found (the template `npm test` writes it; commit it)' };
|
|
89
|
+
return {
|
|
90
|
+
kind: 'failing-tests',
|
|
91
|
+
ok: record.failed === 0,
|
|
92
|
+
reason: record.failed === 0 ? 'all tests pass' : `${record.failed} tests fail`,
|
|
93
|
+
proof: `${record.passed} passed, ${record.failed} failed`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Runner silent: nothing committed for more than two cron periods while the loop is still meant to run. */
|
|
97
|
+
export function runnerSilentCheck(facts, now, periodMinutes = DEFAULT_CRON_PERIOD_MINUTES) {
|
|
98
|
+
const limit = periodMinutes * 2;
|
|
99
|
+
if (facts.hasBlocked || facts.hasDone)
|
|
100
|
+
return { kind: 'runner-silent', ok: true, reason: 'loop is stopped on purpose', proof: facts.hasDone ? 'DONE.md exists' : 'BLOCKED.md exists' };
|
|
101
|
+
const latest = Math.max(facts.lastCommitAt?.getTime() ?? 0, facts.lockAt?.getTime() ?? 0);
|
|
102
|
+
if (latest === 0)
|
|
103
|
+
return { kind: 'runner-silent', ok: true, reason: 'no activity yet', proof: 'no commits and no lock' };
|
|
104
|
+
const silent = minutesBetween(now, new Date(latest));
|
|
105
|
+
return {
|
|
106
|
+
kind: 'runner-silent',
|
|
107
|
+
ok: silent <= limit,
|
|
108
|
+
reason: silent <= limit ? 'runner is active' : `no commit or lock for ${Math.round(silent)} min (limit ${limit})`,
|
|
109
|
+
proof: `last activity ${new Date(latest).toISOString()}, period ${periodMinutes} min`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function readOptional(file) {
|
|
113
|
+
return readFile(file, 'utf8').catch(() => undefined);
|
|
114
|
+
}
|
|
115
|
+
/** Runs all five checks for one clone. */
|
|
116
|
+
export async function checkLoop(dir, facts, now, periodMinutes) {
|
|
117
|
+
const [toml, runLog, testText] = await Promise.all([
|
|
118
|
+
readOptional(path.join(dir, 'netlify.toml')),
|
|
119
|
+
readOptional(path.join(dir, '.ai', 'runs.jsonl')),
|
|
120
|
+
readOptional(path.join(dir, '.ai', 'last-test.json')),
|
|
121
|
+
]);
|
|
122
|
+
const loopId = path.basename(dir);
|
|
123
|
+
const checks = [
|
|
124
|
+
leakCheck(toml),
|
|
125
|
+
staleLockCheck(facts, now),
|
|
126
|
+
// A stopped loop (BLOCKED/DONE) keeps old run records; they say nothing about its health now.
|
|
127
|
+
shortRunsCheck(facts.hasBlocked || facts.hasDone || runLog === undefined ? [] : parseRunLog(runLog)),
|
|
128
|
+
failingTestsCheck(testText === undefined ? undefined : parseTestRecord(testText)),
|
|
129
|
+
runnerSilentCheck(facts, now, periodMinutes),
|
|
130
|
+
];
|
|
131
|
+
return checks.map((c) => ({ loopId, ...c }));
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { serve } from '@hono/node-server';
|
|
4
|
+
import { Hono } from 'hono';
|
|
5
|
+
import { streamSSE } from 'hono/streaming';
|
|
6
|
+
import { DEFAULT_WATCH, EventBus, watchProjects } from './events.js';
|
|
7
|
+
import { projectsSource } from './loops.js';
|
|
8
|
+
import { SECURITY_HEADERS, staticHandler } from './static.js';
|
|
9
|
+
import { loadDemoSnapshot, snapshotSource } from './data.js';
|
|
10
|
+
import { dashboardSettings, ghLogin } from './settings.js';
|
|
11
|
+
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
12
|
+
const LOOPBACK_HOST = /^(?:127\.0\.0\.1|localhost|\[::1\])(?::\d+)?$/i;
|
|
13
|
+
/** True for a Host header or Origin URL that names this machine. */
|
|
14
|
+
export function isLoopbackHost(host) {
|
|
15
|
+
return host !== undefined && LOOPBACK_HOST.test(host);
|
|
16
|
+
}
|
|
17
|
+
/** True when an Origin header is absent (not a browser cross-site call) or is a loopback page. */
|
|
18
|
+
export function isAllowedOrigin(origin) {
|
|
19
|
+
if (origin === undefined)
|
|
20
|
+
return true;
|
|
21
|
+
try {
|
|
22
|
+
return isLoopbackHost(new URL(origin).host);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** `dist/web` from compiled code (`dist/server`) or from source (`src/server`). */
|
|
29
|
+
export function defaultWebDir() {
|
|
30
|
+
const candidates = [new URL('../web/', import.meta.url), new URL('../../dist/web/', import.meta.url)].map((u) => fileURLToPath(u));
|
|
31
|
+
return candidates.find((dir) => existsSync(dir)) ?? candidates[0];
|
|
32
|
+
}
|
|
33
|
+
/** Builds the HTTP app without opening a port, so tests can call `app.request()`. */
|
|
34
|
+
export async function createApp(options) {
|
|
35
|
+
const source = options.source ?? (options.demo ? snapshotSource(await loadDemoSnapshot()) : projectsSource(options.projectsDir ?? process.cwd()));
|
|
36
|
+
const bus = options.bus ?? new EventBus();
|
|
37
|
+
const app = new Hono();
|
|
38
|
+
app.use('*', async (c, next) => {
|
|
39
|
+
await next();
|
|
40
|
+
for (const [name, value] of Object.entries(SECURITY_HEADERS))
|
|
41
|
+
c.res.headers.set(name, value);
|
|
42
|
+
});
|
|
43
|
+
// Defence against DNS rebinding and cross-site requests: this server only answers its own pages.
|
|
44
|
+
app.use('*', async (c, next) => {
|
|
45
|
+
if (!isLoopbackHost(c.req.header('host') ?? new URL(c.req.url).host) || !isAllowedOrigin(c.req.header('origin'))) {
|
|
46
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
47
|
+
}
|
|
48
|
+
// JSON bodies force a CORS preflight, which this server never grants.
|
|
49
|
+
if (c.req.method === 'POST' && !(c.req.header('content-type') ?? '').toLowerCase().startsWith('application/json')) {
|
|
50
|
+
return c.json({ error: 'content-type must be application/json' }, 415);
|
|
51
|
+
}
|
|
52
|
+
await next();
|
|
53
|
+
});
|
|
54
|
+
app.get('/api/health', (c) => c.json({ ok: true, version: pkg.version, demo: options.demo }));
|
|
55
|
+
app.get('/api/loops', async (c) => c.json(await source.loops()));
|
|
56
|
+
app.get('/api/loops/:id', async (c) => {
|
|
57
|
+
const loop = await source.loop(c.req.param('id'));
|
|
58
|
+
return loop ? c.json(loop) : c.json({ error: 'no such loop' }, 404);
|
|
59
|
+
});
|
|
60
|
+
app.post('/api/loops/:id/answer', async (c) => {
|
|
61
|
+
if (!source.answer)
|
|
62
|
+
return c.json({ error: 'demo is read-only' }, 409);
|
|
63
|
+
const body = await c.req.json().catch(() => undefined);
|
|
64
|
+
const { file, answer } = (body ?? {});
|
|
65
|
+
if (typeof answer !== 'string' || answer.trim() === '')
|
|
66
|
+
return c.json({ error: 'answer must be a non-empty string' }, 400);
|
|
67
|
+
// Only the BLOCKED.md at the clone root can be answered; the file name is never used to build a path.
|
|
68
|
+
if (file !== 'BLOCKED.md')
|
|
69
|
+
return c.json({ error: 'file must be "BLOCKED.md"' }, 400);
|
|
70
|
+
try {
|
|
71
|
+
const result = await source.answer(c.req.param('id'), answer);
|
|
72
|
+
return result ? c.json({ ok: true, ...result }) : c.json({ error: 'no such loop' }, 404);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 422);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
app.get('/api/inbox', async (c) => c.json(await source.inbox()));
|
|
79
|
+
app.get('/api/checks', async (c) => c.json(await source.checks()));
|
|
80
|
+
// `gh` is asked once per server, on first use: the account rarely changes and the call takes about a second.
|
|
81
|
+
let settings;
|
|
82
|
+
app.get('/api/settings', async (c) => c.json(await (settings ??= dashboardSettings(options, options.ghAccount ?? ghLogin))));
|
|
83
|
+
app.get('/api/events', (c) => streamSSE(c, async (stream) => {
|
|
84
|
+
const unsubscribe = bus.subscribe((event) => {
|
|
85
|
+
const { type, ...data } = event;
|
|
86
|
+
void stream.writeSSE({ event: type, data: JSON.stringify(data) }).catch(() => undefined);
|
|
87
|
+
});
|
|
88
|
+
let wake = () => undefined;
|
|
89
|
+
let timer;
|
|
90
|
+
stream.onAbort(() => wake());
|
|
91
|
+
await stream.writeSSE({ event: 'ready', data: '{}' });
|
|
92
|
+
// A comment line every 25 s keeps proxies and browsers from dropping an idle connection.
|
|
93
|
+
while (!stream.aborted) {
|
|
94
|
+
await new Promise((resolve) => {
|
|
95
|
+
wake = resolve;
|
|
96
|
+
timer = setTimeout(resolve, 25_000);
|
|
97
|
+
});
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
if (!stream.aborted)
|
|
100
|
+
await stream.write(': keep-alive\n\n');
|
|
101
|
+
}
|
|
102
|
+
unsubscribe();
|
|
103
|
+
}));
|
|
104
|
+
app.all('/api/*', (c) => c.json({ error: 'not found' }, 404));
|
|
105
|
+
const serveWeb = staticHandler(options.webDir ?? defaultWebDir());
|
|
106
|
+
app.get('*', (c) => serveWeb(c));
|
|
107
|
+
app.notFound((c) => c.json({ error: 'not found' }, 404));
|
|
108
|
+
return app;
|
|
109
|
+
}
|
|
110
|
+
/** Listens on 127.0.0.1 only; `url` carries the port actually bound (port 0 picks a free one). */
|
|
111
|
+
export async function startServer(options) {
|
|
112
|
+
const bus = options.bus ?? new EventBus();
|
|
113
|
+
const app = await createApp({ ...options, bus });
|
|
114
|
+
const stopWatching = options.demo ? undefined : watchProjects(options.projectsDir ?? process.cwd(), bus, { ...DEFAULT_WATCH, ...options.watch });
|
|
115
|
+
const server = await new Promise((resolve, reject) => {
|
|
116
|
+
const s = serve({ fetch: app.fetch, port: options.port, hostname: '127.0.0.1' }, () => resolve(s));
|
|
117
|
+
s.once('error', reject);
|
|
118
|
+
});
|
|
119
|
+
const { port } = server.address();
|
|
120
|
+
return {
|
|
121
|
+
url: `http://127.0.0.1:${port}`,
|
|
122
|
+
close: async () => {
|
|
123
|
+
const watcherStopped = stopWatching?.();
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
server.close((e) => (e ? reject(e) : resolve()));
|
|
126
|
+
// Open SSE connections would keep close() waiting forever.
|
|
127
|
+
if ('closeAllConnections' in server)
|
|
128
|
+
server.closeAllConnections();
|
|
129
|
+
});
|
|
130
|
+
await watcherStopped;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { runAnswer } from '../cli/loops.js';
|
|
6
|
+
import { parseBlocked } from '../core/blocked.js';
|
|
7
|
+
import { readLoopFacts, readRunCounter } from '../core/repo.js';
|
|
8
|
+
import { deriveStatus } from '../core/status.js';
|
|
9
|
+
import { checkLoop } from './health.js';
|
|
10
|
+
import { sortByAttention, toSummary } from './data.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
async function readOptional(file) {
|
|
13
|
+
return readFile(file, 'utf8').catch(() => undefined);
|
|
14
|
+
}
|
|
15
|
+
/** Recent commits of a clone, newest first. Empty when the folder is not a repo. */
|
|
16
|
+
export async function readTimeline(dir, limit = 50) {
|
|
17
|
+
try {
|
|
18
|
+
const { stdout } = await execFileAsync('git', ['log', `-${limit}`, '--format=%h%x09%cI%x09%s'], {
|
|
19
|
+
cwd: dir,
|
|
20
|
+
env: { ...process.env, GIT_CEILING_DIRECTORIES: path.dirname(path.resolve(dir)) },
|
|
21
|
+
});
|
|
22
|
+
return stdout
|
|
23
|
+
.split('\n')
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.map((line) => {
|
|
26
|
+
const [sha = '', at = '', ...message] = line.split('\t');
|
|
27
|
+
return { sha, at: new Date(at).toISOString(), message: message.join('\t') };
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Bullet lines ("- x") directly under the first heading or bold label that matches `label`. */
|
|
35
|
+
export function bulletsUnder(markdown, label) {
|
|
36
|
+
const lines = markdown.split(/\r?\n/);
|
|
37
|
+
const start = lines.findIndex((l) => label.test(l.replace(/^#+\s*|\*\*/g, '').trim()));
|
|
38
|
+
if (start === -1)
|
|
39
|
+
return [];
|
|
40
|
+
const bullets = [];
|
|
41
|
+
for (const line of lines.slice(start + 1)) {
|
|
42
|
+
if (/^#+\s/.test(line))
|
|
43
|
+
break;
|
|
44
|
+
const bullet = /^\s*[-*]\s+(.*\S)\s*$/.exec(line);
|
|
45
|
+
if (bullet?.[1])
|
|
46
|
+
bullets.push(bullet[1]);
|
|
47
|
+
else if (bullets.length > 0 && line.trim() === '')
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
return bullets;
|
|
51
|
+
}
|
|
52
|
+
/** The done-when items of a task.md; an item only counts as passing once the loop wrote DONE.md. */
|
|
53
|
+
export function contractFromTask(taskText, finished) {
|
|
54
|
+
return bulletsUnder(taskText, /^done when/i).map((text) => ({ text, pass: finished }));
|
|
55
|
+
}
|
|
56
|
+
/** Reads one clone into the API's LoopDetail. `now` is a parameter so tests are deterministic. */
|
|
57
|
+
export async function readLoop(dir, now = new Date()) {
|
|
58
|
+
const [baseFacts, counter, timeline, taskText] = await Promise.all([
|
|
59
|
+
readLoopFacts(dir),
|
|
60
|
+
readRunCounter(dir),
|
|
61
|
+
readTimeline(dir),
|
|
62
|
+
readOptional(path.join(dir, '.ai', 'task.md')),
|
|
63
|
+
]);
|
|
64
|
+
// Any failing health check turns the loop "failing" with that check's reason (IDEA.md status rule 5).
|
|
65
|
+
const checks = await checkLoop(dir, baseFacts, now);
|
|
66
|
+
const facts = { ...baseFacts, healthFailures: checks.filter((c) => !c.ok).map((c) => c.reason) };
|
|
67
|
+
const status = deriveStatus(facts, now);
|
|
68
|
+
const round = facts.roundsDone + 1;
|
|
69
|
+
const task = taskText ?? '';
|
|
70
|
+
return {
|
|
71
|
+
id: path.basename(dir),
|
|
72
|
+
name: path.basename(dir),
|
|
73
|
+
state: status.state,
|
|
74
|
+
reason: status.reason,
|
|
75
|
+
round,
|
|
76
|
+
// The total is unknown for a real loop (the owner can always add a round), so it never reads "5 of 3".
|
|
77
|
+
roundsTotal: round,
|
|
78
|
+
run: counter?.run ?? 0,
|
|
79
|
+
runLimit: counter?.limit ?? 0,
|
|
80
|
+
...(timeline[0] && { lastCommit: timeline[0] }),
|
|
81
|
+
timeline,
|
|
82
|
+
contract: contractFromTask(task, facts.hasDone),
|
|
83
|
+
knowledge: bulletsUnder(task, /^confirmed/i),
|
|
84
|
+
decisions: bulletsUnder(task, /^decisions/i),
|
|
85
|
+
history: [],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Subfolders of `projectsDir` that look like loop clones (a `.ai` folder). */
|
|
89
|
+
export async function findLoopDirs(projectsDir) {
|
|
90
|
+
const entries = await readdir(projectsDir, { withFileTypes: true }).catch(() => []);
|
|
91
|
+
const dirs = [];
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
94
|
+
continue;
|
|
95
|
+
const dir = path.join(projectsDir, entry.name);
|
|
96
|
+
if ((await stat(path.join(dir, '.ai')).catch(() => undefined))?.isDirectory())
|
|
97
|
+
dirs.push(dir);
|
|
98
|
+
}
|
|
99
|
+
return dirs.sort();
|
|
100
|
+
}
|
|
101
|
+
async function readInboxItem(dir) {
|
|
102
|
+
const file = path.join(dir, 'BLOCKED.md');
|
|
103
|
+
const [text, info] = await Promise.all([readOptional(file), stat(file).catch(() => undefined)]);
|
|
104
|
+
if (text === undefined || !info)
|
|
105
|
+
return undefined;
|
|
106
|
+
const note = parseBlocked(text);
|
|
107
|
+
return { loopId: path.basename(dir), file: 'BLOCKED.md', question: note.question, bestGuess: note.bestGuess, since: info.mtime.toISOString() };
|
|
108
|
+
}
|
|
109
|
+
/** A DataSource over a folder of loop clones, re-read on every call so the dashboard is never stale. */
|
|
110
|
+
export function projectsSource(projectsDir, now = () => new Date()) {
|
|
111
|
+
const all = async () => Promise.all((await findLoopDirs(projectsDir)).map((dir) => readLoop(dir, now())));
|
|
112
|
+
return {
|
|
113
|
+
loops: async () => sortByAttention((await all()).map(toSummary)),
|
|
114
|
+
// Match the id against folder names; never build a path from it.
|
|
115
|
+
loop: async (id) => {
|
|
116
|
+
const dir = (await findLoopDirs(projectsDir)).find((d) => path.basename(d) === id);
|
|
117
|
+
return dir === undefined ? undefined : readLoop(dir, now());
|
|
118
|
+
},
|
|
119
|
+
inbox: async () => {
|
|
120
|
+
const items = await Promise.all((await findLoopDirs(projectsDir)).map(readInboxItem));
|
|
121
|
+
return items.filter((i) => i !== undefined).sort((a, b) => a.since.localeCompare(b.since));
|
|
122
|
+
},
|
|
123
|
+
answer: async (id, answer) => {
|
|
124
|
+
const dir = (await findLoopDirs(projectsDir)).find((d) => path.basename(d) === id);
|
|
125
|
+
if (dir === undefined)
|
|
126
|
+
return undefined;
|
|
127
|
+
// runAnswer reads the exact word "accept" as "use the best guess"; the web form sends literal text only.
|
|
128
|
+
const text = answer.trim();
|
|
129
|
+
await runAnswer(dir, text === 'accept' ? 'Accept.' : text, { dryRun: false, now: now() });
|
|
130
|
+
const commit = (await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], { cwd: dir })).stdout.trim();
|
|
131
|
+
// Best effort: a clone without a remote (or offline) keeps the commit and the loop pulls it later.
|
|
132
|
+
const pushed = await execFileAsync('git', ['push'], { cwd: dir, timeout: 30_000, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } }).then(() => true, () => false);
|
|
133
|
+
return { commit, pushed };
|
|
134
|
+
},
|
|
135
|
+
checks: async () => {
|
|
136
|
+
const dirs = await findLoopDirs(projectsDir);
|
|
137
|
+
const perLoop = await Promise.all(dirs.map(async (dir) => checkLoop(dir, await readLoopFacts(dir), now())));
|
|
138
|
+
return perLoop.flat();
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=loops.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loops.js","sourceRoot":"","sources":["../../src/server/loops.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const DEFAULT_RUNNER = 'routine';
|
|
4
|
+
const LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
|
5
|
+
/** Reads a `gh` login from command output; anything that is not a plain GitHub user name counts as none. */
|
|
6
|
+
export function parseGhLogin(stdout) {
|
|
7
|
+
const login = stdout.trim();
|
|
8
|
+
return LOGIN.test(login) ? login : undefined;
|
|
9
|
+
}
|
|
10
|
+
/** The signed-in `gh` account, or undefined when `gh` is missing, signed out or slow. Uses the user's own `gh` login; no token is read. */
|
|
11
|
+
export function ghLogin() {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
execFile('gh', ['api', 'user', '--jq', '.login'], { timeout: 5000, windowsHide: true }, (error, stdout) => {
|
|
14
|
+
resolve(error ? undefined : parseGhLogin(stdout));
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** Settings for `GET /api/settings`; demo mode reveals nothing about this machine. */
|
|
19
|
+
export async function dashboardSettings(options, lookupGh = ghLogin) {
|
|
20
|
+
if (options.demo)
|
|
21
|
+
return { demo: true, defaultRunner: DEFAULT_RUNNER };
|
|
22
|
+
const ghAccount = await lookupGh();
|
|
23
|
+
return { demo: false, projectsDir: path.resolve(options.projectsDir ?? process.cwd()), ...(ghAccount && { ghAccount }), defaultRunner: DEFAULT_RUNNER };
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/server/settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,MAAM,CAAC,MAAM,cAAc,GAAuC,SAAS,CAAC;AAE5E,MAAM,KAAK,GAAG,qCAAqC,CAAC;AAEpD,4GAA4G;AAC5G,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC5B,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/C,CAAC;AAED,2IAA2I;AAC3I,MAAM,UAAU,OAAO;IACrB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;YACxG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAgD,EAChD,QAAQ,GAAsC,OAAO;IAErD,IAAI,OAAO,CAAC,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC;IACvE,MAAM,SAAS,GAAG,MAAM,QAAQ,EAAE,CAAC;IACnC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC;AAC1J,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sortByAttention, toSummary } from './data.js';
|
|
4
|
+
/**
|
|
5
|
+
* The read-only API as plain files, for hosting the demo on GitHub Pages (no server there).
|
|
6
|
+
* Keys are paths relative to the site root. The web app in static mode fetches these relative URLs.
|
|
7
|
+
*/
|
|
8
|
+
export function staticApiFiles(snapshot, version) {
|
|
9
|
+
const files = new Map();
|
|
10
|
+
files.set('api/health.json', { ok: true, version, demo: true });
|
|
11
|
+
files.set('api/loops.json', sortByAttention(snapshot.loops.map(toSummary)));
|
|
12
|
+
for (const loop of snapshot.loops)
|
|
13
|
+
files.set(`api/loops/${loop.id}.json`, loop);
|
|
14
|
+
files.set('api/inbox.json', snapshot.inbox);
|
|
15
|
+
files.set('api/checks.json', snapshot.checks);
|
|
16
|
+
return files;
|
|
17
|
+
}
|
|
18
|
+
export async function writeStaticApi(outDir, snapshot, version) {
|
|
19
|
+
const written = [];
|
|
20
|
+
for (const [relative, data] of staticApiFiles(snapshot, version)) {
|
|
21
|
+
const file = path.join(outDir, ...relative.split('/'));
|
|
22
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
23
|
+
await writeFile(file, `${JSON.stringify(data, null, 2)}\n`);
|
|
24
|
+
written.push(relative);
|
|
25
|
+
}
|
|
26
|
+
return written;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=static-export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-export.js","sourceRoot":"","sources":["../../src/server/static-export.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEvD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,QAAsB,EAAE,OAAe;IACpE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAmB,CAAC;IACzC,KAAK,CAAC,GAAG,CAAC,iBAAiB,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,KAAK,CAAC,GAAG,CAAC,gBAAgB,EAAE,eAAe,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAC5E,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK;QAAE,KAAK,CAAC,GAAG,CAAC,aAAa,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAChF,KAAK,CAAC,GAAG,CAAC,gBAAgB,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5C,KAAK,CAAC,GAAG,CAAC,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC9C,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,MAAc,EAAE,QAAsB,EAAE,OAAe;IAC1F,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QACvD,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAC5D,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const CSP = [
|
|
4
|
+
"default-src 'none'",
|
|
5
|
+
"script-src 'self'",
|
|
6
|
+
"style-src 'self' 'unsafe-inline'", // React sets inline style attributes; scripts stay locked to 'self'
|
|
7
|
+
"img-src 'self' data:",
|
|
8
|
+
"font-src 'self' data:", // Vite inlines tiny font subsets as data: URIs
|
|
9
|
+
"connect-src 'self'",
|
|
10
|
+
"base-uri 'none'",
|
|
11
|
+
"form-action 'self'",
|
|
12
|
+
"frame-ancestors 'none'",
|
|
13
|
+
].join('; ');
|
|
14
|
+
/** Headers added to every response, API and files alike. */
|
|
15
|
+
export const SECURITY_HEADERS = {
|
|
16
|
+
'Content-Security-Policy': CSP,
|
|
17
|
+
'X-Content-Type-Options': 'nosniff',
|
|
18
|
+
'Referrer-Policy': 'no-referrer',
|
|
19
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
20
|
+
};
|
|
21
|
+
const MIME = {
|
|
22
|
+
'.html': 'text/html; charset=utf-8',
|
|
23
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
24
|
+
'.css': 'text/css; charset=utf-8',
|
|
25
|
+
'.json': 'application/json; charset=utf-8',
|
|
26
|
+
'.svg': 'image/svg+xml',
|
|
27
|
+
'.png': 'image/png',
|
|
28
|
+
'.ico': 'image/x-icon',
|
|
29
|
+
'.woff2': 'font/woff2',
|
|
30
|
+
'.woff': 'font/woff',
|
|
31
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
32
|
+
'.map': 'application/json; charset=utf-8',
|
|
33
|
+
};
|
|
34
|
+
export const mimeFor = (file) => MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream';
|
|
35
|
+
/**
|
|
36
|
+
* Maps a URL path to a file inside `root`, or undefined when it would escape the folder.
|
|
37
|
+
* Both separators are rejected so `..\` cannot sneak through on Windows.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveInside(root, urlPath) {
|
|
40
|
+
let decoded;
|
|
41
|
+
try {
|
|
42
|
+
decoded = decodeURIComponent(urlPath);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (decoded.includes('\0') || decoded.split(/[\\/]/).includes('..'))
|
|
48
|
+
return undefined;
|
|
49
|
+
const file = path.resolve(root, `.${decoded.startsWith('/') ? decoded : `/${decoded}`}`);
|
|
50
|
+
const rootResolved = path.resolve(root);
|
|
51
|
+
return file === rootResolved || file.startsWith(rootResolved + path.sep) ? file : undefined;
|
|
52
|
+
}
|
|
53
|
+
async function isFile(file) {
|
|
54
|
+
return (await stat(file).catch(() => undefined))?.isFile() ?? false;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Serves the built dashboard. Unknown paths without a file extension fall back to index.html so
|
|
58
|
+
* client-side routes work on reload; a missing asset (has an extension) is a real 404.
|
|
59
|
+
*/
|
|
60
|
+
export function staticHandler(root) {
|
|
61
|
+
return async (c) => {
|
|
62
|
+
const urlPath = new URL(c.req.url).pathname;
|
|
63
|
+
const file = resolveInside(root, urlPath);
|
|
64
|
+
if (file === undefined)
|
|
65
|
+
return c.text('Not found', 404);
|
|
66
|
+
let target = file;
|
|
67
|
+
if (!(await isFile(target))) {
|
|
68
|
+
if (path.extname(urlPath) !== '')
|
|
69
|
+
return c.text('Not found', 404);
|
|
70
|
+
target = path.join(root, 'index.html');
|
|
71
|
+
if (!(await isFile(target)))
|
|
72
|
+
return c.text('The dashboard is not built. Run `npm run build`.', 404);
|
|
73
|
+
}
|
|
74
|
+
const body = await readFile(target);
|
|
75
|
+
// Hashed assets never change; index.html must always be re-fetched.
|
|
76
|
+
const cache = target.includes(`${path.sep}assets${path.sep}`) ? 'public, max-age=31536000, immutable' : 'no-cache';
|
|
77
|
+
return new Response(new Uint8Array(body), { headers: { 'Content-Type': mimeFor(target), 'Cache-Control': cache } });
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=static.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static.js","sourceRoot":"","sources":["../../src/server/static.ts"],"names":[],"mappings":"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"}
|