@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.
Files changed (81) hide show
  1. package/README.md +38 -2
  2. package/cli.d.ts +2 -0
  3. package/cli.js +64 -0
  4. package/cli.js.map +1 -0
  5. package/cli.test.d.ts +1 -0
  6. package/cli.test.js +44 -0
  7. package/cli.test.js.map +1 -0
  8. package/compose.d.ts +24 -0
  9. package/compose.js +61 -0
  10. package/compose.js.map +1 -0
  11. package/compose.test.d.ts +1 -0
  12. package/compose.test.js +69 -0
  13. package/compose.test.js.map +1 -0
  14. package/config.d.ts +14 -0
  15. package/config.js +32 -0
  16. package/config.js.map +1 -0
  17. package/config.test.d.ts +1 -0
  18. package/config.test.js +44 -0
  19. package/config.test.js.map +1 -0
  20. package/env.d.ts +18 -0
  21. package/env.js +94 -0
  22. package/env.js.map +1 -0
  23. package/env.test.d.ts +1 -0
  24. package/env.test.js +106 -0
  25. package/env.test.js.map +1 -0
  26. package/exec.d.ts +5 -0
  27. package/exec.js +11 -0
  28. package/exec.js.map +1 -0
  29. package/exec.test.d.ts +1 -0
  30. package/exec.test.js +10 -0
  31. package/exec.test.js.map +1 -0
  32. package/launch.d.ts +13 -0
  33. package/launch.js +36 -0
  34. package/launch.js.map +1 -0
  35. package/launch.test.d.ts +1 -0
  36. package/launch.test.js +19 -0
  37. package/launch.test.js.map +1 -0
  38. package/mem-cap.sh +33 -0
  39. package/mem-cap.test.d.ts +1 -0
  40. package/mem-cap.test.js +38 -0
  41. package/mem-cap.test.js.map +1 -0
  42. package/optional.d.ts +8 -0
  43. package/optional.js +21 -0
  44. package/optional.js.map +1 -0
  45. package/optional.test.d.ts +1 -0
  46. package/optional.test.js +24 -0
  47. package/optional.test.js.map +1 -0
  48. package/package.json +29 -4
  49. package/panes.d.ts +18 -0
  50. package/panes.js +68 -0
  51. package/panes.js.map +1 -0
  52. package/panes.test.d.ts +1 -0
  53. package/panes.test.js +97 -0
  54. package/panes.test.js.map +1 -0
  55. package/setup.d.ts +6 -0
  56. package/setup.js +21 -0
  57. package/setup.js.map +1 -0
  58. package/setup.test.d.ts +1 -0
  59. package/setup.test.js +47 -0
  60. package/setup.test.js.map +1 -0
  61. package/status-pane.sh +112 -0
  62. package/status-pane.test.d.ts +1 -0
  63. package/status-pane.test.js +71 -0
  64. package/status-pane.test.js.map +1 -0
  65. package/status.d.ts +51 -0
  66. package/status.js +147 -0
  67. package/status.js.map +1 -0
  68. package/status.test.d.ts +1 -0
  69. package/status.test.js +123 -0
  70. package/status.test.js.map +1 -0
  71. package/worktree-delete.sh +141 -0
  72. package/worktree.sh +164 -0
  73. package/worktree.test.d.ts +1 -0
  74. package/worktree.test.js +145 -0
  75. package/worktree.test.js.map +1 -0
  76. package/zellij.d.ts +19 -0
  77. package/zellij.js +50 -0
  78. package/zellij.js.map +1 -0
  79. package/zellij.test.d.ts +1 -0
  80. package/zellij.test.js +50 -0
  81. 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"]}
@@ -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
@@ -0,0 +1,6 @@
1
+ import type { DevConfig } from './config.js';
2
+ import { run as realRun } from './exec.js';
3
+ export declare const setup: (root: string, config: DevConfig, deps?: {
4
+ run?: typeof realRun;
5
+ isFree?: (port: number) => Promise<boolean>;
6
+ }) => Promise<void>;
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"]}
@@ -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[]>;