@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/status.js ADDED
@@ -0,0 +1,147 @@
1
+ import { readlinkSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { realExec, jsonLines } from './exec.js';
4
+ import { sessionName, sessionState, listPanes } from './zellij.js';
5
+ import { readDevConfig, readEnvFile, interpolate } from './config.js';
6
+ import { checkReady } from './panes.js';
7
+ const COLORS = { green: '32', yellow: '33', red: '31', dim: '2' };
8
+ export const containerMark = (c, stackRunning = true) => {
9
+ if (c.state === 'not started')
10
+ return { mark: '·', color: 'dim', text: 'not started' };
11
+ if (c.state === 'exited' && c.exitCode === 0) {
12
+ // a one-shot init container (s3-init) that completed, unless the whole stack is down: a stopped mongo exits with 0 too
13
+ return stackRunning ? { mark: '✔', color: 'green', text: 'done' } : { mark: '·', color: 'dim', text: 'stopped' };
14
+ }
15
+ const text = c.health || c.state;
16
+ if (text === 'healthy' || (c.state === 'running' && !c.health))
17
+ return { mark: '✔', color: 'green', text };
18
+ if (text === 'starting' || c.state === 'restarting' || c.state === 'created')
19
+ return { mark: '⠿', color: 'yellow', text };
20
+ return { mark: '✘', color: 'red', text };
21
+ };
22
+ const human = (kb) => kb >= 1048576 ? `${(kb / 1048576).toFixed(1)}G` : `${Math.round(kb / 1024)}M`;
23
+ export const renderStatus = (data, color) => {
24
+ const paint = (c, s) => color ? `\x1b[${COLORS[c]}m${s}\x1b[0m` : s;
25
+ const lines = data.url ? [data.url, ''] : [];
26
+ const stackRunning = data.containers.some(c => c.state === 'running');
27
+ lines.push(...data.containersError
28
+ ? [paint('red', `containers unavailable: ${data.containersError}`)]
29
+ : data.containers.map(c => {
30
+ const m = containerMark(c, stackRunning);
31
+ return `${paint(m.color, m.mark)} ${c.service.padEnd(18)} ${paint(m.color, m.text.padEnd(11))} ${c.memory ?? ''}`.trimEnd();
32
+ }));
33
+ lines.push('');
34
+ if (data.panesError)
35
+ lines.push(paint('red', data.panesError));
36
+ for (const p of data.panes) {
37
+ if (p.exited)
38
+ lines.push(`${paint('red', '✘')} ${p.title.padEnd(18)} ${paint('red', `exited (${p.exit_status ?? '?'})`)}`);
39
+ else if (p.ready === false)
40
+ lines.push(`${paint('yellow', '⠿')} ${p.title.padEnd(18)} ${paint('yellow', `running, not ready (${p.readyUrl})`)}`);
41
+ else
42
+ lines.push(`${paint('green', '✔')} ${p.title.padEnd(18)} ${paint('green', 'running')}`);
43
+ }
44
+ lines.push('');
45
+ for (const p of data.procs)
46
+ lines.push(`${p.label.slice(0, 40).padEnd(40)} ${human(p.rssKb)}`);
47
+ lines.push(`${'processes total'.padEnd(40)} ${human(data.procs.reduce((sum, p) => sum + p.rssKb, 0))}`);
48
+ return lines.join('\n');
49
+ };
50
+ const SHELLS = ['bash', 'sh', 'zsh'];
51
+ /** Processes started by the panes of a zellij session: descendants of its server, shells excluded. */
52
+ export const sessionProcesses = (psOut, session, cwdOf, root, selfPid) => {
53
+ const rows = psOut.split('\n').map(line => line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/)).filter(m => !!m)
54
+ .map(m => ({ pid: Number(m[1]), ppid: Number(m[2]), rssKb: Number(m[3]), args: m[4] }));
55
+ const server = rows.find(r => / --server \S*\/zellij\/\S+\//.test(r.args) && r.args.endsWith('/' + session));
56
+ if (!server)
57
+ return [];
58
+ const inSession = new Set([server.pid]);
59
+ // the status commands (this one, the df-status pane) and their children (ps, sleep, docker) are tooling, not dev processes
60
+ const self = new Set([...(selfPid ? [selfPid] : []), ...rows.filter(r => / df-dev status\b|df-status\b/.test(' ' + r.args.replace(/^\S*node\s+\S*\//, ' '))).map(r => r.pid)]);
61
+ let grew = true;
62
+ while (grew) { // ps order is not parents-first when pids wrap around
63
+ grew = false;
64
+ for (const r of rows) {
65
+ if (!inSession.has(r.pid) && inSession.has(r.ppid)) {
66
+ inSession.add(r.pid);
67
+ grew = true;
68
+ }
69
+ if (!self.has(r.pid) && self.has(r.ppid)) {
70
+ self.add(r.pid);
71
+ grew = true;
72
+ }
73
+ }
74
+ }
75
+ const procs = [];
76
+ for (const r of rows) {
77
+ if (r.pid === server.pid || !inSession.has(r.pid) || self.has(r.pid) || SHELLS.includes(path.basename(r.args.split(' ')[0])))
78
+ continue;
79
+ const cwd = cwdOf(r.pid);
80
+ const dir = cwd && (cwd === root || cwd.startsWith(root + path.sep)) ? (path.relative(root, cwd) || '.') : '?';
81
+ procs.push({ label: `${dir}: ${r.args.replace(/^(\S*\/)?node\s+(?=\S*node_modules\/\.bin\/)/, '').replace(/^\S*node_modules\/\.bin\//, '').replace(/^\S*\//, '')}`, rssKb: r.rssKb });
82
+ }
83
+ return procs;
84
+ };
85
+ const collectContainers = (root, exec) => {
86
+ const ps = jsonLines(exec('docker', ['compose', '--project-directory', root, 'ps', '-a', '--format', 'json']));
87
+ const all = exec('docker', ['compose', '--project-directory', root, '--profile', '*', 'config', '--services']).split('\n').filter(Boolean);
88
+ const memory = {};
89
+ if (ps.length) {
90
+ for (const s of jsonLines(exec('docker', ['stats', '--no-stream', '--format', '{{json .}}', ...ps.map((c) => c.Name)]))) {
91
+ memory[s.Name] = String(s.MemUsage).split(' / ')[0];
92
+ }
93
+ }
94
+ return all.sort().map(service => {
95
+ const c = ps.find((p) => p.Service === service);
96
+ return c ? { service, state: c.State, health: c.Health ?? '', memory: memory[c.Name], exitCode: c.ExitCode } : { service, state: 'not started', health: '' };
97
+ });
98
+ };
99
+ // each part fails on its own: a docker error must not hide the panes, and the reverse
100
+ export const collectStatus = (root, exec = realExec) => {
101
+ const data = { containers: [], panes: [], procs: [] };
102
+ try {
103
+ const env = readEnvFile(root);
104
+ const url = readDevConfig(root).url;
105
+ if (url)
106
+ data.url = interpolate(url, env);
107
+ }
108
+ catch { } // the URL is a convenience, the rest of the status must still show
109
+ try {
110
+ data.containers = collectContainers(root, exec);
111
+ }
112
+ catch (err) {
113
+ data.containersError = err.message.split('\n')[0];
114
+ }
115
+ const session = sessionName(root);
116
+ try {
117
+ const state = sessionState(exec, session);
118
+ if (state === 'running')
119
+ data.panes = listPanes(exec, session).map(p => ({ title: p.title, exited: p.exited, exit_status: p.exit_status }));
120
+ else
121
+ data.panesError = `zellij session "${session}" is not running (${state}): ask the user to start it with "npm run dev-zellij"`;
122
+ }
123
+ catch (err) {
124
+ data.panesError = `panes unavailable: ${err.message.split('\n')[0]}`;
125
+ }
126
+ const cwdOf = (pid) => { try {
127
+ return readlinkSync(`/proc/${pid}/cwd`);
128
+ }
129
+ catch {
130
+ return undefined;
131
+ } };
132
+ try {
133
+ const user = process.getuid ? ['-u', String(process.getuid())] : ['-e'];
134
+ data.procs = sessionProcesses(exec('ps', [...user, '-o', 'pid=,ppid=,rss=,args=']), session, cwdOf, root, process.pid);
135
+ }
136
+ catch { } // the process list is the least important part
137
+ return data;
138
+ };
139
+ /** A running pane can be broken: nodemon (or node --watch) stays alive "waiting for file changes" after a crash. */
140
+ export const checkReadiness = async (panes, config, env, ready = checkReady) => Promise.all(panes.map(async (p) => {
141
+ const template = config.panes[p.title]?.ready;
142
+ if (p.exited || !template)
143
+ return p;
144
+ const readyUrl = interpolate(template, env);
145
+ return { ...p, ready: await ready(readyUrl), readyUrl };
146
+ }));
147
+ //# sourceMappingURL=status.js.map
package/status.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["status.ts"],"names":[],"mappings":"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","sourcesContent":["import { readlinkSync } from 'node:fs'\nimport path from 'node:path'\nimport { realExec, jsonLines, type Exec } from './exec.js'\nimport { sessionName, sessionState, listPanes } from './zellij.js'\nimport { readDevConfig, readEnvFile, interpolate, type DevConfig } from './config.js'\nimport { checkReady } from './panes.js'\n\nexport type Container = { service: string, state: string, health: string, memory?: string, exitCode?: number }\nexport type Proc = { label: string, rssKb: number }\nexport type PaneState = { title: string, exited: boolean, exit_status: number | null, ready?: boolean, readyUrl?: string }\nexport type StatusData = { url?: string, containers: Container[], containersError?: string, panes: PaneState[], panesError?: string, procs: Proc[] }\n\nconst COLORS = { green: '32', yellow: '33', red: '31', dim: '2' } as const\n\nexport const containerMark = (c: Container, stackRunning = true) => {\n if (c.state === 'not started') return { mark: '·', color: 'dim' as const, text: 'not started' }\n if (c.state === 'exited' && c.exitCode === 0) {\n // a one-shot init container (s3-init) that completed, unless the whole stack is down: a stopped mongo exits with 0 too\n return stackRunning ? { mark: '✔', color: 'green' as const, text: 'done' } : { mark: '·', color: 'dim' as const, text: 'stopped' }\n }\n const text = c.health || c.state\n if (text === 'healthy' || (c.state === 'running' && !c.health)) return { mark: '✔', color: 'green' as const, text }\n if (text === 'starting' || c.state === 'restarting' || c.state === 'created') return { mark: '⠿', color: 'yellow' as const, text }\n return { mark: '✘', color: 'red' as const, text }\n}\n\nconst human = (kb: number) => kb >= 1048576 ? `${(kb / 1048576).toFixed(1)}G` : `${Math.round(kb / 1024)}M`\n\nexport const renderStatus = (data: StatusData, color: boolean) => {\n const paint = (c: keyof typeof COLORS, s: string) => color ? `\\x1b[${COLORS[c]}m${s}\\x1b[0m` : s\n const lines = data.url ? [data.url, ''] : []\n const stackRunning = data.containers.some(c => c.state === 'running')\n lines.push(...data.containersError\n ? [paint('red', `containers unavailable: ${data.containersError}`)]\n : data.containers.map(c => {\n const m = containerMark(c, stackRunning)\n return `${paint(m.color, m.mark)} ${c.service.padEnd(18)} ${paint(m.color, m.text.padEnd(11))} ${c.memory ?? ''}`.trimEnd()\n }))\n lines.push('')\n if (data.panesError) lines.push(paint('red', data.panesError))\n for (const p of data.panes) {\n if (p.exited) lines.push(`${paint('red', '✘')} ${p.title.padEnd(18)} ${paint('red', `exited (${p.exit_status ?? '?'})`)}`)\n else if (p.ready === false) lines.push(`${paint('yellow', '⠿')} ${p.title.padEnd(18)} ${paint('yellow', `running, not ready (${p.readyUrl})`)}`)\n else lines.push(`${paint('green', '✔')} ${p.title.padEnd(18)} ${paint('green', 'running')}`)\n }\n lines.push('')\n for (const p of data.procs) lines.push(`${p.label.slice(0, 40).padEnd(40)} ${human(p.rssKb)}`)\n lines.push(`${'processes total'.padEnd(40)} ${human(data.procs.reduce((sum, p) => sum + p.rssKb, 0))}`)\n return lines.join('\\n')\n}\n\nconst SHELLS = ['bash', 'sh', 'zsh']\n\n/** Processes started by the panes of a zellij session: descendants of its server, shells excluded. */\nexport const sessionProcesses = (psOut: string, session: string, cwdOf: (pid: number) => string | undefined, root: string, selfPid?: number): Proc[] => {\n const rows = psOut.split('\\n').map(line => line.trim().match(/^(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(.*)$/)).filter(m => !!m)\n .map(m => ({ pid: Number(m[1]), ppid: Number(m[2]), rssKb: Number(m[3]), args: m[4] }))\n const server = rows.find(r => / --server \\S*\\/zellij\\/\\S+\\//.test(r.args) && r.args.endsWith('/' + session))\n if (!server) return []\n const inSession = new Set([server.pid])\n // the status commands (this one, the df-status pane) and their children (ps, sleep, docker) are tooling, not dev processes\n const self = new Set([...(selfPid ? [selfPid] : []), ...rows.filter(r => / df-dev status\\b|df-status\\b/.test(' ' + r.args.replace(/^\\S*node\\s+\\S*\\//, ' '))).map(r => r.pid)])\n let grew = true\n while (grew) { // ps order is not parents-first when pids wrap around\n grew = false\n for (const r of rows) {\n if (!inSession.has(r.pid) && inSession.has(r.ppid)) { inSession.add(r.pid); grew = true }\n if (!self.has(r.pid) && self.has(r.ppid)) { self.add(r.pid); grew = true }\n }\n }\n const procs: Proc[] = []\n for (const r of rows) {\n if (r.pid === server.pid || !inSession.has(r.pid) || self.has(r.pid) || SHELLS.includes(path.basename(r.args.split(' ')[0]))) continue\n const cwd = cwdOf(r.pid)\n const dir = cwd && (cwd === root || cwd.startsWith(root + path.sep)) ? (path.relative(root, cwd) || '.') : '?'\n procs.push({ label: `${dir}: ${r.args.replace(/^(\\S*\\/)?node\\s+(?=\\S*node_modules\\/\\.bin\\/)/, '').replace(/^\\S*node_modules\\/\\.bin\\//, '').replace(/^\\S*\\//, '')}`, rssKb: r.rssKb })\n }\n return procs\n}\n\nconst collectContainers = (root: string, exec: Exec): Container[] => {\n const ps = jsonLines(exec('docker', ['compose', '--project-directory', root, 'ps', '-a', '--format', 'json']))\n const all = exec('docker', ['compose', '--project-directory', root, '--profile', '*', 'config', '--services']).split('\\n').filter(Boolean)\n const memory: Record<string, string> = {}\n if (ps.length) {\n for (const s of jsonLines(exec('docker', ['stats', '--no-stream', '--format', '{{json .}}', ...ps.map((c: any) => c.Name)]))) {\n memory[s.Name] = String(s.MemUsage).split(' / ')[0]\n }\n }\n return all.sort().map(service => {\n const c = ps.find((p: any) => p.Service === service)\n return c ? { service, state: c.State, health: c.Health ?? '', memory: memory[c.Name], exitCode: c.ExitCode } : { service, state: 'not started', health: '' }\n })\n}\n\n// each part fails on its own: a docker error must not hide the panes, and the reverse\nexport const collectStatus = (root: string, exec: Exec = realExec): StatusData => {\n const data: StatusData = { containers: [], panes: [], procs: [] }\n try {\n const env = readEnvFile(root)\n const url = readDevConfig(root).url\n if (url) data.url = interpolate(url, env)\n } catch {} // the URL is a convenience, the rest of the status must still show\n try {\n data.containers = collectContainers(root, exec)\n } catch (err: any) {\n data.containersError = err.message.split('\\n')[0]\n }\n const session = sessionName(root)\n try {\n const state = sessionState(exec, session)\n if (state === 'running') data.panes = listPanes(exec, session).map(p => ({ title: p.title, exited: p.exited, exit_status: p.exit_status }))\n else data.panesError = `zellij session \"${session}\" is not running (${state}): ask the user to start it with \"npm run dev-zellij\"`\n } catch (err: any) {\n data.panesError = `panes unavailable: ${err.message.split('\\n')[0]}`\n }\n const cwdOf = (pid: number) => { try { return readlinkSync(`/proc/${pid}/cwd`) } catch { return undefined } }\n try {\n const user = process.getuid ? ['-u', String(process.getuid())] : ['-e']\n data.procs = sessionProcesses(exec('ps', [...user, '-o', 'pid=,ppid=,rss=,args=']), session, cwdOf, root, process.pid)\n } catch {} // the process list is the least important part\n return data\n}\n\n/** A running pane can be broken: nodemon (or node --watch) stays alive \"waiting for file changes\" after a crash. */\nexport const checkReadiness = async (panes: PaneState[], config: DevConfig, env: Record<string, string>, ready = checkReady): Promise<PaneState[]> =>\n Promise.all(panes.map(async p => {\n const template = config.panes[p.title]?.ready\n if (p.exited || !template) return p\n const readyUrl = interpolate(template, env)\n return { ...p, ready: await ready(readyUrl), readyUrl }\n }))\n"]}
@@ -0,0 +1 @@
1
+ export {};
package/status.test.js ADDED
@@ -0,0 +1,123 @@
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 { containerMark, renderStatus, sessionProcesses, collectStatus, checkReadiness } from './status.js';
5
+ describe('status', () => {
6
+ it('marks containers like docker compose up --wait', () => {
7
+ assert.equal(containerMark({ service: 'mongo', state: 'running', health: 'healthy' }).mark, '✔');
8
+ assert.equal(containerMark({ service: 'sd', state: 'running', health: '' }).mark, '✔');
9
+ assert.equal(containerMark({ service: 'es', state: 'running', health: 'starting' }).mark, '⠿');
10
+ assert.equal(containerMark({ service: 'df', state: 'exited', health: '' }).color, 'red');
11
+ assert.equal(containerMark({ service: 'agents', state: 'not started', health: '' }).color, 'dim');
12
+ assert.deepEqual(containerMark({ service: 's3-init', state: 'exited', health: '', exitCode: 0 }), { mark: '✔', color: 'green', text: 'done' });
13
+ });
14
+ it('shows a stack stopped with exit code 0 as stopped, not done', () => {
15
+ const out = renderStatus({
16
+ containers: [{ service: 'mongo', state: 'exited', health: '', exitCode: 0 }, { service: 'nginx', state: 'exited', health: '', exitCode: 0 }],
17
+ panes: [],
18
+ procs: []
19
+ }, false);
20
+ assert.match(out, /· mongo\s+stopped/);
21
+ assert.match(out, /· nginx\s+stopped/);
22
+ assert.ok(!out.includes('done'));
23
+ });
24
+ it('renders containers, processes and the total in plain text', () => {
25
+ const out = renderStatus({
26
+ containers: [{ service: 'mongo', state: 'running', health: 'healthy', memory: '115MiB' }],
27
+ panes: [],
28
+ procs: [{ label: 'api: node --watch index.ts', rssKb: 245760 }]
29
+ }, false);
30
+ assert.match(out, /✔ mongo\s+healthy\s+115MiB/);
31
+ assert.match(out, /api: node --watch index.ts\s+240M/);
32
+ assert.match(out, /processes total\s+240M/);
33
+ assert.ok(!out.includes('\x1b['));
34
+ });
35
+ it('counts only the processes of the zellij session, with their full command line', () => {
36
+ const ps = [
37
+ ' 100 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/portals_feat-x',
38
+ ' 101 100 3000 /bin/bash -c npx --no df-dev wait-deps && node --watch index.ts',
39
+ ' 102 101 245760 node --watch index.ts',
40
+ ' 103 100 3000 bash -c vite',
41
+ ' 104 103 1048576 /home/u/.nvm/versions/node/v24.21.0/bin/node /x/portals_feat-x/node_modules/.bin/vite --port 21001',
42
+ ' 200 1 470000 claude',
43
+ ' 201 200 120000 npm exec chrome-devtools-mcp',
44
+ ' 300 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/portals'
45
+ ].join('\n');
46
+ const cwds = { 102: '/x/portals_feat-x/api', 104: '/x/portals_feat-x/ui' };
47
+ const procs = sessionProcesses(ps, 'portals_feat-x', pid => cwds[pid], '/x/portals_feat-x');
48
+ assert.deepEqual(procs, [
49
+ { label: 'api: node --watch index.ts', rssKb: 245760 },
50
+ { label: 'ui: vite --port 21001', rssKb: 1048576 }
51
+ ]);
52
+ });
53
+ it('finds children listed before their parent (pid wrap-around)', () => {
54
+ const ps = [
55
+ ' 50 900 245760 node --watch index.ts',
56
+ ' 100 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/portals',
57
+ ' 900 100 3000 bash -c node --watch index.ts'
58
+ ].join('\n');
59
+ assert.deepEqual(sessionProcesses(ps, 'portals', () => '/x/portals/api', '/x/portals').map(p => p.rssKb), [245760]);
60
+ });
61
+ it('shows exited panes with their exit code', () => {
62
+ const out = renderStatus({
63
+ containers: [],
64
+ panes: [{ title: 'api', exited: true, exit_status: 1 }, { title: 'ui', exited: false, exit_status: null }],
65
+ procs: []
66
+ }, false);
67
+ assert.match(out, /✘ api\s+exited \(1\)/);
68
+ assert.match(out, /✔ ui\s+running/);
69
+ });
70
+ it('still shows panes and processes when docker or ps fails, and says when the session is not running', () => {
71
+ const exec = (cmd, args) => {
72
+ if (cmd === 'docker')
73
+ throw new Error('Cannot connect to the Docker daemon');
74
+ if (args[0] === 'list-sessions')
75
+ throw new Error('No active zellij sessions found.');
76
+ if (cmd === 'ps')
77
+ throw new Error('ps: command not found');
78
+ throw new Error('unexpected ' + cmd + ' ' + args.join(' '));
79
+ };
80
+ const data = collectStatus('/x/portals', exec);
81
+ assert.deepEqual(data.procs, []);
82
+ assert.match(data.containersError ?? '', /Docker daemon/);
83
+ assert.match(data.panesError ?? '', /npm run dev-zellij/);
84
+ const out = renderStatus(data, false);
85
+ assert.match(out, /containers unavailable: .*Docker daemon/);
86
+ assert.match(out, /npm run dev-zellij/);
87
+ });
88
+ it('leaves out the status process itself and shortens executable paths', () => {
89
+ const ps = [
90
+ ' 100 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/portals',
91
+ ' 110 100 50000 node ./node_modules/.bin/df-dev status --watch',
92
+ ' 111 110 5000 ps -u 1000 -o pid=,ppid=,rss=,args=',
93
+ ' 120 100 90000 /home/u/.nvm/versions/node/v24.21.0/bin/node --watch index.ts'
94
+ ].join('\n');
95
+ const procs = sessionProcesses(ps, 'portals', () => '/x/portals/api', '/x/portals', 110);
96
+ assert.deepEqual(procs.map(p => p.label), ['api: node --watch index.ts']);
97
+ });
98
+ it('shows the dev URL first, in full so the terminal can open them', () => {
99
+ const out = renderStatus({ url: 'http://feat-x.localhost:28119/portals-manager/dev', containers: [], panes: [], procs: [] }, false);
100
+ assert.equal(out.split('\n')[0], 'http://feat-x.localhost:28119/portals-manager/dev');
101
+ });
102
+ it('leaves out the status pane and its ps children', () => {
103
+ const ps = [
104
+ ' 100 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/portals',
105
+ ' 130 100 60000 node /x/portals/node_modules/.bin/df-dev status --watch --start-deps',
106
+ ' 131 130 4000 ps -u 1000 -o pid=,ppid=,rss=,args=',
107
+ ' 140 100 3500 bash ./node_modules/.bin/df-status --start-deps',
108
+ ' 141 140 2000 sleep 3',
109
+ ' 120 100 90000 node --watch index.ts'
110
+ ].join('\n');
111
+ assert.deepEqual(sessionProcesses(ps, 'portals', () => '/x/portals/api', '/x/portals').map(p => p.label), ['api: node --watch index.ts']);
112
+ });
113
+ it('flags a running pane whose readiness URL does not answer (node --watch waiting after a crash)', async () => {
114
+ const panes = [{ title: 'api', exited: false, exit_status: null }, { title: 'ui', exited: false, exit_status: null }, { title: 'shell', exited: false, exit_status: null }];
115
+ const config = { ports: [], panes: { api: { ready: 'http://localhost:${P}/api/ping' }, ui: { ready: 'http://localhost:${P}/' } } };
116
+ const checked = await checkReadiness(panes, config, { P: '1' }, async (url) => !url.includes('api'));
117
+ assert.deepEqual(checked.map(p => p.ready), [false, true, undefined]);
118
+ const out = renderStatus({ containers: [], panes: checked, procs: [] }, false);
119
+ assert.match(out, /⠿ api\s+running, not ready \(http:\/\/localhost:1\/api\/ping\)/);
120
+ assert.match(out, /✔ ui\s+running/);
121
+ });
122
+ });
123
+ //# sourceMappingURL=status.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.test.js","sourceRoot":"","sources":["status.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 { containerMark, renderStatus, sessionProcesses, collectStatus, checkReadiness } from './status.js'\nimport type { Exec } from './exec.js'\n\ndescribe('status', () => {\n it('marks containers like docker compose up --wait', () => {\n assert.equal(containerMark({ service: 'mongo', state: 'running', health: 'healthy' }).mark, '✔')\n assert.equal(containerMark({ service: 'sd', state: 'running', health: '' }).mark, '✔')\n assert.equal(containerMark({ service: 'es', state: 'running', health: 'starting' }).mark, '⠿')\n assert.equal(containerMark({ service: 'df', state: 'exited', health: '' }).color, 'red')\n assert.equal(containerMark({ service: 'agents', state: 'not started', health: '' }).color, 'dim')\n assert.deepEqual(containerMark({ service: 's3-init', state: 'exited', health: '', exitCode: 0 }), { mark: '✔', color: 'green', text: 'done' })\n })\n\n it('shows a stack stopped with exit code 0 as stopped, not done', () => {\n const out = renderStatus({\n containers: [{ service: 'mongo', state: 'exited', health: '', exitCode: 0 }, { service: 'nginx', state: 'exited', health: '', exitCode: 0 }],\n panes: [],\n procs: []\n }, false)\n assert.match(out, /· mongo\\s+stopped/)\n assert.match(out, /· nginx\\s+stopped/)\n assert.ok(!out.includes('done'))\n })\n\n it('renders containers, processes and the total in plain text', () => {\n const out = renderStatus({\n containers: [{ service: 'mongo', state: 'running', health: 'healthy', memory: '115MiB' }],\n panes: [],\n procs: [{ label: 'api: node --watch index.ts', rssKb: 245760 }]\n }, false)\n assert.match(out, /✔ mongo\\s+healthy\\s+115MiB/)\n assert.match(out, /api: node --watch index.ts\\s+240M/)\n assert.match(out, /processes total\\s+240M/)\n assert.ok(!out.includes('\\x1b['))\n })\n\n it('counts only the processes of the zellij session, with their full command line', () => {\n const ps = [\n ' 100 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/portals_feat-x',\n ' 101 100 3000 /bin/bash -c npx --no df-dev wait-deps && node --watch index.ts',\n ' 102 101 245760 node --watch index.ts',\n ' 103 100 3000 bash -c vite',\n ' 104 103 1048576 /home/u/.nvm/versions/node/v24.21.0/bin/node /x/portals_feat-x/node_modules/.bin/vite --port 21001',\n ' 200 1 470000 claude',\n ' 201 200 120000 npm exec chrome-devtools-mcp',\n ' 300 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/portals'\n ].join('\\n')\n const cwds: Record<number, string> = { 102: '/x/portals_feat-x/api', 104: '/x/portals_feat-x/ui' }\n const procs = sessionProcesses(ps, 'portals_feat-x', pid => cwds[pid], '/x/portals_feat-x')\n assert.deepEqual(procs, [\n { label: 'api: node --watch index.ts', rssKb: 245760 },\n { label: 'ui: vite --port 21001', rssKb: 1048576 }\n ])\n })\n\n it('finds children listed before their parent (pid wrap-around)', () => {\n const ps = [\n ' 50 900 245760 node --watch index.ts',\n ' 100 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/portals',\n ' 900 100 3000 bash -c node --watch index.ts'\n ].join('\\n')\n assert.deepEqual(sessionProcesses(ps, 'portals', () => '/x/portals/api', '/x/portals').map(p => p.rssKb), [245760])\n })\n\n it('shows exited panes with their exit code', () => {\n const out = renderStatus({\n containers: [],\n panes: [{ title: 'api', exited: true, exit_status: 1 }, { title: 'ui', exited: false, exit_status: null }],\n procs: []\n }, false)\n assert.match(out, /✘ api\\s+exited \\(1\\)/)\n assert.match(out, /✔ ui\\s+running/)\n })\n\n it('still shows panes and processes when docker or ps fails, and says when the session is not running', () => {\n const exec: Exec = (cmd, args) => {\n if (cmd === 'docker') throw new Error('Cannot connect to the Docker daemon')\n if (args[0] === 'list-sessions') throw new Error('No active zellij sessions found.')\n if (cmd === 'ps') throw new Error('ps: command not found')\n throw new Error('unexpected ' + cmd + ' ' + args.join(' '))\n }\n const data = collectStatus('/x/portals', exec)\n assert.deepEqual(data.procs, [])\n assert.match(data.containersError ?? '', /Docker daemon/)\n assert.match(data.panesError ?? '', /npm run dev-zellij/)\n const out = renderStatus(data, false)\n assert.match(out, /containers unavailable: .*Docker daemon/)\n assert.match(out, /npm run dev-zellij/)\n })\n\n it('leaves out the status process itself and shortens executable paths', () => {\n const ps = [\n ' 100 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/portals',\n ' 110 100 50000 node ./node_modules/.bin/df-dev status --watch',\n ' 111 110 5000 ps -u 1000 -o pid=,ppid=,rss=,args=',\n ' 120 100 90000 /home/u/.nvm/versions/node/v24.21.0/bin/node --watch index.ts'\n ].join('\\n')\n const procs = sessionProcesses(ps, 'portals', () => '/x/portals/api', '/x/portals', 110)\n assert.deepEqual(procs.map(p => p.label), ['api: node --watch index.ts'])\n })\n\n it('shows the dev URL first, in full so the terminal can open them', () => {\n const out = renderStatus({ url: 'http://feat-x.localhost:28119/portals-manager/dev', containers: [], panes: [], procs: [] }, false)\n assert.equal(out.split('\\n')[0], 'http://feat-x.localhost:28119/portals-manager/dev')\n })\n\n it('leaves out the status pane and its ps children', () => {\n const ps = [\n ' 100 1 20000 /usr/bin/zellij --server /run/user/1000/zellij/0.45.1/portals',\n ' 130 100 60000 node /x/portals/node_modules/.bin/df-dev status --watch --start-deps',\n ' 131 130 4000 ps -u 1000 -o pid=,ppid=,rss=,args=',\n ' 140 100 3500 bash ./node_modules/.bin/df-status --start-deps',\n ' 141 140 2000 sleep 3',\n ' 120 100 90000 node --watch index.ts'\n ].join('\\n')\n assert.deepEqual(sessionProcesses(ps, 'portals', () => '/x/portals/api', '/x/portals').map(p => p.label), ['api: node --watch index.ts'])\n })\n\n it('flags a running pane whose readiness URL does not answer (node --watch waiting after a crash)', async () => {\n const panes = [{ title: 'api', exited: false, exit_status: null }, { title: 'ui', exited: false, exit_status: null }, { title: 'shell', exited: false, exit_status: null }]\n const config = { ports: [], panes: { api: { ready: 'http://localhost:${P}/api/ping' }, ui: { ready: 'http://localhost:${P}/' } } }\n const checked = await checkReadiness(panes, config, { P: '1' }, async url => !url.includes('api'))\n assert.deepEqual(checked.map(p => p.ready), [false, true, undefined])\n const out = renderStatus({ containers: [], panes: checked, procs: [] }, false)\n assert.match(out, /⠿ api\\s+running, not ready \\(http:\\/\\/localhost:1\\/api\\/ping\\)/)\n assert.match(out, /✔ ui\\s+running/)\n })\n})\n"]}
@@ -0,0 +1,141 @@
1
+ #!/bin/bash
2
+ # Delete a worktree, its branch, and its docker containers/volumes/local images
3
+ # (when the worktree has a compose file).
4
+ #
5
+ # Refuses to proceed if the worktree has uncommitted changes or commits not
6
+ # pushed to any origin ref. Pass -f / --force to bypass these checks (e.g. a
7
+ # squash-merged branch); the deleted branch's commit is printed to recover it.
8
+
9
+ set -e
10
+
11
+ FORCE=false
12
+ BRANCH_NAME=""
13
+
14
+ while [ $# -gt 0 ]; do
15
+ case "$1" in
16
+ -f|--force) FORCE=true ;;
17
+ -h|--help)
18
+ echo "Usage: df-worktree-delete <branch> [-f|--force]" >&2
19
+ echo " --force / -f skip uncommitted/unpushed safety checks (the branch is deleted all the same)"
20
+ exit 0
21
+ ;;
22
+ -*)
23
+ echo "Error: unknown flag '$1'" >&2
24
+ echo "Usage: df-worktree-delete <branch> [-f|--force]" >&2
25
+ exit 1
26
+ ;;
27
+ *)
28
+ if [ -z "$BRANCH_NAME" ]; then
29
+ BRANCH_NAME="$1"
30
+ else
31
+ echo "Error: too many positional arguments" >&2
32
+ exit 1
33
+ fi
34
+ ;;
35
+ esac
36
+ shift
37
+ done
38
+
39
+ if [ -z "$BRANCH_NAME" ]; then
40
+ echo "Error: Please provide a branch name." >&2
41
+ echo "Usage: df-worktree-delete <branch> [-f|--force]" >&2
42
+ exit 1
43
+ fi
44
+
45
+ # the branch name becomes a directory, a zellij session and a DEV_HOST label
46
+ if ! [[ "$BRANCH_NAME" =~ ^[A-Za-z0-9._-]{1,60}$ ]]; then
47
+ echo "Error: invalid branch name \"$BRANCH_NAME\": letters, digits, '.', '_' and '-' only, no \"/\", at most 60 characters (e.g. feat-xyz)" >&2
48
+ exit 1
49
+ fi
50
+
51
+ # the worktrees live next to the main checkout: refuse a subfolder or another worktree
52
+ TOPLEVEL=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "Error: not inside a git repository." >&2; exit 1; }
53
+ if [ "$TOPLEVEL" != "$(pwd -P)" ]; then
54
+ echo "Error: run df-worktree-delete from the root of the repository: cd $TOPLEVEL" >&2
55
+ exit 1
56
+ fi
57
+ COMMON_DIR=$(git rev-parse --path-format=absolute --git-common-dir)
58
+ if [ "$(git rev-parse --absolute-git-dir)" != "$COMMON_DIR" ]; then
59
+ echo "Error: run df-worktree-delete from the main checkout, not from a worktree: cd $(dirname "$COMMON_DIR")" >&2
60
+ exit 1
61
+ fi
62
+
63
+ REPO_NAME=$(basename "$PWD")
64
+ TARGET_DIR="../${REPO_NAME}_${BRANCH_NAME}"
65
+
66
+ if [ ! -d "$TARGET_DIR" ]; then
67
+ echo "Error: Worktree directory $TARGET_DIR does not exist."
68
+ exit 1
69
+ fi
70
+
71
+ ORIGIN_DIR="$PWD"
72
+
73
+ if [ "$FORCE" != true ]; then
74
+ echo "Checking $TARGET_DIR for uncommitted / unpushed work…"
75
+ cd "$TARGET_DIR"
76
+
77
+ # Refresh remote refs so the "unpushed" check is accurate
78
+ git fetch origin --quiet 2>/dev/null || true
79
+
80
+ DIRTY=$(git status --porcelain)
81
+ if [ -n "$DIRTY" ]; then
82
+ echo
83
+ echo "❌ Refusing to delete: $TARGET_DIR has uncommitted changes:"
84
+ echo "$DIRTY" | sed 's/^/ /'
85
+ echo
86
+ echo "Commit / push / stash them, or pass -f / --force to bypass."
87
+ exit 1
88
+ fi
89
+
90
+ # Commits reachable from HEAD but NOT reachable from any origin/* ref.
91
+ # Catches: never-pushed branches, branches ahead of upstream, detached HEAD with unique commits.
92
+ UNPUSHED_COUNT=$(git rev-list --count HEAD --not --remotes=origin 2>/dev/null || echo 0)
93
+ if [ "$UNPUSHED_COUNT" -gt 0 ]; then
94
+ echo
95
+ echo "❌ Refusing to delete: $TARGET_DIR has $UNPUSHED_COUNT commit(s) not present on any origin ref:"
96
+ git --no-pager log --oneline HEAD --not --remotes=origin | sed 's/^/ /'
97
+ echo
98
+ echo "Push them (e.g. 'git push -u origin $BRANCH_NAME'), or pass -f / --force to bypass."
99
+ exit 1
100
+ fi
101
+
102
+ cd "$ORIGIN_DIR"
103
+ echo "✓ working tree clean and all commits present on origin"
104
+ fi
105
+
106
+ # the branch checked out in the worktree, deleted with it when it is the one named (not a detached HEAD)
107
+ WORKTREE_BRANCH=$(git -C "$TARGET_DIR" branch --show-current)
108
+
109
+ cd "$TARGET_DIR"
110
+ # a repository without containers has no compose file: docker compose would fail, or find a parent's
111
+ HAS_COMPOSE=false
112
+ for f in compose.yaml compose.yml docker-compose.yaml docker-compose.yml; do
113
+ [ -f "$f" ] && HAS_COMPOSE=true
114
+ done
115
+ if [ "$HAS_COMPOSE" = true ]; then
116
+ echo "Stopping & removing docker compose services in $TARGET_DIR (with volumes and orphans)"
117
+ # --profile '*' : catch every container the worktree may have started, whatever the repo names its profiles
118
+ # -v : drop named volumes (mongo, elasticsearch, s3mock, clamav…) — data is worktree-scoped
119
+ # --remove-orphans : sweep any container left over from a previous compose file
120
+ # --rmi local : remove images built locally for this worktree (keeps remote-pulled images)
121
+ docker compose --profile '*' down -v --remove-orphans --rmi local
122
+ fi
123
+
124
+ echo "Removing git worktree at $TARGET_DIR"
125
+ cd "$ORIGIN_DIR"
126
+ if [ "$FORCE" = true ]; then
127
+ git worktree remove --force "$TARGET_DIR" # untracked files (generated types, UI builds) would block a plain remove
128
+ else
129
+ git worktree remove "$TARGET_DIR"
130
+ fi
131
+ zellij delete-session "$(basename "$TARGET_DIR")" --force >/dev/null 2>&1 || true
132
+
133
+ if [ "$WORKTREE_BRANCH" = "$BRANCH_NAME" ]; then
134
+ # without --force every commit is on origin; with it, the printed commit is the way back
135
+ echo "Deleting branch $BRANCH_NAME (was $(git rev-parse --short "$BRANCH_NAME"))"
136
+ git branch -q -D "$BRANCH_NAME"
137
+ fi
138
+
139
+ echo "-----------------------------------------------"
140
+ echo "✅ Worktree $BRANCH_NAME deleted!"
141
+ echo "-----------------------------------------------"