@data-fair/lib-dev 0.0.0-stage → 0.1.1
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 +38 -2
- package/cli.d.ts +2 -0
- package/cli.js +64 -0
- package/cli.js.map +1 -0
- package/cli.test.d.ts +1 -0
- package/cli.test.js +44 -0
- package/cli.test.js.map +1 -0
- package/compose.d.ts +24 -0
- package/compose.js +61 -0
- package/compose.js.map +1 -0
- package/compose.test.d.ts +1 -0
- package/compose.test.js +69 -0
- package/compose.test.js.map +1 -0
- package/config.d.ts +14 -0
- package/config.js +32 -0
- package/config.js.map +1 -0
- package/config.test.d.ts +1 -0
- package/config.test.js +44 -0
- package/config.test.js.map +1 -0
- package/env.d.ts +18 -0
- package/env.js +94 -0
- package/env.js.map +1 -0
- package/env.test.d.ts +1 -0
- package/env.test.js +106 -0
- package/env.test.js.map +1 -0
- package/exec.d.ts +5 -0
- package/exec.js +11 -0
- package/exec.js.map +1 -0
- package/exec.test.d.ts +1 -0
- package/exec.test.js +10 -0
- package/exec.test.js.map +1 -0
- package/launch.d.ts +13 -0
- package/launch.js +36 -0
- package/launch.js.map +1 -0
- package/launch.test.d.ts +1 -0
- package/launch.test.js +19 -0
- package/launch.test.js.map +1 -0
- package/mem-cap.sh +33 -0
- package/mem-cap.test.d.ts +1 -0
- package/mem-cap.test.js +38 -0
- package/mem-cap.test.js.map +1 -0
- package/optional.d.ts +8 -0
- package/optional.js +21 -0
- package/optional.js.map +1 -0
- package/optional.test.d.ts +1 -0
- package/optional.test.js +24 -0
- package/optional.test.js.map +1 -0
- package/package.json +29 -4
- package/panes.d.ts +18 -0
- package/panes.js +68 -0
- package/panes.js.map +1 -0
- package/panes.test.d.ts +1 -0
- package/panes.test.js +97 -0
- package/panes.test.js.map +1 -0
- package/setup.d.ts +6 -0
- package/setup.js +21 -0
- package/setup.js.map +1 -0
- package/setup.test.d.ts +1 -0
- package/setup.test.js +47 -0
- package/setup.test.js.map +1 -0
- package/status-pane.sh +112 -0
- package/status-pane.test.d.ts +1 -0
- package/status-pane.test.js +71 -0
- package/status-pane.test.js.map +1 -0
- package/status.d.ts +51 -0
- package/status.js +147 -0
- package/status.js.map +1 -0
- package/status.test.d.ts +1 -0
- package/status.test.js +123 -0
- package/status.test.js.map +1 -0
- package/worktree-delete.sh +141 -0
- package/worktree.sh +164 -0
- package/worktree.test.d.ts +1 -0
- package/worktree.test.js +145 -0
- package/worktree.test.js.map +1 -0
- package/zellij.d.ts +19 -0
- package/zellij.js +50 -0
- package/zellij.js.map +1 -0
- package/zellij.test.d.ts +1 -0
- package/zellij.test.js +50 -0
- package/zellij.test.js.map +1 -0
package/panes.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { interpolate, readEnvFile } from './config.js';
|
|
2
|
+
import { realExec } from './exec.js';
|
|
3
|
+
import { requireSession, findPane, listPanes, dumpScreen, sendKeys } from './zellij.js';
|
|
4
|
+
// eslint-disable-next-line no-control-regex -- matching ANSI escape sequences is the point
|
|
5
|
+
export const stripAnsi = (text) => text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '');
|
|
6
|
+
export const tailLines = (text, n) => text.replace(/\s+$/, '').split('\n').slice(-n).join('\n');
|
|
7
|
+
export const logs = (root, title, n, exec = realExec) => {
|
|
8
|
+
const session = requireSession(exec, root);
|
|
9
|
+
return tailLines(stripAnsi(dumpScreen(exec, session, findPane(exec, session, title))), n);
|
|
10
|
+
};
|
|
11
|
+
export const checkReady = async (url) => {
|
|
12
|
+
try {
|
|
13
|
+
return (await fetch(url, { signal: AbortSignal.timeout(2000) })).ok;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const STOP_TIMEOUT = 10_000;
|
|
20
|
+
const READY_TIMEOUT = 60_000;
|
|
21
|
+
export const restart = async (root, title, config, deps = {}) => {
|
|
22
|
+
const exec = deps.exec ?? realExec;
|
|
23
|
+
const ready = deps.ready ?? checkReady;
|
|
24
|
+
const sleep = deps.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
25
|
+
const now = deps.now ?? Date.now;
|
|
26
|
+
const session = requireSession(exec, root);
|
|
27
|
+
let pane = findPane(exec, session, title);
|
|
28
|
+
// the failure message must survive a failing dump
|
|
29
|
+
const lastLogs = () => {
|
|
30
|
+
try {
|
|
31
|
+
return tailLines(stripAnsi(dumpScreen(exec, session, pane)), 20);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return `logs unavailable: ${err.message}`;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
// follow the pane by id: a process can retitle its pane while exiting
|
|
38
|
+
const current = () => {
|
|
39
|
+
const found = listPanes(exec, session).find(p => p.id === pane.id);
|
|
40
|
+
if (!found)
|
|
41
|
+
throw new Error(`pane "${title}" disappeared from session ${session}`);
|
|
42
|
+
return found;
|
|
43
|
+
};
|
|
44
|
+
if (!pane.terminal_command)
|
|
45
|
+
throw new Error(`pane "${title}" has no command to rerun`);
|
|
46
|
+
// resolve the URL first, so a broken .env fails before anything is stopped
|
|
47
|
+
const readyUrl = config.panes[title]?.ready && interpolate(config.panes[title].ready, deps.env ?? readEnvFile(root));
|
|
48
|
+
if (!pane.exited) {
|
|
49
|
+
sendKeys(exec, session, pane, 'Ctrl c');
|
|
50
|
+
const deadline = now() + STOP_TIMEOUT;
|
|
51
|
+
while (!(pane = current()).exited) {
|
|
52
|
+
if (now() > deadline)
|
|
53
|
+
return { ok: false, message: `pane "${title}" did not stop within 10s after Ctrl+C`, logs: lastLogs() };
|
|
54
|
+
await sleep(200);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
sendKeys(exec, session, pane, 'Enter');
|
|
58
|
+
if (!readyUrl)
|
|
59
|
+
return { ok: true, message: `pane "${title}" restarted (no readiness URL in dev/dev.json, check its logs)` };
|
|
60
|
+
const start = now();
|
|
61
|
+
while (now() - start < READY_TIMEOUT) {
|
|
62
|
+
if (await ready(readyUrl))
|
|
63
|
+
return { ok: true, message: `pane "${title}" ready after ${((now() - start) / 1000).toFixed(1)}s` };
|
|
64
|
+
await sleep(500);
|
|
65
|
+
}
|
|
66
|
+
return { ok: false, message: `pane "${title}" not ready after 60s (${readyUrl}), report to the user instead of retrying`, logs: lastLogs() };
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=panes.js.map
|
package/panes.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panes.js","sourceRoot":"","sources":["panes.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DevConfig } from './config.js'\nimport { interpolate, readEnvFile } from './config.js'\nimport { realExec, type Exec } from './exec.js'\nimport { requireSession, findPane, listPanes, dumpScreen, sendKeys, type Pane } from './zellij.js'\n\n// eslint-disable-next-line no-control-regex -- matching ANSI escape sequences is the point\nexport const stripAnsi = (text: string) => text.replace(/\\x1b\\[[0-9;?]*[ -/]*[@-~]/g, '')\n\nexport const tailLines = (text: string, n: number) => text.replace(/\\s+$/, '').split('\\n').slice(-n).join('\\n')\n\nexport const logs = (root: string, title: string, n: number, exec: Exec = realExec) => {\n const session = requireSession(exec, root)\n return tailLines(stripAnsi(dumpScreen(exec, session, findPane(exec, session, title))), n)\n}\n\nexport type RestartResult = { ok: boolean, message: string, logs?: string }\n\nexport const checkReady = async (url: string) => {\n try {\n return (await fetch(url, { signal: AbortSignal.timeout(2000) })).ok\n } catch {\n return false\n }\n}\n\nconst STOP_TIMEOUT = 10_000\nconst READY_TIMEOUT = 60_000\n\nexport const restart = async (root: string, title: string, config: DevConfig, deps: {\n exec?: Exec,\n ready?: (url: string) => Promise<boolean>,\n sleep?: (ms: number) => Promise<void>,\n now?: () => number,\n env?: Record<string, string>\n} = {}): Promise<RestartResult> => {\n const exec = deps.exec ?? realExec\n const ready = deps.ready ?? checkReady\n const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)))\n const now = deps.now ?? Date.now\n const session = requireSession(exec, root)\n let pane = findPane(exec, session, title)\n // the failure message must survive a failing dump\n const lastLogs = () => {\n try {\n return tailLines(stripAnsi(dumpScreen(exec, session, pane)), 20)\n } catch (err: any) {\n return `logs unavailable: ${err.message}`\n }\n }\n // follow the pane by id: a process can retitle its pane while exiting\n const current = (): Pane => {\n const found = listPanes(exec, session).find(p => p.id === pane.id)\n if (!found) throw new Error(`pane \"${title}\" disappeared from session ${session}`)\n return found\n }\n if (!pane.terminal_command) throw new Error(`pane \"${title}\" has no command to rerun`)\n // resolve the URL first, so a broken .env fails before anything is stopped\n const readyUrl = config.panes[title]?.ready && interpolate(config.panes[title].ready!, deps.env ?? readEnvFile(root))\n\n if (!pane.exited) {\n sendKeys(exec, session, pane, 'Ctrl c')\n const deadline = now() + STOP_TIMEOUT\n while (!(pane = current()).exited) {\n if (now() > deadline) return { ok: false, message: `pane \"${title}\" did not stop within 10s after Ctrl+C`, logs: lastLogs() }\n await sleep(200)\n }\n }\n sendKeys(exec, session, pane, 'Enter')\n\n if (!readyUrl) return { ok: true, message: `pane \"${title}\" restarted (no readiness URL in dev/dev.json, check its logs)` }\n const start = now()\n while (now() - start < READY_TIMEOUT) {\n if (await ready(readyUrl)) return { ok: true, message: `pane \"${title}\" ready after ${((now() - start) / 1000).toFixed(1)}s` }\n await sleep(500)\n }\n return { ok: false, message: `pane \"${title}\" not ready after 60s (${readyUrl}), report to the user instead of retrying`, logs: lastLogs() }\n}\n"]}
|
package/panes.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/panes.test.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* eslint-disable no-template-curly-in-string -- ${VAR} placeholders are the dev/dev.json syntax */
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { strict as assert } from 'assert';
|
|
4
|
+
import { stripAnsi, tailLines, restart } from './panes.js';
|
|
5
|
+
const config = { ports: [], panes: { api: { ready: 'http://localhost:${DEV_API_PORT}/api/ping' } } };
|
|
6
|
+
// a fake zellij whose api pane stops on Ctrl c (unless stubborn) and runs again on Enter
|
|
7
|
+
const makeZellij = (opts = {}) => {
|
|
8
|
+
const state = { exited: false, keys: [], title: 'api' };
|
|
9
|
+
const exec = (cmd, args) => {
|
|
10
|
+
if (args[0] === '--version')
|
|
11
|
+
return 'zellij 0.45.1';
|
|
12
|
+
if (args[0] === 'list-sessions')
|
|
13
|
+
return 'portals [Created 1m ago]\n';
|
|
14
|
+
const action = args[3];
|
|
15
|
+
if (action === 'list-panes')
|
|
16
|
+
return JSON.stringify([{ id: 3, title: state.title, exited: state.exited, exit_status: state.exited ? 0 : null, is_plugin: false, terminal_command: 'node --watch index.ts' }]);
|
|
17
|
+
if (action === 'send-keys') {
|
|
18
|
+
const keys = args[6];
|
|
19
|
+
state.keys.push(keys);
|
|
20
|
+
if (keys === 'Ctrl c' && !opts.stubborn)
|
|
21
|
+
state.exited = true;
|
|
22
|
+
if (keys === 'Ctrl c' && opts.retitle)
|
|
23
|
+
state.title = 'node (exited)';
|
|
24
|
+
if (keys === 'Enter')
|
|
25
|
+
state.exited = false;
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
if (action === 'dump-screen') {
|
|
29
|
+
if (opts.dumpFails)
|
|
30
|
+
throw new Error('spawnSync zellij ENOBUFS');
|
|
31
|
+
return 'line1\nAPI crashed\n\n';
|
|
32
|
+
}
|
|
33
|
+
throw new Error('unexpected ' + args.join(' '));
|
|
34
|
+
};
|
|
35
|
+
return { exec, state };
|
|
36
|
+
};
|
|
37
|
+
const clock = () => {
|
|
38
|
+
let t = 0;
|
|
39
|
+
return { now: () => t, sleep: async (ms) => { t += ms; } };
|
|
40
|
+
};
|
|
41
|
+
describe('logs helpers', () => {
|
|
42
|
+
it('strips ANSI codes', () => {
|
|
43
|
+
assert.equal(stripAnsi('\x1b[32mVITE\x1b[0m ready'), 'VITE ready');
|
|
44
|
+
});
|
|
45
|
+
it('keeps the last n non-trailing-blank lines', () => {
|
|
46
|
+
assert.equal(tailLines('a\nb\nc\n\n\n', 2), 'b\nc');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('restart', () => {
|
|
50
|
+
it('stops the pane, reruns it and waits for readiness', async () => {
|
|
51
|
+
const { exec, state } = makeZellij();
|
|
52
|
+
const { now, sleep } = clock();
|
|
53
|
+
let checks = 0;
|
|
54
|
+
const res = await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async (url) => { assert.equal(url, 'http://localhost:21000/api/ping'); return ++checks >= 3; } });
|
|
55
|
+
assert.deepEqual(state.keys, ['Ctrl c', 'Enter']);
|
|
56
|
+
assert.equal(res.ok, true);
|
|
57
|
+
assert.match(res.message, /ready after/);
|
|
58
|
+
});
|
|
59
|
+
it('reports a pane that ignores Ctrl c instead of pressing Enter on it', async () => {
|
|
60
|
+
const { exec, state } = makeZellij({ stubborn: true });
|
|
61
|
+
const { now, sleep } = clock();
|
|
62
|
+
const res = await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async () => true });
|
|
63
|
+
assert.equal(res.ok, false);
|
|
64
|
+
assert.match(res.message, /did not stop within 10s/);
|
|
65
|
+
assert.deepEqual(state.keys, ['Ctrl c']);
|
|
66
|
+
});
|
|
67
|
+
it('returns the last logs when the process never gets ready', async () => {
|
|
68
|
+
const { exec } = makeZellij();
|
|
69
|
+
const { now, sleep } = clock();
|
|
70
|
+
const res = await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async () => false });
|
|
71
|
+
assert.equal(res.ok, false);
|
|
72
|
+
assert.match(res.message, /not ready after 60s/);
|
|
73
|
+
assert.match(res.logs ?? '', /API crashed/);
|
|
74
|
+
});
|
|
75
|
+
it('reruns an already exited pane without sending Ctrl c', async () => {
|
|
76
|
+
const { exec, state } = makeZellij();
|
|
77
|
+
state.exited = true;
|
|
78
|
+
const { now, sleep } = clock();
|
|
79
|
+
await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async () => true });
|
|
80
|
+
assert.deepEqual(state.keys, ['Enter']);
|
|
81
|
+
});
|
|
82
|
+
it('follows the pane by id when its title changes after Ctrl c', async () => {
|
|
83
|
+
const { exec, state } = makeZellij({ retitle: true });
|
|
84
|
+
const { now, sleep } = clock();
|
|
85
|
+
const res = await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async () => true });
|
|
86
|
+
assert.deepEqual(state.keys, ['Ctrl c', 'Enter']);
|
|
87
|
+
assert.equal(res.ok, true);
|
|
88
|
+
});
|
|
89
|
+
it('keeps the failure message when the logs cannot be read', async () => {
|
|
90
|
+
const { exec } = makeZellij({ dumpFails: true });
|
|
91
|
+
const { now, sleep } = clock();
|
|
92
|
+
const res = await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async () => false });
|
|
93
|
+
assert.match(res.message, /not ready after 60s/);
|
|
94
|
+
assert.match(res.logs ?? '', /logs unavailable: .*ENOBUFS/);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=panes.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panes.test.js","sourceRoot":"","sources":["panes.test.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable no-template-curly-in-string -- ${VAR} placeholders are the dev/dev.json syntax */\nimport { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { stripAnsi, tailLines, restart } from './panes.js'\nimport type { Exec } from './exec.js'\n\nconst config = { ports: [], panes: { api: { ready: 'http://localhost:${DEV_API_PORT}/api/ping' } } }\n\n// a fake zellij whose api pane stops on Ctrl c (unless stubborn) and runs again on Enter\nconst makeZellij = (opts: { stubborn?: boolean, retitle?: boolean, dumpFails?: boolean } = {}) => {\n const state = { exited: false, keys: [] as string[], title: 'api' }\n const exec: Exec = (cmd, args) => {\n if (args[0] === '--version') return 'zellij 0.45.1'\n if (args[0] === 'list-sessions') return 'portals [Created 1m ago]\\n'\n const action = args[3]\n if (action === 'list-panes') return JSON.stringify([{ id: 3, title: state.title, exited: state.exited, exit_status: state.exited ? 0 : null, is_plugin: false, terminal_command: 'node --watch index.ts' }])\n if (action === 'send-keys') {\n const keys = args[6]\n state.keys.push(keys)\n if (keys === 'Ctrl c' && !opts.stubborn) state.exited = true\n if (keys === 'Ctrl c' && opts.retitle) state.title = 'node (exited)'\n if (keys === 'Enter') state.exited = false\n return ''\n }\n if (action === 'dump-screen') {\n if (opts.dumpFails) throw new Error('spawnSync zellij ENOBUFS')\n return 'line1\\nAPI crashed\\n\\n'\n }\n throw new Error('unexpected ' + args.join(' '))\n }\n return { exec, state }\n}\n\nconst clock = () => {\n let t = 0\n return { now: () => t, sleep: async (ms: number) => { t += ms } }\n}\n\ndescribe('logs helpers', () => {\n it('strips ANSI codes', () => {\n assert.equal(stripAnsi('\\x1b[32mVITE\\x1b[0m ready'), 'VITE ready')\n })\n it('keeps the last n non-trailing-blank lines', () => {\n assert.equal(tailLines('a\\nb\\nc\\n\\n\\n', 2), 'b\\nc')\n })\n})\n\ndescribe('restart', () => {\n it('stops the pane, reruns it and waits for readiness', async () => {\n const { exec, state } = makeZellij()\n const { now, sleep } = clock()\n let checks = 0\n const res = await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async (url) => { assert.equal(url, 'http://localhost:21000/api/ping'); return ++checks >= 3 } })\n assert.deepEqual(state.keys, ['Ctrl c', 'Enter'])\n assert.equal(res.ok, true)\n assert.match(res.message, /ready after/)\n })\n\n it('reports a pane that ignores Ctrl c instead of pressing Enter on it', async () => {\n const { exec, state } = makeZellij({ stubborn: true })\n const { now, sleep } = clock()\n const res = await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async () => true })\n assert.equal(res.ok, false)\n assert.match(res.message, /did not stop within 10s/)\n assert.deepEqual(state.keys, ['Ctrl c'])\n })\n\n it('returns the last logs when the process never gets ready', async () => {\n const { exec } = makeZellij()\n const { now, sleep } = clock()\n const res = await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async () => false })\n assert.equal(res.ok, false)\n assert.match(res.message, /not ready after 60s/)\n assert.match(res.logs ?? '', /API crashed/)\n })\n\n it('reruns an already exited pane without sending Ctrl c', async () => {\n const { exec, state } = makeZellij()\n state.exited = true\n const { now, sleep } = clock()\n await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async () => true })\n assert.deepEqual(state.keys, ['Enter'])\n })\n\n it('follows the pane by id when its title changes after Ctrl c', async () => {\n const { exec, state } = makeZellij({ retitle: true })\n const { now, sleep } = clock()\n const res = await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async () => true })\n assert.deepEqual(state.keys, ['Ctrl c', 'Enter'])\n assert.equal(res.ok, true)\n })\n\n it('keeps the failure message when the logs cannot be read', async () => {\n const { exec } = makeZellij({ dumpFails: true })\n const { now, sleep } = clock()\n const res = await restart('/x/portals', 'api', config, { exec, now, sleep, env: { DEV_API_PORT: '21000' }, ready: async () => false })\n assert.match(res.message, /not ready after 60s/)\n assert.match(res.logs ?? '', /logs unavailable: .*ENOBUFS/)\n })\n})\n"]}
|
package/setup.d.ts
ADDED
package/setup.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { initEnv } from './env.js';
|
|
4
|
+
import { run as realRun } from './exec.js';
|
|
5
|
+
export const setup = async (root, config, deps = {}) => {
|
|
6
|
+
const run = deps.run ?? realRun;
|
|
7
|
+
const env = await initEnv(root, config, { isFree: deps.isFree });
|
|
8
|
+
console.log(env.written ? `created ${env.file}` : `kept existing ${env.file} (npx --no df-dev init-env --force to regenerate)`);
|
|
9
|
+
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
10
|
+
// repo-specific steps (native modules…) that the types or the UI build depend on
|
|
11
|
+
for (const command of config.setup ?? [])
|
|
12
|
+
run('bash', ['-c', command], root);
|
|
13
|
+
if (pkg.scripts?.['build-types'])
|
|
14
|
+
run('npm', ['run', 'build-types'], root);
|
|
15
|
+
// the API's serve-spa reads ui/dist/index.html at startup, and the build generates the
|
|
16
|
+
// auto-import declarations check-types needs: build once, later runs keep the existing build
|
|
17
|
+
if (existsSync(path.join(root, 'ui/package.json')) && !existsSync(path.join(root, 'ui/dist/index.html'))) {
|
|
18
|
+
run('npm', ['-w', 'ui', 'run', 'build'], root);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=setup.js.map
|
package/setup.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAClC,OAAO,EAAE,GAAG,IAAI,OAAO,EAAE,MAAM,WAAW,CAAA;AAE1C,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,EAAE,IAAY,EAAE,MAAiB,EAAE,OAA8E,EAAE,EAAE,EAAE;IAC/I,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAA;IAC/B,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;IAChE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,iBAAiB,GAAG,CAAC,IAAI,mDAAmD,CAAC,CAAA;IAC/H,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAAC,CAAA;IAC7E,iFAAiF;IACjF,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,KAAK,IAAI,EAAE;QAAE,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,CAAA;IAC5E,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC;QAAE,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,aAAa,CAAC,EAAE,IAAI,CAAC,CAAA;IAC1E,uFAAuF;IACvF,6FAA6F;IAC7F,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC;QACzG,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,CAAA;IAChD,CAAC;AACH,CAAC,CAAA","sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport path from 'node:path'\nimport type { DevConfig } from './config.js'\nimport { initEnv } from './env.js'\nimport { run as realRun } from './exec.js'\n\nexport const setup = async (root: string, config: DevConfig, deps: { run?: typeof realRun, isFree?: (port: number) => Promise<boolean> } = {}) => {\n const run = deps.run ?? realRun\n const env = await initEnv(root, config, { isFree: deps.isFree })\n console.log(env.written ? `created ${env.file}` : `kept existing ${env.file} (npx --no df-dev init-env --force to regenerate)`)\n const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))\n // repo-specific steps (native modules…) that the types or the UI build depend on\n for (const command of config.setup ?? []) run('bash', ['-c', command], root)\n if (pkg.scripts?.['build-types']) run('npm', ['run', 'build-types'], root)\n // the API's serve-spa reads ui/dist/index.html at startup, and the build generates the\n // auto-import declarations check-types needs: build once, later runs keep the existing build\n if (existsSync(path.join(root, 'ui/package.json')) && !existsSync(path.join(root, 'ui/dist/index.html'))) {\n run('npm', ['-w', 'ui', 'run', 'build'], root)\n }\n}\n"]}
|
package/setup.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/setup.test.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'assert';
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { setup } from './setup.js';
|
|
7
|
+
const makeRepo = (scripts, ui = true) => {
|
|
8
|
+
const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'));
|
|
9
|
+
writeFileSync(path.join(root, 'package.json'), JSON.stringify({ scripts }));
|
|
10
|
+
if (ui) {
|
|
11
|
+
mkdirSync(path.join(root, 'ui'));
|
|
12
|
+
writeFileSync(path.join(root, 'ui/package.json'), '{}');
|
|
13
|
+
}
|
|
14
|
+
return root;
|
|
15
|
+
};
|
|
16
|
+
const config = { ports: ['NGINX_PORT'], panes: {} };
|
|
17
|
+
describe('setup', () => {
|
|
18
|
+
it('creates .env, runs build-types then builds the ui workspace', async () => {
|
|
19
|
+
const root = makeRepo({ 'build-types': 'df-build-types api/types' });
|
|
20
|
+
const calls = [];
|
|
21
|
+
await setup(root, config, { run: (cmd, args) => { calls.push([cmd, ...args].join(' ')); }, isFree: async () => true });
|
|
22
|
+
assert.ok(existsSync(path.join(root, '.env')));
|
|
23
|
+
assert.deepEqual(calls, ['npm run build-types', 'npm -w ui run build']);
|
|
24
|
+
});
|
|
25
|
+
it('does not rebuild an existing UI build and skips build-types when the repo has none', async () => {
|
|
26
|
+
const root = makeRepo({});
|
|
27
|
+
mkdirSync(path.join(root, 'ui/dist'));
|
|
28
|
+
writeFileSync(path.join(root, 'ui/dist/index.html'), '<html>real</html>');
|
|
29
|
+
const calls = [];
|
|
30
|
+
await setup(root, config, { run: (cmd, args) => { calls.push(cmd); }, isFree: async () => true });
|
|
31
|
+
assert.deepEqual(calls, []);
|
|
32
|
+
assert.equal(readFileSync(path.join(root, 'ui/dist/index.html'), 'utf8'), '<html>real</html>');
|
|
33
|
+
});
|
|
34
|
+
it('builds no UI in a repo without a ui workspace', async () => {
|
|
35
|
+
const root = makeRepo({}, false);
|
|
36
|
+
const calls = [];
|
|
37
|
+
await setup(root, config, { run: (cmd, args) => { calls.push(cmd); }, isFree: async () => true });
|
|
38
|
+
assert.deepEqual(calls, []);
|
|
39
|
+
});
|
|
40
|
+
it('runs the repo-specific setup commands before build-types', async () => {
|
|
41
|
+
const root = makeRepo({ 'build-types': 'df-build-types api/types' });
|
|
42
|
+
const calls = [];
|
|
43
|
+
await setup(root, { ...config, setup: ['npm -w parquet-writer run build'] }, { run: (cmd, args) => { calls.push([cmd, ...args].join(' ')); }, isFree: async () => true });
|
|
44
|
+
assert.deepEqual(calls, ['bash -c npm -w parquet-writer run build', 'npm run build-types', 'npm -w ui run build']);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
//# sourceMappingURL=setup.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.test.js","sourceRoot":"","sources":["setup.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\nimport { setup } from './setup.js'\n\nconst makeRepo = (scripts: Record<string, string>, ui = true) => {\n const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'))\n writeFileSync(path.join(root, 'package.json'), JSON.stringify({ scripts }))\n if (ui) {\n mkdirSync(path.join(root, 'ui'))\n writeFileSync(path.join(root, 'ui/package.json'), '{}')\n }\n return root\n}\nconst config = { ports: ['NGINX_PORT'], panes: {} }\n\ndescribe('setup', () => {\n it('creates .env, runs build-types then builds the ui workspace', async () => {\n const root = makeRepo({ 'build-types': 'df-build-types api/types' })\n const calls: string[] = []\n await setup(root, config, { run: (cmd, args) => { calls.push([cmd, ...args].join(' ')) }, isFree: async () => true })\n assert.ok(existsSync(path.join(root, '.env')))\n assert.deepEqual(calls, ['npm run build-types', 'npm -w ui run build'])\n })\n\n it('does not rebuild an existing UI build and skips build-types when the repo has none', async () => {\n const root = makeRepo({})\n mkdirSync(path.join(root, 'ui/dist'))\n writeFileSync(path.join(root, 'ui/dist/index.html'), '<html>real</html>')\n const calls: string[] = []\n await setup(root, config, { run: (cmd, args) => { calls.push(cmd) }, isFree: async () => true })\n assert.deepEqual(calls, [])\n assert.equal(readFileSync(path.join(root, 'ui/dist/index.html'), 'utf8'), '<html>real</html>')\n })\n\n it('builds no UI in a repo without a ui workspace', async () => {\n const root = makeRepo({}, false)\n const calls: string[] = []\n await setup(root, config, { run: (cmd, args) => { calls.push(cmd) }, isFree: async () => true })\n assert.deepEqual(calls, [])\n })\n\n it('runs the repo-specific setup commands before build-types', async () => {\n const root = makeRepo({ 'build-types': 'df-build-types api/types' })\n const calls: string[] = []\n await setup(root, { ...config, setup: ['npm -w parquet-writer run build'] }, { run: (cmd, args) => { calls.push([cmd, ...args].join(' ')) }, isFree: async () => true })\n assert.deepEqual(calls, ['bash -c npm -w parquet-writer run build', 'npm run build-types', 'npm -w ui run build'])\n })\n})\n"]}
|
package/status-pane.sh
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Status pane of the zellij layout: containers (like `docker compose up --wait`) and one RAM line,
|
|
3
|
+
# refreshed every 3 s. Plain bash + awk on purpose: it runs all day in every session, a node
|
|
4
|
+
# process would hold ~55 MB for this. Agents use the one-shot `df-dev status` instead.
|
|
5
|
+
# Usage (from the repo root): df-status [--start-deps] [--once]
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
start_deps=false
|
|
9
|
+
once=false
|
|
10
|
+
for arg in "$@"; do
|
|
11
|
+
case "$arg" in
|
|
12
|
+
--start-deps) start_deps=true ;;
|
|
13
|
+
--once) once=true ;;
|
|
14
|
+
esac
|
|
15
|
+
done
|
|
16
|
+
|
|
17
|
+
session=$(basename "$PWD")
|
|
18
|
+
up_status=$(mktemp)
|
|
19
|
+
# services the background `docker compose up` will start: shown as waiting until it returns
|
|
20
|
+
up_pending=$(mktemp)
|
|
21
|
+
trap 'rm -f "$up_status" "$up_pending"' EXIT
|
|
22
|
+
|
|
23
|
+
# the status pane is the one that starts the containers: in the background, so the view shows them coming up
|
|
24
|
+
if [ "$start_deps" = true ]; then
|
|
25
|
+
docker compose config --services > "$up_pending" 2>/dev/null
|
|
26
|
+
( docker compose up -d >/dev/null 2>"$up_status.err"; code=$?; [ "$code" -ne 0 ] && echo "docker compose up failed ($code): $(tail -1 "$up_status.err")" > "$up_status"; rm -f "$up_status.err"; : > "$up_pending" ) &
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [ -n "${NO_COLOR:-}" ]; then G=; Y=; R=; D=; N=; else G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; D=$'\e[2m'; N=$'\e[0m'; fi
|
|
30
|
+
|
|
31
|
+
# the compose file does not change while the session runs: parse it once, not every 3 s
|
|
32
|
+
services=$(docker compose --profile '*' config --services 2>/dev/null | sort)
|
|
33
|
+
|
|
34
|
+
render() {
|
|
35
|
+
local ps names stats procs
|
|
36
|
+
ps=$(docker compose ps -a --format '{{.Service}}\t{{.State}}\t{{.Health}}\t{{.ExitCode}}\t{{.Name}}' 2>/dev/null)
|
|
37
|
+
names=$(printf '%s\n' "$ps" | awk -F'\t' '$2 == "running" { print $5 }')
|
|
38
|
+
stats=""
|
|
39
|
+
# shellcheck disable=SC2086
|
|
40
|
+
[ -n "$names" ] && stats=$(docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}' $names 2>/dev/null)
|
|
41
|
+
procs=$(ps -u "$(id -u)" -o pid=,ppid=,rss=,args= 2>/dev/null)
|
|
42
|
+
|
|
43
|
+
[ -s "$up_status" ] && printf '%s%s%s\n\n' "$R" "$(cat "$up_status")" "$N"
|
|
44
|
+
|
|
45
|
+
# data goes through the environment: awk -v would interpret the backslashes of the ps arguments
|
|
46
|
+
# LC_ALL=C: sous locale fr, awk lit "1.5GiB" comme 1 et écrit "1,5G" (séparateur décimal)
|
|
47
|
+
S_SERVICES="$services" S_PS="$ps" S_STATS="$stats" S_PROCS="$procs" S_PENDING="$(cat "$up_pending")" \
|
|
48
|
+
LC_ALL=C awk -F'\t' -v G="$G" -v Y="$Y" -v R="$R" -v D="$D" -v N="$N" -v session="$session" -v self="$$" '
|
|
49
|
+
function kib(v, n, u) {
|
|
50
|
+
n = v + 0; u = v; sub(/^[0-9.]+ */, "", u)
|
|
51
|
+
if (u == "GiB") return n * 1048576; if (u == "MiB") return n * 1024; if (u == "KiB") return n
|
|
52
|
+
if (u == "GB") return n * 976562.5; if (u == "MB") return n * 976.5625; if (u == "kB") return n * 0.9765625
|
|
53
|
+
return 0
|
|
54
|
+
}
|
|
55
|
+
function human(k) { return k >= 1048576 ? sprintf("%.1fG", k / 1048576) : sprintf("%dM", k / 1024 + 0.5) }
|
|
56
|
+
BEGIN {
|
|
57
|
+
services = ENVIRON["S_SERVICES"]; ps = ENVIRON["S_PS"]; stats = ENVIRON["S_STATS"]; procs = ENVIRON["S_PROCS"]
|
|
58
|
+
nps = split(ps, rows, "\n")
|
|
59
|
+
for (i = 1; i <= nps; i++) if (split(rows[i], f, "\t") >= 5) { state[f[1]] = f[2]; health[f[1]] = f[3]; code[f[1]] = f[4]; cname[f[1]] = f[5]; if (f[2] == "running") running = 1 }
|
|
60
|
+
npe = split(ENVIRON["S_PENDING"], pe, "\n")
|
|
61
|
+
for (i = 1; i <= npe; i++) pending[pe[i]] = 1
|
|
62
|
+
nst = split(stats, srows, "\n")
|
|
63
|
+
for (i = 1; i <= nst; i++) if (split(srows[i], f, "\t") >= 2) { split(f[2], m, " / "); mem[f[1]] = m[1] }
|
|
64
|
+
|
|
65
|
+
nsv = split(services, svc, "\n")
|
|
66
|
+
for (i = 1; i <= nsv; i++) {
|
|
67
|
+
s = svc[i]; if (s == "") continue
|
|
68
|
+
if (!(s in state)) { printf "%s· %-18s not started%s\n", D, s, N; continue }
|
|
69
|
+
st = state[s]; h = health[s]; text = (h != "" ? h : st)
|
|
70
|
+
# a one-shot init container (s3-init) that completed, unless the whole stack is down: a stopped mongo exits with 0 too
|
|
71
|
+
if (st == "exited" && code[s] == "0" && running) { printf "%s✔%s %-18s %sdone%s\n", G, N, s, G, N; continue }
|
|
72
|
+
if (st == "exited" && code[s] == "0") { printf "%s· %-18s stopped%s\n", D, s, N; continue }
|
|
73
|
+
if (st != "running" && (s in pending)) { printf "%s⠿%s %-18s %swaiting%s\n", Y, N, s, Y, N; continue }
|
|
74
|
+
if (st == "exited") text = "exited " code[s]
|
|
75
|
+
if (text == "healthy" || (st == "running" && h == "")) { c = G; mark = "✔" }
|
|
76
|
+
else if (text == "starting" || st == "restarting" || st == "created") { c = Y; mark = "⠿" }
|
|
77
|
+
else { c = R; mark = "✘" }
|
|
78
|
+
k = (cname[s] in mem) ? kib(mem[cname[s]]) : 0
|
|
79
|
+
containers += k
|
|
80
|
+
line = sprintf("%s%s%s %-18s %s%-11s%s %s", c, mark, N, s, c, text, N, (k ? human(k) : ""))
|
|
81
|
+
sub(/ +$/, "", line); print line
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# dev servers: descendants of this session zellij server, without this status script
|
|
85
|
+
npr = split(procs, prows, "\n")
|
|
86
|
+
for (i = 1; i <= npr; i++) {
|
|
87
|
+
if (split(prows[i], f, " ") < 4) continue
|
|
88
|
+
pid[i] = f[1]; ppid[i] = f[2]; rss[i] = f[3]
|
|
89
|
+
if (prows[i] ~ / --server / && prows[i] ~ ("/" session "$")) server = f[1]
|
|
90
|
+
}
|
|
91
|
+
if (server != "") {
|
|
92
|
+
insession[server] = 1; out[self] = 1
|
|
93
|
+
do {
|
|
94
|
+
grew = 0
|
|
95
|
+
for (i in pid) {
|
|
96
|
+
if (!(pid[i] in insession) && (ppid[i] in insession)) { insession[pid[i]] = 1; grew = 1 }
|
|
97
|
+
if (!(pid[i] in out) && (ppid[i] in out)) { out[pid[i]] = 1; grew = 1 }
|
|
98
|
+
}
|
|
99
|
+
} while (grew)
|
|
100
|
+
for (i in pid) if (pid[i] != server && (pid[i] in insession) && !(pid[i] in out)) devservers += rss[i]
|
|
101
|
+
}
|
|
102
|
+
printf "\nRAM containers %s · dev servers %s · total %s\n", human(containers), human(devservers), human(containers + devservers)
|
|
103
|
+
}'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
while true; do
|
|
107
|
+
view=$(render)
|
|
108
|
+
# also clear the scrollback, or the pane history grows by a screen every refresh
|
|
109
|
+
printf '\e[H\e[2J\e[3J%s\n' "$view"
|
|
110
|
+
[ "$once" = true ] && exit 0
|
|
111
|
+
sleep 3
|
|
112
|
+
done
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'assert';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
// fake docker and ps on the PATH: the status pane script only reads their output
|
|
8
|
+
const RUNNING_PS = 'mongo\\trunning\\thealthy\\t0\\tp-mongo-1\\nes\\trunning\\tstarting\\t0\\tp-es-1\\ns3-init\\texited\\t\\t0\\tp-s3-init-1\\nsd\\texited\\t\\t1\\tp-sd-1\\n';
|
|
9
|
+
const fakeBin = (session, psOut = RUNNING_PS, log = '/dev/null') => {
|
|
10
|
+
const bin = mkdtempSync(path.join(tmpdir(), 'fakebin-'));
|
|
11
|
+
const write = (name, body) => { writeFileSync(path.join(bin, name), '#!/usr/bin/env bash\n' + body); chmodSync(path.join(bin, name), 0o755); };
|
|
12
|
+
write('docker', `echo "$*" >> ${log}
|
|
13
|
+
case "$*" in
|
|
14
|
+
*"config --services"*) printf 'agents\\nmongo\\ns3-init\\nes\\nsd\\n' ;;
|
|
15
|
+
*"up -d"*) sleep 1 ;;
|
|
16
|
+
*"compose ps"*) printf '${psOut}' ;;
|
|
17
|
+
*stats*) printf 'p-mongo-1\\t512MiB / 31GiB\\np-es-1\\t1.5GiB / 31GiB\\n' ;;
|
|
18
|
+
esac`);
|
|
19
|
+
write('ps', `printf '%s\\n' \\
|
|
20
|
+
' 100 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/${session}' \\
|
|
21
|
+
' 101 100 1048576 node --watch index.ts' \\
|
|
22
|
+
' 102 101 524288 node index.ts' \\
|
|
23
|
+
' 200 1 900000 claude'`);
|
|
24
|
+
return bin;
|
|
25
|
+
};
|
|
26
|
+
describe('status pane (bash)', () => {
|
|
27
|
+
it('renders containers like compose up --wait and one RAM line', () => {
|
|
28
|
+
const root = path.join(mkdtempSync(path.join(tmpdir(), 'repo-')), 'portals_x');
|
|
29
|
+
mkdirSync(root);
|
|
30
|
+
const res = spawnSync('bash', [path.join(import.meta.dirname, 'status-pane.sh'), '--once'], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: fakeBin('portals_x') + ':' + process.env.PATH, NO_COLOR: '1' } });
|
|
31
|
+
assert.equal(res.status, 0, res.stderr);
|
|
32
|
+
// eslint-disable-next-line no-control-regex -- strips the ANSI codes of the pane output
|
|
33
|
+
const lines = res.stdout.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').split('\n').filter(Boolean);
|
|
34
|
+
assert.deepEqual(lines, [
|
|
35
|
+
'· agents not started',
|
|
36
|
+
'⠿ es starting 1.5G',
|
|
37
|
+
'✔ mongo healthy 512M',
|
|
38
|
+
'✔ s3-init done',
|
|
39
|
+
'✘ sd exited 1',
|
|
40
|
+
'RAM containers 2.0G · dev servers 1.5G · total 3.5G'
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
it('shows the containers the background compose up has not started yet as waiting, not failed', () => {
|
|
44
|
+
const root = path.join(mkdtempSync(path.join(tmpdir(), 'repo-')), 'portals_x');
|
|
45
|
+
mkdirSync(root);
|
|
46
|
+
const res = spawnSync('bash', [path.join(import.meta.dirname, 'status-pane.sh'), '--once', '--start-deps'], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: fakeBin('portals_x') + ':' + process.env.PATH, NO_COLOR: '1' } });
|
|
47
|
+
assert.equal(res.status, 0, res.stderr);
|
|
48
|
+
assert.match(res.stdout, /⠿ sd\s+waiting/);
|
|
49
|
+
assert.match(res.stdout, /✔ s3-init\s+done/);
|
|
50
|
+
});
|
|
51
|
+
it('shows a stack stopped with exit code 0 as stopped, not done', () => {
|
|
52
|
+
const root = path.join(mkdtempSync(path.join(tmpdir(), 'repo-')), 'portals_x');
|
|
53
|
+
mkdirSync(root);
|
|
54
|
+
const stopped = 'mongo\\texited\\t\\t0\\tp-mongo-1\\ns3-init\\texited\\t\\t0\\tp-s3-init-1\\n';
|
|
55
|
+
const res = spawnSync('bash', [path.join(import.meta.dirname, 'status-pane.sh'), '--once'], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: fakeBin('portals_x', stopped) + ':' + process.env.PATH, NO_COLOR: '1' } });
|
|
56
|
+
assert.equal(res.status, 0, res.stderr);
|
|
57
|
+
assert.match(res.stdout, /· mongo\s+stopped/);
|
|
58
|
+
assert.match(res.stdout, /· s3-init\s+stopped/);
|
|
59
|
+
assert.ok(!res.stdout.includes('done'));
|
|
60
|
+
});
|
|
61
|
+
it('parses the compose file once, not at every refresh', () => {
|
|
62
|
+
const root = path.join(mkdtempSync(path.join(tmpdir(), 'repo-')), 'portals_x');
|
|
63
|
+
mkdirSync(root);
|
|
64
|
+
const log = path.join(root, 'docker.log');
|
|
65
|
+
spawnSync('timeout', ['4', 'bash', path.join(import.meta.dirname, 'status-pane.sh')], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: fakeBin('portals_x', RUNNING_PS, log) + ':' + process.env.PATH, NO_COLOR: '1' } });
|
|
66
|
+
const calls = readFileSync(log, 'utf8').split('\n');
|
|
67
|
+
assert.ok(calls.filter(c => c.includes('compose ps')).length >= 2, 'refreshed at least twice');
|
|
68
|
+
assert.equal(calls.filter(c => c.includes('config --services')).length, 1);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=status-pane.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-pane.test.js","sourceRoot":"","sources":["status-pane.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { spawnSync } from 'node:child_process'\nimport { mkdtempSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\n\n// fake docker and ps on the PATH: the status pane script only reads their output\nconst RUNNING_PS = 'mongo\\\\trunning\\\\thealthy\\\\t0\\\\tp-mongo-1\\\\nes\\\\trunning\\\\tstarting\\\\t0\\\\tp-es-1\\\\ns3-init\\\\texited\\\\t\\\\t0\\\\tp-s3-init-1\\\\nsd\\\\texited\\\\t\\\\t1\\\\tp-sd-1\\\\n'\nconst fakeBin = (session: string, psOut = RUNNING_PS, log = '/dev/null') => {\n const bin = mkdtempSync(path.join(tmpdir(), 'fakebin-'))\n const write = (name: string, body: string) => { writeFileSync(path.join(bin, name), '#!/usr/bin/env bash\\n' + body); chmodSync(path.join(bin, name), 0o755) }\n write('docker', `echo \"$*\" >> ${log}\ncase \"$*\" in\n *\"config --services\"*) printf 'agents\\\\nmongo\\\\ns3-init\\\\nes\\\\nsd\\\\n' ;;\n *\"up -d\"*) sleep 1 ;;\n *\"compose ps\"*) printf '${psOut}' ;;\n *stats*) printf 'p-mongo-1\\\\t512MiB / 31GiB\\\\np-es-1\\\\t1.5GiB / 31GiB\\\\n' ;;\nesac`)\n write('ps', `printf '%s\\\\n' \\\\\n ' 100 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/${session}' \\\\\n ' 101 100 1048576 node --watch index.ts' \\\\\n ' 102 101 524288 node index.ts' \\\\\n ' 200 1 900000 claude'`)\n return bin\n}\n\ndescribe('status pane (bash)', () => {\n it('renders containers like compose up --wait and one RAM line', () => {\n const root = path.join(mkdtempSync(path.join(tmpdir(), 'repo-')), 'portals_x')\n mkdirSync(root)\n const res = spawnSync('bash', [path.join(import.meta.dirname, 'status-pane.sh'), '--once'], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: fakeBin('portals_x') + ':' + process.env.PATH, NO_COLOR: '1' } })\n assert.equal(res.status, 0, res.stderr)\n // eslint-disable-next-line no-control-regex -- strips the ANSI codes of the pane output\n const lines = res.stdout.replace(/\\x1b\\[[0-9;]*[A-Za-z]/g, '').split('\\n').filter(Boolean)\n assert.deepEqual(lines, [\n '· agents not started',\n '⠿ es starting 1.5G',\n '✔ mongo healthy 512M',\n '✔ s3-init done',\n '✘ sd exited 1',\n 'RAM containers 2.0G · dev servers 1.5G · total 3.5G'\n ])\n })\n\n it('shows the containers the background compose up has not started yet as waiting, not failed', () => {\n const root = path.join(mkdtempSync(path.join(tmpdir(), 'repo-')), 'portals_x')\n mkdirSync(root)\n const res = spawnSync('bash', [path.join(import.meta.dirname, 'status-pane.sh'), '--once', '--start-deps'], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: fakeBin('portals_x') + ':' + process.env.PATH, NO_COLOR: '1' } })\n assert.equal(res.status, 0, res.stderr)\n assert.match(res.stdout, /⠿ sd\\s+waiting/)\n assert.match(res.stdout, /✔ s3-init\\s+done/)\n })\n\n it('shows a stack stopped with exit code 0 as stopped, not done', () => {\n const root = path.join(mkdtempSync(path.join(tmpdir(), 'repo-')), 'portals_x')\n mkdirSync(root)\n const stopped = 'mongo\\\\texited\\\\t\\\\t0\\\\tp-mongo-1\\\\ns3-init\\\\texited\\\\t\\\\t0\\\\tp-s3-init-1\\\\n'\n const res = spawnSync('bash', [path.join(import.meta.dirname, 'status-pane.sh'), '--once'], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: fakeBin('portals_x', stopped) + ':' + process.env.PATH, NO_COLOR: '1' } })\n assert.equal(res.status, 0, res.stderr)\n assert.match(res.stdout, /· mongo\\s+stopped/)\n assert.match(res.stdout, /· s3-init\\s+stopped/)\n assert.ok(!res.stdout.includes('done'))\n })\n\n it('parses the compose file once, not at every refresh', () => {\n const root = path.join(mkdtempSync(path.join(tmpdir(), 'repo-')), 'portals_x')\n mkdirSync(root)\n const log = path.join(root, 'docker.log')\n spawnSync('timeout', ['4', 'bash', path.join(import.meta.dirname, 'status-pane.sh')], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: fakeBin('portals_x', RUNNING_PS, log) + ':' + process.env.PATH, NO_COLOR: '1' } })\n const calls = readFileSync(log, 'utf8').split('\\n')\n assert.ok(calls.filter(c => c.includes('compose ps')).length >= 2, 'refreshed at least twice')\n assert.equal(calls.filter(c => c.includes('config --services')).length, 1)\n })\n})\n"]}
|
package/status.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Exec } from './exec.js';
|
|
2
|
+
import { type DevConfig } from './config.js';
|
|
3
|
+
export type Container = {
|
|
4
|
+
service: string;
|
|
5
|
+
state: string;
|
|
6
|
+
health: string;
|
|
7
|
+
memory?: string;
|
|
8
|
+
exitCode?: number;
|
|
9
|
+
};
|
|
10
|
+
export type Proc = {
|
|
11
|
+
label: string;
|
|
12
|
+
rssKb: number;
|
|
13
|
+
};
|
|
14
|
+
export type PaneState = {
|
|
15
|
+
title: string;
|
|
16
|
+
exited: boolean;
|
|
17
|
+
exit_status: number | null;
|
|
18
|
+
ready?: boolean;
|
|
19
|
+
readyUrl?: string;
|
|
20
|
+
};
|
|
21
|
+
export type StatusData = {
|
|
22
|
+
url?: string;
|
|
23
|
+
containers: Container[];
|
|
24
|
+
containersError?: string;
|
|
25
|
+
panes: PaneState[];
|
|
26
|
+
panesError?: string;
|
|
27
|
+
procs: Proc[];
|
|
28
|
+
};
|
|
29
|
+
export declare const containerMark: (c: Container, stackRunning?: boolean) => {
|
|
30
|
+
mark: string;
|
|
31
|
+
color: "dim";
|
|
32
|
+
text: string;
|
|
33
|
+
} | {
|
|
34
|
+
mark: string;
|
|
35
|
+
color: "green";
|
|
36
|
+
text: string;
|
|
37
|
+
} | {
|
|
38
|
+
mark: string;
|
|
39
|
+
color: "yellow";
|
|
40
|
+
text: string;
|
|
41
|
+
} | {
|
|
42
|
+
mark: string;
|
|
43
|
+
color: "red";
|
|
44
|
+
text: string;
|
|
45
|
+
};
|
|
46
|
+
export declare const renderStatus: (data: StatusData, color: boolean) => string;
|
|
47
|
+
/** Processes started by the panes of a zellij session: descendants of its server, shells excluded. */
|
|
48
|
+
export declare const sessionProcesses: (psOut: string, session: string, cwdOf: (pid: number) => string | undefined, root: string, selfPid?: number) => Proc[];
|
|
49
|
+
export declare const collectStatus: (root: string, exec?: Exec) => StatusData;
|
|
50
|
+
/** A running pane can be broken: nodemon (or node --watch) stays alive "waiting for file changes" after a crash. */
|
|
51
|
+
export declare const checkReadiness: (panes: PaneState[], config: DevConfig, env: Record<string, string>, ready?: (url: string) => Promise<boolean>) => Promise<PaneState[]>;
|