sideby 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 +453 -2
- package/README.zh-CN.md +455 -0
- package/dist/build.json +1 -0
- package/dist/src/cli/args.d.ts +13 -0
- package/dist/src/cli/args.js +38 -0
- package/dist/src/cli/commands.d.ts +37 -0
- package/dist/src/cli/commands.js +487 -0
- package/dist/src/cli/format.d.ts +16 -0
- package/dist/src/cli/format.js +80 -0
- package/dist/src/cli/json-schemas.d.ts +2 -0
- package/dist/src/cli/json-schemas.js +224 -0
- package/dist/src/cli/main.d.ts +2 -0
- package/dist/src/cli/main.js +186 -0
- package/dist/src/cli/schemas.d.ts +1 -0
- package/dist/src/cli/schemas.js +16 -0
- package/dist/src/core/account-name.d.ts +2 -0
- package/dist/src/core/account-name.js +2 -0
- package/dist/src/core/accounts.d.ts +18 -0
- package/dist/src/core/accounts.js +116 -0
- package/dist/src/core/config.d.ts +60 -0
- package/dist/src/core/config.js +181 -0
- package/dist/src/core/create.d.ts +30 -0
- package/dist/src/core/create.js +126 -0
- package/dist/src/core/doctor.d.ts +41 -0
- package/dist/src/core/doctor.js +171 -0
- package/dist/src/core/errors.d.ts +12 -0
- package/dist/src/core/errors.js +14 -0
- package/dist/src/core/file-memo.d.ts +24 -0
- package/dist/src/core/file-memo.js +41 -0
- package/dist/src/core/fs-safe.d.ts +38 -0
- package/dist/src/core/fs-safe.js +205 -0
- package/dist/src/core/handoff.d.ts +41 -0
- package/dist/src/core/handoff.js +54 -0
- package/dist/src/core/identity.d.ts +23 -0
- package/dist/src/core/identity.js +79 -0
- package/dist/src/core/last-doctor.d.ts +36 -0
- package/dist/src/core/last-doctor.js +69 -0
- package/dist/src/core/launch.d.ts +35 -0
- package/dist/src/core/launch.js +108 -0
- package/dist/src/core/login-path.d.ts +23 -0
- package/dist/src/core/login-path.js +96 -0
- package/dist/src/core/paths.d.ts +13 -0
- package/dist/src/core/paths.js +32 -0
- package/dist/src/core/quota-levels.d.ts +19 -0
- package/dist/src/core/quota-levels.js +34 -0
- package/dist/src/core/secret-file.d.ts +30 -0
- package/dist/src/core/secret-file.js +116 -0
- package/dist/src/core/share-mode-names.d.ts +1 -0
- package/dist/src/core/share-mode-names.js +10 -0
- package/dist/src/core/share-modes.d.ts +24 -0
- package/dist/src/core/share-modes.js +405 -0
- package/dist/src/core/shell-init.d.ts +25 -0
- package/dist/src/core/shell-init.js +61 -0
- package/dist/src/core/skill-check.d.ts +11 -0
- package/dist/src/core/skill-check.js +57 -0
- package/dist/src/core/usage-days.d.ts +15 -0
- package/dist/src/core/usage-days.js +33 -0
- package/dist/src/core/version.d.ts +9 -0
- package/dist/src/core/version.js +46 -0
- package/dist/src/desktop/app.d.ts +60 -0
- package/dist/src/desktop/app.js +316 -0
- package/dist/src/desktop/icon.d.ts +16 -0
- package/dist/src/desktop/icon.js +174 -0
- package/dist/src/families/claude/index.d.ts +10 -0
- package/dist/src/families/claude/index.js +124 -0
- package/dist/src/families/claude/layout.d.ts +6 -0
- package/dist/src/families/claude/layout.js +5 -0
- package/dist/src/families/claude/quota-cache.d.ts +31 -0
- package/dist/src/families/claude/quota-cache.js +111 -0
- package/dist/src/families/claude/quota.d.ts +3 -0
- package/dist/src/families/claude/quota.js +53 -0
- package/dist/src/families/claude/usage.d.ts +22 -0
- package/dist/src/families/claude/usage.js +146 -0
- package/dist/src/families/codex/index.d.ts +5 -0
- package/dist/src/families/codex/index.js +67 -0
- package/dist/src/families/codex/rollout.d.ts +67 -0
- package/dist/src/families/codex/rollout.js +298 -0
- package/dist/src/families/grok/index.d.ts +8 -0
- package/dist/src/families/grok/index.js +78 -0
- package/dist/src/families/index.d.ts +3 -0
- package/dist/src/families/index.js +40 -0
- package/dist/src/families/logos.d.ts +21 -0
- package/dist/src/families/logos.js +26 -0
- package/dist/src/families/pi/index.d.ts +3 -0
- package/dist/src/families/pi/index.js +54 -0
- package/dist/src/families/shared/toml.d.ts +5 -0
- package/dist/src/families/shared/toml.js +16 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +4 -0
- package/dist/src/panel/background.d.ts +62 -0
- package/dist/src/panel/background.js +274 -0
- package/dist/src/panel/handler.d.ts +93 -0
- package/dist/src/panel/handler.js +411 -0
- package/dist/src/panel/i18n.d.ts +287 -0
- package/dist/src/panel/i18n.js +570 -0
- package/dist/src/panel/page-logic.d.ts +154 -0
- package/dist/src/panel/page-logic.js +326 -0
- package/dist/src/panel/page-script.d.ts +2 -0
- package/dist/src/panel/page-script.js +1633 -0
- package/dist/src/panel/page-styles.d.ts +7 -0
- package/dist/src/panel/page-styles.js +514 -0
- package/dist/src/panel/page.d.ts +20 -0
- package/dist/src/panel/page.js +96 -0
- package/dist/src/panel/server.d.ts +32 -0
- package/dist/src/panel/server.js +133 -0
- package/dist/src/panel/theme.d.ts +116 -0
- package/dist/src/panel/theme.js +132 -0
- package/dist/src/plugins/account-script.d.ts +4 -0
- package/dist/src/plugins/account-script.js +66 -0
- package/dist/src/plugins/bus.d.ts +33 -0
- package/dist/src/plugins/bus.js +84 -0
- package/dist/src/plugins/loader.d.ts +58 -0
- package/dist/src/plugins/loader.js +250 -0
- package/dist/src/quota/claude-statusline.d.ts +28 -0
- package/dist/src/quota/claude-statusline.js +301 -0
- package/dist/src/quota/statusline-tap.d.ts +11 -0
- package/dist/src/quota/statusline-tap.js +104 -0
- package/dist/src/runtime.d.ts +149 -0
- package/dist/src/runtime.js +371 -0
- package/dist/src/types.d.ts +278 -0
- package/dist/src/types.js +3 -0
- package/dist/testing/demo.d.ts +8 -0
- package/dist/testing/demo.js +50 -0
- package/dist/testing/index.d.ts +36 -0
- package/dist/testing/index.js +117 -0
- package/package.json +66 -4
- package/schemas/alias.json +102 -0
- package/schemas/app-install.json +74 -0
- package/schemas/app-uninstall.json +54 -0
- package/schemas/config.json +166 -0
- package/schemas/doctor.json +229 -0
- package/schemas/error.json +25 -0
- package/schemas/list.json +177 -0
- package/schemas/new.json +181 -0
- package/schemas/next.json +127 -0
- package/schemas/plugins.json +74 -0
- package/schemas/quota-setup.json +60 -0
- package/schemas/quota-teardown.json +30 -0
- package/schemas/quota.json +203 -0
- package/skills/sideby/SKILL.md +52 -0
- package/skills/sideby/references/accounts.md +31 -0
- package/skills/sideby/references/diagnose.md +47 -0
- package/skills/sideby/references/handoff.md +41 -0
- package/skills/sideby/references/quota.md +14 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// `sideby ui --background` and `--stop`: one detached Panel per state directory, found again through a pid file.
|
|
2
|
+
// The detached process runs `sideby ui --serve-detached`; that flag is internal. `--stop` signals a pid only when
|
|
3
|
+
// the process, its start time and the Panel answering on the recorded port all match the pid file.
|
|
4
|
+
import { execFile, spawn } from 'node:child_process';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import { mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { loginShellPath } from '../core/login-path.js';
|
|
9
|
+
import { buildId } from '../core/version.js';
|
|
10
|
+
import { DEFAULT_PANEL_PORT, isSidebyPanel, panelIdentity, startPanelServer, } from './server.js';
|
|
11
|
+
export const DETACHED_FLAG = 'serve-detached';
|
|
12
|
+
const START_TIMEOUT_MS = 20_000;
|
|
13
|
+
const LOCK_STALE_MS = 30_000;
|
|
14
|
+
const STOP_TIMEOUT_MS = 5000;
|
|
15
|
+
const LOG_MAX_BYTES = 1_000_000;
|
|
16
|
+
export const pidFilePath = (p) => join(p.stateDir, 'panel.json');
|
|
17
|
+
export const logFilePath = (p) => join(p.stateDir, 'panel.log');
|
|
18
|
+
const lockPath = (p) => join(p.stateDir, 'panel.lock');
|
|
19
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
20
|
+
export async function readPidFile(p) {
|
|
21
|
+
try {
|
|
22
|
+
const d = JSON.parse(await readFile(pidFilePath(p), 'utf8'));
|
|
23
|
+
if (Number.isInteger(d.pid) && Number.isInteger(d.port) && typeof d.url === 'string')
|
|
24
|
+
return { ...d, instance: typeof d.instance === 'string' ? d.instance : '' };
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
function alive(pid) {
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, 0);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return err.code === 'EPERM';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Output of `ps -o <field>= -p <pid>`, or undefined when the process is gone or `ps` is unavailable. */
|
|
39
|
+
function psField(pid, field) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
execFile('ps', ['-ww', '-o', `${field}=`, '-p', String(pid)], { timeout: 3000, env: { ...process.env, LC_ALL: 'C' } }, (err, stdout) => resolve(err ? undefined : stdout.trim() || undefined));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** When `pid` started, as `ps` prints it; with the pid it names one process, which a reused pid does not match. */
|
|
45
|
+
export const processStartTime = (pid) => psField(pid, 'lstart');
|
|
46
|
+
/**
|
|
47
|
+
* Checks that the pid file names a live detached Panel: the process is alive, started when the pid file says,
|
|
48
|
+
* runs `ui --serve-detached`, and the Panel on the recorded port answers with the same instance id and pid.
|
|
49
|
+
*/
|
|
50
|
+
async function checkPanel(rec) {
|
|
51
|
+
const stale = (reason) => ({ status: 'stale', reason });
|
|
52
|
+
if (!rec.instance)
|
|
53
|
+
return stale('it names no panel instance');
|
|
54
|
+
if (!alive(rec.pid))
|
|
55
|
+
return stale(`process ${rec.pid} is not running`);
|
|
56
|
+
if (rec.processStart && (await processStartTime(rec.pid)) !== rec.processStart)
|
|
57
|
+
return stale(`process ${rec.pid} is not the one that wrote it`);
|
|
58
|
+
const cmd = await psField(rec.pid, 'command');
|
|
59
|
+
if (!(cmd && / ui( |$)/.test(cmd) && cmd.includes(`--${DETACHED_FLAG}`)))
|
|
60
|
+
return stale(`process ${rec.pid} is not a sideby panel`);
|
|
61
|
+
const id = await panelIdentity(rec.port);
|
|
62
|
+
if (!id)
|
|
63
|
+
return {
|
|
64
|
+
status: 'unconfirmed',
|
|
65
|
+
reason: `process ${rec.pid} does not answer as a sideby panel on port ${rec.port}; if it is one, stop it with \`kill ${rec.pid}\``,
|
|
66
|
+
};
|
|
67
|
+
if (id.instance !== rec.instance || id.pid !== rec.pid)
|
|
68
|
+
return stale(`the panel on port ${rec.port} is not the one it names`);
|
|
69
|
+
return { status: 'ours', ...(id.build ? { build: id.build } : {}) };
|
|
70
|
+
}
|
|
71
|
+
/** URL of a running background Panel, or of any sideby Panel on `port`; removes a stale pid file. */
|
|
72
|
+
export async function findRunningPanel(p, port) {
|
|
73
|
+
const rec = await readPidFile(p);
|
|
74
|
+
if (rec) {
|
|
75
|
+
const check = await checkPanel(rec);
|
|
76
|
+
if (check.status === 'ours')
|
|
77
|
+
return rec.url;
|
|
78
|
+
if (check.status === 'stale')
|
|
79
|
+
await rm(pidFilePath(p), { force: true });
|
|
80
|
+
}
|
|
81
|
+
return (await isSidebyPanel(port)) ? `http://127.0.0.1:${port}/` : undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Stops this state directory's background Panel when it runs another build than `build` (or is older than build
|
|
85
|
+
* ids). Only a Panel the pid file names and that answers as itself is stopped; true when one was.
|
|
86
|
+
*/
|
|
87
|
+
async function stopOutdated(p, build) {
|
|
88
|
+
const rec = await readPidFile(p);
|
|
89
|
+
if (!rec)
|
|
90
|
+
return false;
|
|
91
|
+
const check = await checkPanel(rec);
|
|
92
|
+
if (check.status !== 'ours' || check.build === build)
|
|
93
|
+
return false;
|
|
94
|
+
return (await stopBackgroundPanel(p)).status === 'stopped';
|
|
95
|
+
}
|
|
96
|
+
/** Takes the start lock, waiting while another launch holds it; a lock older than 30 s is taken over. */
|
|
97
|
+
async function withStartLock(p, fn) {
|
|
98
|
+
const lock = lockPath(p);
|
|
99
|
+
const deadline = Date.now() + START_TIMEOUT_MS;
|
|
100
|
+
for (;;) {
|
|
101
|
+
try {
|
|
102
|
+
const fh = await open(lock, 'wx', 0o600);
|
|
103
|
+
await fh.writeFile(String(process.pid));
|
|
104
|
+
await fh.close();
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (err.code !== 'EEXIST')
|
|
109
|
+
throw err;
|
|
110
|
+
const age = await stat(lock).then((s) => Date.now() - s.mtimeMs, () => 0);
|
|
111
|
+
if (age > LOCK_STALE_MS || Date.now() > deadline)
|
|
112
|
+
await rm(lock, { force: true });
|
|
113
|
+
else
|
|
114
|
+
await sleep(200);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return await fn();
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await rm(lock, { force: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function openLog(p) {
|
|
125
|
+
const log = logFilePath(p);
|
|
126
|
+
const size = await stat(log).then((s) => s.size, () => 0);
|
|
127
|
+
if (size > LOG_MAX_BYTES)
|
|
128
|
+
await rename(log, `${log}.1`).catch(() => { });
|
|
129
|
+
return open(log, 'a', 0o600);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Makes sure one background Panel runs and returns its URL. Reuses a running one; otherwise starts
|
|
133
|
+
* `sideby ui --serve-detached` in its own session, logging to the state directory, and waits until it listens.
|
|
134
|
+
*/
|
|
135
|
+
export async function startBackgroundPanel(opts) {
|
|
136
|
+
const { paths } = opts;
|
|
137
|
+
const port = opts.port ?? DEFAULT_PANEL_PORT;
|
|
138
|
+
// After an upgrade or reinstall the old background Panel still runs the old code: replace it.
|
|
139
|
+
const replaced = await stopOutdated(paths, buildId(opts.env));
|
|
140
|
+
const running = await findRunningPanel(paths, port);
|
|
141
|
+
if (running)
|
|
142
|
+
return { url: running, reused: true };
|
|
143
|
+
await mkdir(paths.stateDir, { recursive: true, mode: 0o700 });
|
|
144
|
+
return withStartLock(paths, async () => {
|
|
145
|
+
// Another launch may have started one while this one waited for the lock.
|
|
146
|
+
const again = await findRunningPanel(paths, port);
|
|
147
|
+
if (again)
|
|
148
|
+
return { url: again, reused: true };
|
|
149
|
+
const log = await openLog(paths);
|
|
150
|
+
try {
|
|
151
|
+
await log.write(`\n[${new Date().toISOString()}] starting background panel on port ${port}\n`);
|
|
152
|
+
const child = spawn(process.execPath, [opts.entry, 'ui', `--${DETACHED_FLAG}`, '--no-open', '--port', String(port)], {
|
|
153
|
+
env: opts.env,
|
|
154
|
+
detached: true,
|
|
155
|
+
stdio: ['ignore', log.fd, log.fd, 'ipc'],
|
|
156
|
+
});
|
|
157
|
+
const msg = await new Promise((resolve, reject) => {
|
|
158
|
+
const timer = setTimeout(() => {
|
|
159
|
+
child.kill('SIGTERM');
|
|
160
|
+
reject(new Error(`the background panel did not start; see ${logFilePath(paths)}`));
|
|
161
|
+
}, START_TIMEOUT_MS);
|
|
162
|
+
child.once('message', (m) => {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
resolve(m);
|
|
165
|
+
});
|
|
166
|
+
child.once('error', (err) => {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
reject(err);
|
|
169
|
+
});
|
|
170
|
+
child.once('exit', (code) => {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
reject(new Error(`the background panel exited (code ${code}); see ${logFilePath(paths)}`));
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
child.removeAllListeners();
|
|
176
|
+
if (child.connected)
|
|
177
|
+
child.disconnect();
|
|
178
|
+
child.unref();
|
|
179
|
+
if (msg.reused)
|
|
180
|
+
return { url: msg.url, reused: true };
|
|
181
|
+
return { url: msg.url, reused: false, pid: child.pid, ...(replaced ? { replaced } : {}) };
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
await log.close();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Body of the detached process: serve the Panel with Hosts found through the login shell's PATH, tell the
|
|
190
|
+
* launching process the URL over IPC, keep the pid file while running, and remove it on SIGTERM or SIGINT.
|
|
191
|
+
*/
|
|
192
|
+
export async function serveDetached(opts) {
|
|
193
|
+
const { paths, log } = opts;
|
|
194
|
+
// Started now, awaited by the first request: the server listens without waiting for the shell.
|
|
195
|
+
const envReady = loginShellPath(opts.env).then((r) => {
|
|
196
|
+
log(r.source === 'login-shell'
|
|
197
|
+
? 'host PATH: from the login shell'
|
|
198
|
+
: `host PATH: login shell unusable (${r.reason}); using the inherited PATH`);
|
|
199
|
+
return { ...opts.env, PATH: r.path };
|
|
200
|
+
});
|
|
201
|
+
const instance = randomBytes(16).toString('hex');
|
|
202
|
+
const srv = await startPanelServer({
|
|
203
|
+
runtimeFactory: async () => opts.runtimeFactory(await envReady),
|
|
204
|
+
instance,
|
|
205
|
+
...(opts.port ? { port: opts.port } : {}),
|
|
206
|
+
open: false,
|
|
207
|
+
log,
|
|
208
|
+
});
|
|
209
|
+
const send = (m) => {
|
|
210
|
+
if (process.send)
|
|
211
|
+
process.send(m);
|
|
212
|
+
if (process.connected)
|
|
213
|
+
process.disconnect();
|
|
214
|
+
};
|
|
215
|
+
if (srv.reused) {
|
|
216
|
+
send({ url: srv.url, reused: true });
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
const rec = {
|
|
220
|
+
pid: process.pid,
|
|
221
|
+
port: Number(new URL(srv.url).port),
|
|
222
|
+
url: srv.url,
|
|
223
|
+
startedAt: new Date().toISOString(),
|
|
224
|
+
instance,
|
|
225
|
+
};
|
|
226
|
+
const processStart = await processStartTime(process.pid);
|
|
227
|
+
if (processStart)
|
|
228
|
+
rec.processStart = processStart;
|
|
229
|
+
await writeFile(pidFilePath(paths), `${JSON.stringify(rec, null, 2)}\n`, { mode: 0o600 });
|
|
230
|
+
send({ url: srv.url, reused: false });
|
|
231
|
+
await new Promise((resolve) => {
|
|
232
|
+
const stop = (sig) => {
|
|
233
|
+
log(`[${new Date().toISOString()}] ${sig}: stopping`);
|
|
234
|
+
void srv.close().finally(resolve);
|
|
235
|
+
};
|
|
236
|
+
process.once('SIGTERM', () => stop('SIGTERM'));
|
|
237
|
+
process.once('SIGINT', () => stop('SIGINT'));
|
|
238
|
+
process.on('SIGHUP', () => { });
|
|
239
|
+
});
|
|
240
|
+
const now = await readPidFile(paths);
|
|
241
|
+
if (now?.pid === process.pid)
|
|
242
|
+
await rm(pidFilePath(paths), { force: true });
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
/** Stops the background Panel named in the pid file, signalling it only when checkPanel confirms it. */
|
|
246
|
+
export async function stopBackgroundPanel(paths) {
|
|
247
|
+
const rec = await readPidFile(paths);
|
|
248
|
+
if (!rec)
|
|
249
|
+
return { status: 'not-running' };
|
|
250
|
+
const check = await checkPanel(rec);
|
|
251
|
+
if (check.status === 'stale') {
|
|
252
|
+
await rm(pidFilePath(paths), { force: true });
|
|
253
|
+
return { status: 'not-running', detail: `removed a stale pid file (${check.reason})` };
|
|
254
|
+
}
|
|
255
|
+
if (check.status === 'unconfirmed')
|
|
256
|
+
return { status: 'failed', pid: rec.pid, detail: check.reason };
|
|
257
|
+
try {
|
|
258
|
+
process.kill(rec.pid, 'SIGTERM');
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
return { status: 'failed', pid: rec.pid, detail: err.message };
|
|
262
|
+
}
|
|
263
|
+
const deadline = Date.now() + STOP_TIMEOUT_MS;
|
|
264
|
+
while (alive(rec.pid) && Date.now() < deadline)
|
|
265
|
+
await sleep(100);
|
|
266
|
+
if (alive(rec.pid))
|
|
267
|
+
return {
|
|
268
|
+
status: 'failed',
|
|
269
|
+
pid: rec.pid,
|
|
270
|
+
detail: `process ${rec.pid} did not exit within ${STOP_TIMEOUT_MS / 1000} s; stop it with \`kill ${rec.pid}\``,
|
|
271
|
+
};
|
|
272
|
+
await rm(pidFilePath(paths), { force: true });
|
|
273
|
+
return { status: 'stopped', pid: rec.pid, url: rec.url };
|
|
274
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { AccountStatus, DoctorHistory, FamilyInfo, Runtime } from '../runtime.ts';
|
|
3
|
+
import type { Account, FamilyLogo, QuotaResult, QuotaSetupPlan, UsageResult } from '../types.ts';
|
|
4
|
+
import { type PanelTheme } from './theme.ts';
|
|
5
|
+
export declare const PANEL_VERSION: string;
|
|
6
|
+
export declare const MAX_BODY_BYTES: number;
|
|
7
|
+
/** Write requests allowed to wait behind the running one; more get 409. */
|
|
8
|
+
export declare const MAX_QUEUED_WRITES = 4;
|
|
9
|
+
export interface PanelHandlerOptions {
|
|
10
|
+
/** A Runtime, or a factory called per request so every response reflects the disk. */
|
|
11
|
+
runtime: Runtime | (() => Promise<Runtime>);
|
|
12
|
+
/** Mount point such as `/accounts`; empty or `/` for the root. */
|
|
13
|
+
basePath?: string;
|
|
14
|
+
/** Accepted `Host` header values, for example `127.0.0.1:17420`. */
|
|
15
|
+
allowedHosts: string[];
|
|
16
|
+
/** Rejects changes (fix, create, quota setup apply and teardown) with 403. */
|
|
17
|
+
readOnly?: boolean;
|
|
18
|
+
version?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Design tokens that make the page match the embedding page. Checked when the handler is created: an unknown
|
|
21
|
+
* token or a value that could leave its CSS declaration throws a TypeError.
|
|
22
|
+
*/
|
|
23
|
+
theme?: PanelTheme;
|
|
24
|
+
/**
|
|
25
|
+
* The Panel's language. `en` or `zh` wins over what the viewer saved and hides the Panel's own switch; `auto`
|
|
26
|
+
* (the default) uses the viewer's saved choice, then the browser language. Checked when the handler is created:
|
|
27
|
+
* any other value throws a TypeError. A Portal that changes language creates the handler again.
|
|
28
|
+
*/
|
|
29
|
+
lang?: 'auto' | 'en' | 'zh';
|
|
30
|
+
}
|
|
31
|
+
export type PanelHandler = ((req: IncomingMessage, res: ServerResponse) => Promise<boolean>) & {
|
|
32
|
+
/** Per-instance secret the page sends as `x-sideby-token`. */
|
|
33
|
+
readonly token: string;
|
|
34
|
+
};
|
|
35
|
+
export type PanelAccount = (AccountStatus | (Account & Partial<AccountStatus>)) & {
|
|
36
|
+
quota?: QuotaResult;
|
|
37
|
+
usage?: UsageResult;
|
|
38
|
+
error?: string;
|
|
39
|
+
/** Latest Doctor result for this Account; absent when it was never checked. */
|
|
40
|
+
health?: {
|
|
41
|
+
at: string;
|
|
42
|
+
fail: number;
|
|
43
|
+
warn: number;
|
|
44
|
+
};
|
|
45
|
+
/** Config `aliases` that point at this Account, sorted; absent when none do. */
|
|
46
|
+
aliases?: string[];
|
|
47
|
+
/** Host arguments of those aliases that add any (alias → arguments); absent when none do. */
|
|
48
|
+
aliasArgs?: Record<string, string[]>;
|
|
49
|
+
};
|
|
50
|
+
export type { DoctorHistory };
|
|
51
|
+
/** Counts of failing and warning Findings per checked Account, from the Doctor history. */
|
|
52
|
+
export declare function healthByAccount(history: DoctorHistory | undefined): Map<string, {
|
|
53
|
+
at: string;
|
|
54
|
+
fail: number;
|
|
55
|
+
warn: number;
|
|
56
|
+
}>;
|
|
57
|
+
/** Alias names per Account ref, sorted. An alias whose target matches no Account is left out. */
|
|
58
|
+
export declare function aliasesByAccount(aliases: Readonly<Record<string, string>> | undefined, accounts: readonly Account[]): Map<string, string[]>;
|
|
59
|
+
/** A Family as the page sees it: FamilyInfo plus the mark from its FamilyDef. */
|
|
60
|
+
export type PanelFamily = FamilyInfo & {
|
|
61
|
+
logo?: FamilyLogo;
|
|
62
|
+
};
|
|
63
|
+
export interface PanelState {
|
|
64
|
+
schemaVersion: 1;
|
|
65
|
+
version: string;
|
|
66
|
+
/** buildId() of the process serving the Panel: a page from another build reloads itself. */
|
|
67
|
+
build: string;
|
|
68
|
+
readOnly: boolean;
|
|
69
|
+
generatedAt: string;
|
|
70
|
+
families: PanelFamily[];
|
|
71
|
+
accounts: PanelAccount[];
|
|
72
|
+
pluginErrors: {
|
|
73
|
+
where: string;
|
|
74
|
+
message: string;
|
|
75
|
+
}[];
|
|
76
|
+
quotaSetups: {
|
|
77
|
+
family: string;
|
|
78
|
+
summary: string;
|
|
79
|
+
plan: QuotaSetupPlan;
|
|
80
|
+
}[];
|
|
81
|
+
/** Latest Doctor result per Account and per Family. */
|
|
82
|
+
history?: DoctorHistory;
|
|
83
|
+
/** The config `aliases` as alias → Account ref (arguments left out), so the page can tell which names are taken. */
|
|
84
|
+
configAliases?: Record<string, string>;
|
|
85
|
+
/**
|
|
86
|
+
* Family → the Account `sideby next <family>` would pick (spec §3.13). Only Families with a Quota source,
|
|
87
|
+
* at least two subscription Accounts, some Quota data and a pick; API Accounts are left out as in the CLI default.
|
|
88
|
+
*/
|
|
89
|
+
handoff?: Record<string, string>;
|
|
90
|
+
}
|
|
91
|
+
/** The `handoff` map of the Panel state, from the statuses and Quota already read for the page. */
|
|
92
|
+
export declare function handoffByFamily(rt: Runtime, accounts: readonly PanelAccount[], now: Date): Record<string, string>;
|
|
93
|
+
export declare function createPanelHandler(opts: PanelHandlerOptions): PanelHandler;
|