@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/env.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { readEnvFile } from './config.js';
|
|
6
|
+
// below the kernel ephemeral range (32768+), so a port is not taken later by an outgoing connection
|
|
7
|
+
export const PORT_MIN = 20000;
|
|
8
|
+
export const PORT_MAX = 29999;
|
|
9
|
+
export const isPortFree = (port) => new Promise(resolve => {
|
|
10
|
+
const server = net.createServer();
|
|
11
|
+
server.once('error', () => resolve(false));
|
|
12
|
+
server.listen(port, '127.0.0.1', () => server.close(() => resolve(true)));
|
|
13
|
+
});
|
|
14
|
+
export const pickPorts = async (ports, isFree = isPortFree, random = Math.random) => {
|
|
15
|
+
const result = {};
|
|
16
|
+
const taken = new Set();
|
|
17
|
+
for (const name of ports) {
|
|
18
|
+
for (let attempt = 0;; attempt++) {
|
|
19
|
+
if (attempt >= 100)
|
|
20
|
+
throw new Error(`no free port found for ${name} in ${PORT_MIN}-${PORT_MAX}`);
|
|
21
|
+
const port = PORT_MIN + Math.floor(random() * (PORT_MAX - PORT_MIN + 1));
|
|
22
|
+
if (!taken.has(port) && await isFree(port)) {
|
|
23
|
+
result[name] = port;
|
|
24
|
+
taken.add(port);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
/** Ports already written in the .env of the other worktrees of the repository: free now while their env is stopped, taken later. */
|
|
32
|
+
const gitWorktreeList = (root) => execFileSync('git', ['worktree', 'list', '--porcelain'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
33
|
+
export const otherWorktreePorts = (root, worktreeList = gitWorktreeList) => {
|
|
34
|
+
const ports = new Set();
|
|
35
|
+
let list = '';
|
|
36
|
+
try {
|
|
37
|
+
list = worktreeList(root);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return ports; // not a git checkout: nothing to avoid
|
|
41
|
+
}
|
|
42
|
+
for (const line of list.split('\n')) {
|
|
43
|
+
if (!line.startsWith('worktree '))
|
|
44
|
+
continue;
|
|
45
|
+
const dir = line.slice('worktree '.length);
|
|
46
|
+
if (path.resolve(dir) === path.resolve(root))
|
|
47
|
+
continue;
|
|
48
|
+
for (const value of Object.values(readEnvFile(dir))) {
|
|
49
|
+
const port = Number(value);
|
|
50
|
+
if (Number.isInteger(port) && port >= PORT_MIN && port <= PORT_MAX)
|
|
51
|
+
ports.add(port);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return ports;
|
|
55
|
+
};
|
|
56
|
+
// quoted only when parseEnv would otherwise cut or change the value (#, spaces, quotes, new lines)
|
|
57
|
+
export const formatEnvValue = (value) => {
|
|
58
|
+
const s = String(value);
|
|
59
|
+
if (/^[\w.,:/@%+=-]*$/.test(s))
|
|
60
|
+
return s;
|
|
61
|
+
// single quotes are read literally by parseEnv, backticks for a value that contains one
|
|
62
|
+
for (const quote of ["'", '`', '"'])
|
|
63
|
+
if (!s.includes(quote))
|
|
64
|
+
return quote + s + quote;
|
|
65
|
+
throw new Error(`cannot write a .env value that contains ', \` and ": ${s}`);
|
|
66
|
+
};
|
|
67
|
+
export const hostLabel = (branch) => branch.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 63).replace(/-$/, '') || 'dev';
|
|
68
|
+
const currentBranch = (root) => {
|
|
69
|
+
try {
|
|
70
|
+
return execFileSync('git', ['branch', '--show-current'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || 'detached';
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return 'dev';
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
export const initEnv = async (root, config, opts = {}) => {
|
|
77
|
+
const file = path.join(root, '.env');
|
|
78
|
+
if (existsSync(file) && !opts.force)
|
|
79
|
+
return { written: false, file };
|
|
80
|
+
const reserved = opts.reserved ?? otherWorktreePorts(root);
|
|
81
|
+
const isFree = opts.isFree ?? isPortFree;
|
|
82
|
+
const ports = await pickPorts(config.ports, async (port) => !reserved.has(port) && await isFree(port), opts.random);
|
|
83
|
+
const vars = {
|
|
84
|
+
// with --force, variables added by hand survive; the generated ones are drawn again
|
|
85
|
+
...readEnvFile(root),
|
|
86
|
+
DEV_HOST: `${hostLabel(opts.branch ?? currentBranch(root))}.localhost`,
|
|
87
|
+
// all dev services listen on the loopback, the SSRF protection of the default http agents must accept it
|
|
88
|
+
SSRF_PUBLIC_IPS: '127.0.0.1,::1',
|
|
89
|
+
...ports
|
|
90
|
+
};
|
|
91
|
+
writeFileSync(file, Object.entries(vars).map(([k, v]) => `${k}=${formatEnvValue(v)}`).join('\n') + '\n');
|
|
92
|
+
return { written: true, file };
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=env.js.map
|
package/env.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["env.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, writeFileSync } from 'node:fs'\nimport net from 'node:net'\nimport path from 'node:path'\nimport { execFileSync } from 'node:child_process'\nimport { readEnvFile, type DevConfig } from './config.js'\n\n// below the kernel ephemeral range (32768+), so a port is not taken later by an outgoing connection\nexport const PORT_MIN = 20000\nexport const PORT_MAX = 29999\n\nexport const isPortFree = (port: number) => new Promise<boolean>(resolve => {\n const server = net.createServer()\n server.once('error', () => resolve(false))\n server.listen(port, '127.0.0.1', () => server.close(() => resolve(true)))\n})\n\nexport const pickPorts = async (ports: string[], isFree = isPortFree, random = Math.random) => {\n const result: Record<string, number> = {}\n const taken = new Set<number>()\n for (const name of ports) {\n for (let attempt = 0; ; attempt++) {\n if (attempt >= 100) throw new Error(`no free port found for ${name} in ${PORT_MIN}-${PORT_MAX}`)\n const port = PORT_MIN + Math.floor(random() * (PORT_MAX - PORT_MIN + 1))\n if (!taken.has(port) && await isFree(port)) {\n result[name] = port\n taken.add(port)\n break\n }\n }\n }\n return result\n}\n\n/** Ports already written in the .env of the other worktrees of the repository: free now while their env is stopped, taken later. */\nconst gitWorktreeList = (root: string) => execFileSync('git', ['worktree', 'list', '--porcelain'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })\n\nexport const otherWorktreePorts = (root: string, worktreeList = gitWorktreeList) => {\n const ports = new Set<number>()\n let list = ''\n try {\n list = worktreeList(root)\n } catch {\n return ports // not a git checkout: nothing to avoid\n }\n for (const line of list.split('\\n')) {\n if (!line.startsWith('worktree ')) continue\n const dir = line.slice('worktree '.length)\n if (path.resolve(dir) === path.resolve(root)) continue\n for (const value of Object.values(readEnvFile(dir))) {\n const port = Number(value)\n if (Number.isInteger(port) && port >= PORT_MIN && port <= PORT_MAX) ports.add(port)\n }\n }\n return ports\n}\n\n// quoted only when parseEnv would otherwise cut or change the value (#, spaces, quotes, new lines)\nexport const formatEnvValue = (value: string | number) => {\n const s = String(value)\n if (/^[\\w.,:/@%+=-]*$/.test(s)) return s\n // single quotes are read literally by parseEnv, backticks for a value that contains one\n for (const quote of [\"'\", '`', '\"']) if (!s.includes(quote)) return quote + s + quote\n throw new Error(`cannot write a .env value that contains ', \\` and \": ${s}`)\n}\n\nexport const hostLabel = (branch: string) =>\n branch.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 63).replace(/-$/, '') || 'dev'\n\nconst currentBranch = (root: string) => {\n try {\n return execFileSync('git', ['branch', '--show-current'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || 'detached'\n } catch {\n return 'dev'\n }\n}\n\nexport const initEnv = async (root: string, config: DevConfig, opts: { force?: boolean, branch?: string, isFree?: (port: number) => Promise<boolean>, reserved?: Set<number>, random?: () => number } = {}) => {\n const file = path.join(root, '.env')\n if (existsSync(file) && !opts.force) return { written: false, file }\n const reserved = opts.reserved ?? otherWorktreePorts(root)\n const isFree = opts.isFree ?? isPortFree\n const ports = await pickPorts(config.ports, async port => !reserved.has(port) && await isFree(port), opts.random)\n const vars: Record<string, string | number> = {\n // with --force, variables added by hand survive; the generated ones are drawn again\n ...readEnvFile(root),\n DEV_HOST: `${hostLabel(opts.branch ?? currentBranch(root))}.localhost`,\n // all dev services listen on the loopback, the SSRF protection of the default http agents must accept it\n SSRF_PUBLIC_IPS: '127.0.0.1,::1',\n ...ports\n }\n writeFileSync(file, Object.entries(vars).map(([k, v]) => `${k}=${formatEnvValue(v)}`).join('\\n') + '\\n')\n return { written: true, file }\n}\n"]}
|
package/env.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/env.test.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'assert';
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { pickPorts, hostLabel, initEnv, otherWorktreePorts, PORT_MIN, PORT_MAX } from './env.js';
|
|
7
|
+
import { readEnvFile } from './config.js';
|
|
8
|
+
const config = { ports: ['NGINX_PORT', 'DEV_API_PORT'], panes: {} };
|
|
9
|
+
describe('pickPorts', () => {
|
|
10
|
+
it('draws distinct free ports in range', async () => {
|
|
11
|
+
const ports = await pickPorts(config.ports, async () => true);
|
|
12
|
+
for (const name of ['NGINX_PORT', 'DEV_API_PORT']) {
|
|
13
|
+
assert.ok(ports[name] >= PORT_MIN && ports[name] <= PORT_MAX);
|
|
14
|
+
}
|
|
15
|
+
assert.notEqual(ports.NGINX_PORT, ports.DEV_API_PORT);
|
|
16
|
+
});
|
|
17
|
+
it('skips ports that are busy', async () => {
|
|
18
|
+
const busy = new Set([PORT_MIN]);
|
|
19
|
+
let n = 0;
|
|
20
|
+
const random = () => (n++ === 0 ? 0 : 0.5); // first draw hits the busy PORT_MIN
|
|
21
|
+
const ports = await pickPorts(['A'], async (port) => !busy.has(port), random);
|
|
22
|
+
assert.notEqual(ports.A, PORT_MIN);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('hostLabel', () => {
|
|
26
|
+
it('turns any branch name into a valid hostname label', () => {
|
|
27
|
+
assert.equal(hostLabel('feat/X_y'), 'feat-x-y');
|
|
28
|
+
assert.equal(hostLabel('master'), 'master');
|
|
29
|
+
assert.equal(hostLabel('--weird--'), 'weird');
|
|
30
|
+
});
|
|
31
|
+
it('caps the label at 63 characters (DNS limit)', () => {
|
|
32
|
+
const label = hostLabel('a'.repeat(70) + '-b');
|
|
33
|
+
assert.equal(label.length, 63);
|
|
34
|
+
assert.ok(!label.endsWith('-'));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('initEnv', () => {
|
|
38
|
+
it('writes ports, DEV_HOST and SSRF_PUBLIC_IPS', async () => {
|
|
39
|
+
const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'));
|
|
40
|
+
const res = await initEnv(root, config, { branch: 'feat/X', isFree: async () => true });
|
|
41
|
+
assert.equal(res.written, true);
|
|
42
|
+
const env = readEnvFile(root);
|
|
43
|
+
assert.equal(env.DEV_HOST, 'feat-x.localhost');
|
|
44
|
+
assert.equal(env.SSRF_PUBLIC_IPS, '127.0.0.1,::1');
|
|
45
|
+
assert.ok(Number(env.DEV_API_PORT) >= PORT_MIN);
|
|
46
|
+
});
|
|
47
|
+
it('never overwrites an existing .env without force', async () => {
|
|
48
|
+
const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'));
|
|
49
|
+
writeFileSync(path.join(root, '.env'), 'NGINX_PORT=1\n');
|
|
50
|
+
const res = await initEnv(root, config, { branch: 'x', isFree: async () => true });
|
|
51
|
+
assert.equal(res.written, false);
|
|
52
|
+
assert.equal(readFileSync(path.join(root, '.env'), 'utf8'), 'NGINX_PORT=1\n');
|
|
53
|
+
});
|
|
54
|
+
it('regenerates with force', async () => {
|
|
55
|
+
const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'));
|
|
56
|
+
writeFileSync(path.join(root, '.env'), 'NGINX_PORT=1\n');
|
|
57
|
+
await initEnv(root, config, { force: true, branch: 'x', isFree: async () => true });
|
|
58
|
+
assert.notEqual(readEnvFile(root).NGINX_PORT, '1');
|
|
59
|
+
});
|
|
60
|
+
it('keeps the variables added by hand when regenerating with force', async () => {
|
|
61
|
+
const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'));
|
|
62
|
+
writeFileSync(path.join(root, '.env'), 'NGINX_PORT=1\nDEV_HOST=old.localhost\nMY_TOKEN=secret\n');
|
|
63
|
+
await initEnv(root, config, { force: true, branch: 'x', isFree: async () => true });
|
|
64
|
+
const env = readEnvFile(root);
|
|
65
|
+
assert.equal(env.MY_TOKEN, 'secret');
|
|
66
|
+
assert.equal(env.DEV_HOST, 'x.localhost');
|
|
67
|
+
assert.notEqual(env.NGINX_PORT, '1');
|
|
68
|
+
});
|
|
69
|
+
it('keeps hand-added values that need quotes intact through a regeneration', async () => {
|
|
70
|
+
const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'));
|
|
71
|
+
writeFileSync(path.join(root, '.env'), 'NGINX_PORT=1\nSECRET=\'abc#def\'\nMSG="a b"\nQUOTE=`it\'s`\nCERT=\'line1\nline2\'\nPLAIN=x\n');
|
|
72
|
+
await initEnv(root, config, { force: true, branch: 'x', isFree: async () => true, reserved: new Set() });
|
|
73
|
+
await initEnv(root, config, { force: true, branch: 'x', isFree: async () => true, reserved: new Set() });
|
|
74
|
+
const env = readEnvFile(root);
|
|
75
|
+
assert.equal(env.SECRET, 'abc#def');
|
|
76
|
+
assert.equal(env.MSG, 'a b');
|
|
77
|
+
assert.equal(env.QUOTE, "it's");
|
|
78
|
+
assert.equal(env.CERT, 'line1\nline2');
|
|
79
|
+
assert.equal(env.PLAIN, 'x');
|
|
80
|
+
assert.match(readFileSync(path.join(root, '.env'), 'utf8'), /^PLAIN=x$/m);
|
|
81
|
+
});
|
|
82
|
+
it('never draws a port written in the .env of another worktree', async () => {
|
|
83
|
+
const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'));
|
|
84
|
+
// the first draw falls on a port another worktree uses, the second on a fresh one
|
|
85
|
+
const draws = [PORT_MIN + 5, PORT_MIN + 6].map(p => (p - PORT_MIN + 0.5) / (PORT_MAX - PORT_MIN + 1));
|
|
86
|
+
let i = 0;
|
|
87
|
+
await initEnv(root, { ...config, ports: ['NGINX_PORT'] }, { branch: 'x', isFree: async () => true, reserved: new Set([PORT_MIN + 5]), random: () => draws[i++] });
|
|
88
|
+
assert.equal(readEnvFile(root).NGINX_PORT, String(PORT_MIN + 6));
|
|
89
|
+
assert.equal(i, 2);
|
|
90
|
+
});
|
|
91
|
+
it('reads the ports of the other worktrees of the repository', () => {
|
|
92
|
+
const tmp = mkdtempSync(path.join(tmpdir(), 'df-dev-wt-'));
|
|
93
|
+
const root = path.join(tmp, 'repo');
|
|
94
|
+
const other = path.join(tmp, 'repo_feat-a');
|
|
95
|
+
mkdirSync(root);
|
|
96
|
+
mkdirSync(other);
|
|
97
|
+
writeFileSync(path.join(other, '.env'), `NGINX_PORT=${PORT_MIN + 5}\nDEV_HOST=feat-a.localhost\nOTHER=80\n`);
|
|
98
|
+
writeFileSync(path.join(root, '.env'), `NGINX_PORT=${PORT_MIN + 6}\n`);
|
|
99
|
+
// output of git worktree list --porcelain, given as text: no git in this test
|
|
100
|
+
const list = () => `worktree ${root}\nHEAD abc\nbranch refs/heads/master\n\nworktree ${other}\nHEAD def\nbranch refs/heads/feat-a\n`;
|
|
101
|
+
assert.deepEqual([...otherWorktreePorts(root, list)], [PORT_MIN + 5]);
|
|
102
|
+
assert.deepEqual([...otherWorktreePorts(other, list)], [PORT_MIN + 6]);
|
|
103
|
+
assert.deepEqual([...otherWorktreePorts(root, () => { throw new Error('not a git repository'); })], []);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
//# sourceMappingURL=env.test.js.map
|
package/env.test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.test.js","sourceRoot":"","sources":["env.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\nimport { pickPorts, hostLabel, initEnv, otherWorktreePorts, PORT_MIN, PORT_MAX } from './env.js'\nimport { readEnvFile } from './config.js'\n\nconst config = { ports: ['NGINX_PORT', 'DEV_API_PORT'], panes: {} }\n\ndescribe('pickPorts', () => {\n it('draws distinct free ports in range', async () => {\n const ports = await pickPorts(config.ports, async () => true)\n for (const name of ['NGINX_PORT', 'DEV_API_PORT']) {\n assert.ok(ports[name] >= PORT_MIN && ports[name] <= PORT_MAX)\n }\n assert.notEqual(ports.NGINX_PORT, ports.DEV_API_PORT)\n })\n\n it('skips ports that are busy', async () => {\n const busy = new Set([PORT_MIN])\n let n = 0\n const random = () => (n++ === 0 ? 0 : 0.5) // first draw hits the busy PORT_MIN\n const ports = await pickPorts(['A'], async port => !busy.has(port), random)\n assert.notEqual(ports.A, PORT_MIN)\n })\n})\n\ndescribe('hostLabel', () => {\n it('turns any branch name into a valid hostname label', () => {\n assert.equal(hostLabel('feat/X_y'), 'feat-x-y')\n assert.equal(hostLabel('master'), 'master')\n assert.equal(hostLabel('--weird--'), 'weird')\n })\n\n it('caps the label at 63 characters (DNS limit)', () => {\n const label = hostLabel('a'.repeat(70) + '-b')\n assert.equal(label.length, 63)\n assert.ok(!label.endsWith('-'))\n })\n})\n\ndescribe('initEnv', () => {\n it('writes ports, DEV_HOST and SSRF_PUBLIC_IPS', async () => {\n const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'))\n const res = await initEnv(root, config, { branch: 'feat/X', isFree: async () => true })\n assert.equal(res.written, true)\n const env = readEnvFile(root)\n assert.equal(env.DEV_HOST, 'feat-x.localhost')\n assert.equal(env.SSRF_PUBLIC_IPS, '127.0.0.1,::1')\n assert.ok(Number(env.DEV_API_PORT) >= PORT_MIN)\n })\n\n it('never overwrites an existing .env without force', async () => {\n const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'))\n writeFileSync(path.join(root, '.env'), 'NGINX_PORT=1\\n')\n const res = await initEnv(root, config, { branch: 'x', isFree: async () => true })\n assert.equal(res.written, false)\n assert.equal(readFileSync(path.join(root, '.env'), 'utf8'), 'NGINX_PORT=1\\n')\n })\n\n it('regenerates with force', async () => {\n const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'))\n writeFileSync(path.join(root, '.env'), 'NGINX_PORT=1\\n')\n await initEnv(root, config, { force: true, branch: 'x', isFree: async () => true })\n assert.notEqual(readEnvFile(root).NGINX_PORT, '1')\n })\n\n it('keeps the variables added by hand when regenerating with force', async () => {\n const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'))\n writeFileSync(path.join(root, '.env'), 'NGINX_PORT=1\\nDEV_HOST=old.localhost\\nMY_TOKEN=secret\\n')\n await initEnv(root, config, { force: true, branch: 'x', isFree: async () => true })\n const env = readEnvFile(root)\n assert.equal(env.MY_TOKEN, 'secret')\n assert.equal(env.DEV_HOST, 'x.localhost')\n assert.notEqual(env.NGINX_PORT, '1')\n })\n\n it('keeps hand-added values that need quotes intact through a regeneration', async () => {\n const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'))\n writeFileSync(path.join(root, '.env'), 'NGINX_PORT=1\\nSECRET=\\'abc#def\\'\\nMSG=\"a b\"\\nQUOTE=`it\\'s`\\nCERT=\\'line1\\nline2\\'\\nPLAIN=x\\n')\n await initEnv(root, config, { force: true, branch: 'x', isFree: async () => true, reserved: new Set() })\n await initEnv(root, config, { force: true, branch: 'x', isFree: async () => true, reserved: new Set() })\n const env = readEnvFile(root)\n assert.equal(env.SECRET, 'abc#def')\n assert.equal(env.MSG, 'a b')\n assert.equal(env.QUOTE, \"it's\")\n assert.equal(env.CERT, 'line1\\nline2')\n assert.equal(env.PLAIN, 'x')\n assert.match(readFileSync(path.join(root, '.env'), 'utf8'), /^PLAIN=x$/m)\n })\n\n it('never draws a port written in the .env of another worktree', async () => {\n const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'))\n // the first draw falls on a port another worktree uses, the second on a fresh one\n const draws = [PORT_MIN + 5, PORT_MIN + 6].map(p => (p - PORT_MIN + 0.5) / (PORT_MAX - PORT_MIN + 1))\n let i = 0\n await initEnv(root, { ...config, ports: ['NGINX_PORT'] }, { branch: 'x', isFree: async () => true, reserved: new Set([PORT_MIN + 5]), random: () => draws[i++] })\n assert.equal(readEnvFile(root).NGINX_PORT, String(PORT_MIN + 6))\n assert.equal(i, 2)\n })\n\n it('reads the ports of the other worktrees of the repository', () => {\n const tmp = mkdtempSync(path.join(tmpdir(), 'df-dev-wt-'))\n const root = path.join(tmp, 'repo')\n const other = path.join(tmp, 'repo_feat-a')\n mkdirSync(root)\n mkdirSync(other)\n writeFileSync(path.join(other, '.env'), `NGINX_PORT=${PORT_MIN + 5}\\nDEV_HOST=feat-a.localhost\\nOTHER=80\\n`)\n writeFileSync(path.join(root, '.env'), `NGINX_PORT=${PORT_MIN + 6}\\n`)\n // output of git worktree list --porcelain, given as text: no git in this test\n const list = () => `worktree ${root}\\nHEAD abc\\nbranch refs/heads/master\\n\\nworktree ${other}\\nHEAD def\\nbranch refs/heads/feat-a\\n`\n assert.deepEqual([...otherWorktreePorts(root, list)], [PORT_MIN + 5])\n assert.deepEqual([...otherWorktreePorts(other, list)], [PORT_MIN + 6])\n assert.deepEqual([...otherWorktreePorts(root, () => { throw new Error('not a git repository') })], [])\n })\n})\n"]}
|
package/exec.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type Exec = (cmd: string, args: string[]) => string;
|
|
2
|
+
export declare const realExec: Exec;
|
|
3
|
+
/** Run a command with the terminal attached (output visible to the user), throw if it fails. */
|
|
4
|
+
export declare const run: (cmd: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv) => void;
|
|
5
|
+
export declare const jsonLines: (out: string) => any[];
|
package/exec.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
export const realExec = (cmd, args) => execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 256 * 1024 * 1024 }); // a full pane scrollback exceeds the 1 MiB default
|
|
3
|
+
/** Run a command with the terminal attached (output visible to the user), throw if it fails. */
|
|
4
|
+
export const run = (cmd, args, cwd, env = process.env) => {
|
|
5
|
+
const result = spawnSync(cmd, args, { cwd, env, stdio: 'inherit' });
|
|
6
|
+
if (result.status !== 0)
|
|
7
|
+
throw new Error(`${cmd} ${args.join(' ')} failed with exit code ${result.status}`);
|
|
8
|
+
};
|
|
9
|
+
// docker compose prints one JSON object per line (older versions: a single JSON array)
|
|
10
|
+
export const jsonLines = (out) => out.split('\n').filter(l => l.trim()).flatMap(l => JSON.parse(l));
|
|
11
|
+
//# sourceMappingURL=exec.js.map
|
package/exec.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec.js","sourceRoot":"","sources":["exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAI5D,MAAM,CAAC,MAAM,QAAQ,GAAS,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CAAA,CAAC,mDAAmD;AAE/M,gGAAgG;AAChG,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,IAAc,EAAE,GAAW,EAAE,MAAyB,OAAO,CAAC,GAAG,EAAE,EAAE;IACpG,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;IACnE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,0BAA0B,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;AAC7G,CAAC,CAAA;AAED,uFAAuF;AACvF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,GAAW,EAAS,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA","sourcesContent":["import { execFileSync, spawnSync } from 'node:child_process'\n\nexport type Exec = (cmd: string, args: string[]) => string\n\nexport const realExec: Exec = (cmd, args) => execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 256 * 1024 * 1024 }) // a full pane scrollback exceeds the 1 MiB default\n\n/** Run a command with the terminal attached (output visible to the user), throw if it fails. */\nexport const run = (cmd: string, args: string[], cwd: string, env: NodeJS.ProcessEnv = process.env) => {\n const result = spawnSync(cmd, args, { cwd, env, stdio: 'inherit' })\n if (result.status !== 0) throw new Error(`${cmd} ${args.join(' ')} failed with exit code ${result.status}`)\n}\n\n// docker compose prints one JSON object per line (older versions: a single JSON array)\nexport const jsonLines = (out: string): any[] => out.split('\\n').filter(l => l.trim()).flatMap(l => JSON.parse(l))\n"]}
|
package/exec.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/exec.test.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'assert';
|
|
3
|
+
import { realExec } from './exec.js';
|
|
4
|
+
describe('realExec', () => {
|
|
5
|
+
it('reads outputs larger than 1 MiB (a full pane scrollback)', () => {
|
|
6
|
+
const out = realExec('node', ['-e', "process.stdout.write('x'.repeat(3 * 1024 * 1024))"]);
|
|
7
|
+
assert.equal(out.length, 3 * 1024 * 1024);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
//# sourceMappingURL=exec.test.js.map
|
package/exec.test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec.test.js","sourceRoot":"","sources":["exec.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,QAAQ,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEpC,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,mDAAmD,CAAC,CAAC,CAAA;QACzF,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { realExec } from './exec.js'\n\ndescribe('realExec', () => {\n it('reads outputs larger than 1 MiB (a full pane scrollback)', () => {\n const out = realExec('node', ['-e', \"process.stdout.write('x'.repeat(3 * 1024 * 1024))\"])\n assert.equal(out.length, 3 * 1024 * 1024)\n })\n})\n"]}
|
package/launch.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Exec } from './exec.js';
|
|
2
|
+
import { type SessionState } from './zellij.js';
|
|
3
|
+
export declare const launchPlan: (state: SessionState, session: string, services: string[]) => {
|
|
4
|
+
deleteFirst: boolean;
|
|
5
|
+
args: string[];
|
|
6
|
+
warning?: string;
|
|
7
|
+
};
|
|
8
|
+
declare const realSpawn: (args: string[], env: NodeJS.ProcessEnv) => number;
|
|
9
|
+
export declare const launch: (root: string, services: string[], deps?: {
|
|
10
|
+
exec?: Exec;
|
|
11
|
+
spawn?: typeof realSpawn;
|
|
12
|
+
}) => number;
|
|
13
|
+
export {};
|
package/launch.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { readEnvFile } from './config.js';
|
|
5
|
+
import { realExec } from './exec.js';
|
|
6
|
+
import { sessionName, sessionState, checkVersion } from './zellij.js';
|
|
7
|
+
export const launchPlan = (state, session, services) => {
|
|
8
|
+
if (state === 'running') {
|
|
9
|
+
const plan = { deleteFirst: false, args: ['attach', session] };
|
|
10
|
+
if (services.length)
|
|
11
|
+
plan.warning = `session ${session} is already running, --services ignored: add them with "npx --no df-dev up ${services.join(' ')}"`;
|
|
12
|
+
return plan;
|
|
13
|
+
}
|
|
14
|
+
// an EXITED session keeps its name reserved and makes the creation fail;
|
|
15
|
+
// --layout with --session would add a tab to an existing session instead of creating one
|
|
16
|
+
return { deleteFirst: state === 'exited', args: ['--session', session, '--new-session-with-layout', '.zellij.kdl'] };
|
|
17
|
+
};
|
|
18
|
+
const realSpawn = (args, env) => spawnSync('zellij', args, { stdio: 'inherit', env }).status ?? 1;
|
|
19
|
+
export const launch = (root, services, deps = {}) => {
|
|
20
|
+
const exec = deps.exec ?? realExec;
|
|
21
|
+
if (!existsSync(path.join(root, '.env')))
|
|
22
|
+
throw new Error('no .env in this worktree, run "npx --no df-dev setup" first');
|
|
23
|
+
checkVersion(exec);
|
|
24
|
+
const session = sessionName(root);
|
|
25
|
+
const plan = launchPlan(sessionState(exec, session), session, services);
|
|
26
|
+
if (plan.warning)
|
|
27
|
+
console.warn(plan.warning);
|
|
28
|
+
if (plan.deleteFirst)
|
|
29
|
+
exec('zellij', ['delete-session', session]);
|
|
30
|
+
const env = { ...process.env, ...readEnvFile(root) };
|
|
31
|
+
if (services.length)
|
|
32
|
+
env.COMPOSE_PROFILES = services.join(',');
|
|
33
|
+
process.chdir(root);
|
|
34
|
+
return (deps.spawn ?? realSpawn)(plan.args, env);
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=launch.js.map
|
package/launch.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launch.js","sourceRoot":"","sources":["launch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAa,MAAM,WAAW,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAqB,MAAM,aAAa,CAAA;AAExF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAmB,EAAE,OAAe,EAAE,QAAkB,EAAE,EAAE;IACrF,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,GAA+D,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAA;QAC1H,IAAI,QAAQ,CAAC,MAAM;YAAE,IAAI,CAAC,OAAO,GAAG,WAAW,OAAO,8EAA8E,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;QACzJ,OAAO,IAAI,CAAA;IACb,CAAC;IACD,yEAAyE;IACzE,yFAAyF;IACzF,OAAO,EAAE,WAAW,EAAE,KAAK,KAAK,QAAQ,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,OAAO,EAAE,2BAA2B,EAAE,aAAa,CAAC,EAAE,CAAA;AACtH,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,CAAC,IAAc,EAAE,GAAsB,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,CAAA;AAE9H,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,QAAkB,EAAE,OAAkD,EAAE,EAAE,EAAE;IAC/G,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAA;IAClC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAA;IACxH,YAAY,CAAC,IAAI,CAAC,CAAA;IAClB,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,CAAA;IACjC,MAAM,IAAI,GAAG,UAAU,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAA;IACvE,IAAI,IAAI,CAAC,OAAO;QAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC5C,IAAI,IAAI,CAAC,WAAW;QAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAA;IACjE,MAAM,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,WAAW,CAAC,IAAI,CAAC,EAAE,CAAA;IACpD,IAAI,QAAQ,CAAC,MAAM;QAAE,GAAG,CAAC,gBAAgB,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC9D,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACnB,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;AAClD,CAAC,CAAA","sourcesContent":["import { existsSync } from 'node:fs'\nimport path from 'node:path'\nimport { spawnSync } from 'node:child_process'\nimport { readEnvFile } from './config.js'\nimport { realExec, type Exec } from './exec.js'\nimport { sessionName, sessionState, checkVersion, type SessionState } from './zellij.js'\n\nexport const launchPlan = (state: SessionState, session: string, services: string[]) => {\n if (state === 'running') {\n const plan: { deleteFirst: boolean, args: string[], warning?: string } = { deleteFirst: false, args: ['attach', session] }\n if (services.length) plan.warning = `session ${session} is already running, --services ignored: add them with \"npx --no df-dev up ${services.join(' ')}\"`\n return plan\n }\n // an EXITED session keeps its name reserved and makes the creation fail;\n // --layout with --session would add a tab to an existing session instead of creating one\n return { deleteFirst: state === 'exited', args: ['--session', session, '--new-session-with-layout', '.zellij.kdl'] }\n}\n\nconst realSpawn = (args: string[], env: NodeJS.ProcessEnv) => spawnSync('zellij', args, { stdio: 'inherit', env }).status ?? 1\n\nexport const launch = (root: string, services: string[], deps: { exec?: Exec, spawn?: typeof realSpawn } = {}) => {\n const exec = deps.exec ?? realExec\n if (!existsSync(path.join(root, '.env'))) throw new Error('no .env in this worktree, run \"npx --no df-dev setup\" first')\n checkVersion(exec)\n const session = sessionName(root)\n const plan = launchPlan(sessionState(exec, session), session, services)\n if (plan.warning) console.warn(plan.warning)\n if (plan.deleteFirst) exec('zellij', ['delete-session', session])\n const env = { ...process.env, ...readEnvFile(root) }\n if (services.length) env.COMPOSE_PROFILES = services.join(',')\n process.chdir(root)\n return (deps.spawn ?? realSpawn)(plan.args, env)\n}\n"]}
|
package/launch.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/launch.test.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'assert';
|
|
3
|
+
import { launchPlan } from './launch.js';
|
|
4
|
+
describe('launchPlan', () => {
|
|
5
|
+
it('creates a missing session with the layout', () => {
|
|
6
|
+
assert.deepEqual(launchPlan('absent', 'portals', []), { deleteFirst: false, args: ['--session', 'portals', '--new-session-with-layout', '.zellij.kdl'] });
|
|
7
|
+
});
|
|
8
|
+
it('deletes an EXITED session before creating it again', () => {
|
|
9
|
+
assert.equal(launchPlan('exited', 'portals', []).deleteFirst, true);
|
|
10
|
+
});
|
|
11
|
+
it('attaches to a running session and never kills it', () => {
|
|
12
|
+
const plan = launchPlan('running', 'portals', []);
|
|
13
|
+
assert.deepEqual(plan, { deleteFirst: false, args: ['attach', 'portals'] });
|
|
14
|
+
});
|
|
15
|
+
it('warns that --services is ignored when attaching to a running session', () => {
|
|
16
|
+
assert.match(launchPlan('running', 'portals', ['agents']).warning ?? '', /df-dev up agents/);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
//# sourceMappingURL=launch.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launch.test.js","sourceRoot":"","sources":["launch.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,QAAQ,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,EAAE,SAAS,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,2BAA2B,EAAE,aAAa,CAAC,EAAE,CAAC,CAAA;IAC3J,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAA;IACrE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,EAAE,SAAS,EAAE,EAAE,CAAC,CAAA;QACjD,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,CAAA;IAC7E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,EAAE,kBAAkB,CAAC,CAAA;IAC9F,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { launchPlan } from './launch.js'\n\ndescribe('launchPlan', () => {\n it('creates a missing session with the layout', () => {\n assert.deepEqual(launchPlan('absent', 'portals', []), { deleteFirst: false, args: ['--session', 'portals', '--new-session-with-layout', '.zellij.kdl'] })\n })\n\n it('deletes an EXITED session before creating it again', () => {\n assert.equal(launchPlan('exited', 'portals', []).deleteFirst, true)\n })\n\n it('attaches to a running session and never kills it', () => {\n const plan = launchPlan('running', 'portals', [])\n assert.deepEqual(plan, { deleteFirst: false, args: ['attach', 'portals'] })\n })\n\n it('warns that --services is ignored when attaching to a running session', () => {\n assert.match(launchPlan('running', 'portals', ['agents']).warning ?? '', /df-dev up agents/)\n })\n})\n"]}
|
package/mem-cap.sh
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run a dev process under a hard memory cap, so a runaway one is killed by its own cgroup
|
|
3
|
+
# instead of driving the whole host out of memory. A V8 heap limit is not enough: most of
|
|
4
|
+
# vite's memory is rolldown's native memory. No-op where systemd-run is unavailable.
|
|
5
|
+
# Usage: df-mem-cap vite --port 5173 (DEV_MEMORY_MAX=4G by default)
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
: "${DEV_MEMORY_MAX:=4G}"
|
|
9
|
+
|
|
10
|
+
if command -v systemd-run >/dev/null 2>&1 && [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -S "${XDG_RUNTIME_DIR}/systemd/private" ]; then
|
|
11
|
+
# systemd-run does not see the PATH additions of npm (node_modules/.bin)
|
|
12
|
+
cmd=$(command -v "$1") || { echo "df-mem-cap: command not found: $1" >&2; exit 127; }
|
|
13
|
+
shift
|
|
14
|
+
# inside a capped cgroup node sizes its heap from the cap (about half of it: 2.2 GB for 4G),
|
|
15
|
+
# so vite died of a V8 heap OOM long before the cgroup limit; give the heap 3/4 of the cap
|
|
16
|
+
# and leave the rest to native memory (rolldown), unless the caller already chose a size
|
|
17
|
+
if [[ "${NODE_OPTIONS:-}" != *--max-old-space-size* ]]; then
|
|
18
|
+
case "$DEV_MEMORY_MAX" in
|
|
19
|
+
*[Gg]) cap_mb=$(( ${DEV_MEMORY_MAX%[Gg]} * 1024 )) ;;
|
|
20
|
+
*[Mm]) cap_mb=${DEV_MEMORY_MAX%[Mm]} ;;
|
|
21
|
+
*) cap_mb= ;;
|
|
22
|
+
esac
|
|
23
|
+
[ -n "$cap_mb" ] && export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=$(( cap_mb * 3 / 4 ))"
|
|
24
|
+
fi
|
|
25
|
+
# --collect: otherwise every OOM-killed run leaves a failed scope unit to reset by hand
|
|
26
|
+
exec systemd-run --user --scope --quiet --collect \
|
|
27
|
+
--unit="dev-$(basename "$cmd")-$$" \
|
|
28
|
+
-p MemoryMax="$DEV_MEMORY_MAX" \
|
|
29
|
+
-p MemorySwapMax=0 \
|
|
30
|
+
-- "$cmd" "$@"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
exec "$@"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/mem-cap.test.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'assert';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, mkdtempSync, writeFileSync, chmodSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
const script = path.join(import.meta.dirname, 'mem-cap.sh');
|
|
8
|
+
const runtimeDir = process.env.XDG_RUNTIME_DIR;
|
|
9
|
+
// the cap only applies with a user systemd instance: the script checks its socket
|
|
10
|
+
const hasSystemd = !!runtimeDir && existsSync(path.join(runtimeDir, 'systemd/private'));
|
|
11
|
+
// fake systemd-run on the PATH: prints the NODE_OPTIONS the capped command would get
|
|
12
|
+
const fakeBin = () => {
|
|
13
|
+
const bin = mkdtempSync(path.join(tmpdir(), 'fakebin-'));
|
|
14
|
+
writeFileSync(path.join(bin, 'systemd-run'), '#!/usr/bin/env bash\necho "$NODE_OPTIONS"\n');
|
|
15
|
+
chmodSync(path.join(bin, 'systemd-run'), 0o755);
|
|
16
|
+
return bin;
|
|
17
|
+
};
|
|
18
|
+
const nodeOptions = (env) => {
|
|
19
|
+
const res = spawnSync('bash', [script, 'node', '-e', ''], {
|
|
20
|
+
env: { ...process.env, PATH: `${fakeBin()}:${process.env.PATH}`, NODE_OPTIONS: '', ...env },
|
|
21
|
+
encoding: 'utf8'
|
|
22
|
+
});
|
|
23
|
+
assert.equal(res.status, 0, res.stderr);
|
|
24
|
+
return res.stdout.trim();
|
|
25
|
+
};
|
|
26
|
+
describe('df-mem-cap', { skip: !hasSystemd && 'no user systemd instance' }, () => {
|
|
27
|
+
it('gives the V8 heap 3/4 of the memory cap', () => {
|
|
28
|
+
assert.equal(nodeOptions({ DEV_MEMORY_MAX: '4G' }), '--max-old-space-size=3072');
|
|
29
|
+
assert.equal(nodeOptions({ DEV_MEMORY_MAX: '1536M' }), '--max-old-space-size=1152');
|
|
30
|
+
});
|
|
31
|
+
it('keeps the other node options', () => {
|
|
32
|
+
assert.equal(nodeOptions({ DEV_MEMORY_MAX: '2G', NODE_OPTIONS: '--no-warnings' }), '--no-warnings --max-old-space-size=1536');
|
|
33
|
+
});
|
|
34
|
+
it('never overrides a heap size chosen by the caller', () => {
|
|
35
|
+
assert.equal(nodeOptions({ NODE_OPTIONS: '--max-old-space-size=1000' }), '--max-old-space-size=1000');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
//# sourceMappingURL=mem-cap.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mem-cap.test.js","sourceRoot":"","sources":["mem-cap.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,QAAQ,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAC3E,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;AAC3D,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAA;AAC9C,kFAAkF;AAClF,MAAM,UAAU,GAAG,CAAC,CAAC,UAAU,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC,CAAA;AAEvF,qFAAqF;AACrF,MAAM,OAAO,GAAG,GAAG,EAAE;IACnB,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,UAAU,CAAC,CAAC,CAAA;IACxD,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,6CAA6C,CAAC,CAAA;IAC3F,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,KAAK,CAAC,CAAA;IAC/C,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAED,MAAM,WAAW,GAAG,CAAC,GAA2B,EAAE,EAAE;IAClD,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE;QACxD,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,GAAG,GAAG,EAAE;QAC3F,QAAQ,EAAE,MAAM;KACjB,CAAC,CAAA;IACF,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACvC,OAAO,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAA;AAED,QAAQ,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,CAAC,UAAU,IAAI,0BAA0B,EAAE,EAAE,GAAG,EAAE;IAC/E,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,EAAE,2BAA2B,CAAC,CAAA;QAChF,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC,EAAE,2BAA2B,CAAC,CAAA;IACrF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,YAAY,EAAE,eAAe,EAAE,CAAC,EAAE,yCAAyC,CAAC,CAAA;IAC/H,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,YAAY,EAAE,2BAA2B,EAAE,CAAC,EAAE,2BAA2B,CAAC,CAAA;IACvG,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, mkdtempSync, writeFileSync, chmodSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\n\nconst script = path.join(import.meta.dirname, 'mem-cap.sh')\nconst runtimeDir = process.env.XDG_RUNTIME_DIR\n// the cap only applies with a user systemd instance: the script checks its socket\nconst hasSystemd = !!runtimeDir && existsSync(path.join(runtimeDir, 'systemd/private'))\n\n// fake systemd-run on the PATH: prints the NODE_OPTIONS the capped command would get\nconst fakeBin = () => {\n const bin = mkdtempSync(path.join(tmpdir(), 'fakebin-'))\n writeFileSync(path.join(bin, 'systemd-run'), '#!/usr/bin/env bash\\necho \"$NODE_OPTIONS\"\\n')\n chmodSync(path.join(bin, 'systemd-run'), 0o755)\n return bin\n}\n\nconst nodeOptions = (env: Record<string, string>) => {\n const res = spawnSync('bash', [script, 'node', '-e', ''], {\n env: { ...process.env, PATH: `${fakeBin()}:${process.env.PATH}`, NODE_OPTIONS: '', ...env },\n encoding: 'utf8'\n })\n assert.equal(res.status, 0, res.stderr)\n return res.stdout.trim()\n}\n\ndescribe('df-mem-cap', { skip: !hasSystemd && 'no user systemd instance' }, () => {\n it('gives the V8 heap 3/4 of the memory cap', () => {\n assert.equal(nodeOptions({ DEV_MEMORY_MAX: '4G' }), '--max-old-space-size=3072')\n assert.equal(nodeOptions({ DEV_MEMORY_MAX: '1536M' }), '--max-old-space-size=1152')\n })\n\n it('keeps the other node options', () => {\n assert.equal(nodeOptions({ DEV_MEMORY_MAX: '2G', NODE_OPTIONS: '--no-warnings' }), '--no-warnings --max-old-space-size=1536')\n })\n\n it('never overrides a heap size chosen by the caller', () => {\n assert.equal(nodeOptions({ NODE_OPTIONS: '--max-old-space-size=1000' }), '--max-old-space-size=1000')\n })\n})\n"]}
|
package/optional.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type Obj = Record<string, any>;
|
|
2
|
+
/**
|
|
3
|
+
* Config overrides (dev/dev.json "optional") of the optional compose services that are not running.
|
|
4
|
+
* Exported as NODE_CONFIG by the api pane: node-config merges it over the config files, like a
|
|
5
|
+
* local-development file but without touching one the developer may already have.
|
|
6
|
+
*/
|
|
7
|
+
export declare const absentConfig: (optional: Record<string, Obj>, running: string[]) => Obj;
|
|
8
|
+
export {};
|
package/optional.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
2
|
+
const merge = (target, source) => {
|
|
3
|
+
for (const [key, value] of Object.entries(source)) {
|
|
4
|
+
target[key] = isObj(value) && isObj(target[key]) ? merge({ ...target[key] }, value) : value;
|
|
5
|
+
}
|
|
6
|
+
return target;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Config overrides (dev/dev.json "optional") of the optional compose services that are not running.
|
|
10
|
+
* Exported as NODE_CONFIG by the api pane: node-config merges it over the config files, like a
|
|
11
|
+
* local-development file but without touching one the developer may already have.
|
|
12
|
+
*/
|
|
13
|
+
export const absentConfig = (optional, running) => {
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const [service, overrides] of Object.entries(optional)) {
|
|
16
|
+
if (!running.includes(service))
|
|
17
|
+
merge(result, overrides);
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=optional.js.map
|
package/optional.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optional.js","sourceRoot":"","sources":["optional.ts"],"names":[],"mappings":"AAEA,MAAM,KAAK,GAAG,CAAC,CAAU,EAAY,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AAEhG,MAAM,KAAK,GAAG,CAAC,MAAW,EAAE,MAAW,EAAE,EAAE;IACzC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;IAC7F,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAA6B,EAAE,OAAiB,EAAE,EAAE;IAC/E,MAAM,MAAM,GAAQ,EAAE,CAAA;IACtB,KAAK,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5D,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAC1D,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC,CAAA","sourcesContent":["type Obj = Record<string, any>\n\nconst isObj = (v: unknown): v is Obj => typeof v === 'object' && v !== null && !Array.isArray(v)\n\nconst merge = (target: Obj, source: Obj) => {\n for (const [key, value] of Object.entries(source)) {\n target[key] = isObj(value) && isObj(target[key]) ? merge({ ...target[key] }, value) : value\n }\n return target\n}\n\n/**\n * Config overrides (dev/dev.json \"optional\") of the optional compose services that are not running.\n * Exported as NODE_CONFIG by the api pane: node-config merges it over the config files, like a\n * local-development file but without touching one the developer may already have.\n */\nexport const absentConfig = (optional: Record<string, Obj>, running: string[]) => {\n const result: Obj = {}\n for (const [service, overrides] of Object.entries(optional)) {\n if (!running.includes(service)) merge(result, overrides)\n }\n return result\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/optional.test.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'assert';
|
|
3
|
+
import { absentConfig } from './optional.js';
|
|
4
|
+
const optional = {
|
|
5
|
+
events: { privateEventsUrl: null },
|
|
6
|
+
clamav: { clamav: { active: false } },
|
|
7
|
+
agents: { privateAgentsUrl: null, agents: { chat: false } }
|
|
8
|
+
};
|
|
9
|
+
describe('absentConfig', () => {
|
|
10
|
+
it('merges the overrides of the optional services that are not running', () => {
|
|
11
|
+
assert.deepEqual(absentConfig(optional, ['mongodb', 'es', 'events']), {
|
|
12
|
+
clamav: { active: false },
|
|
13
|
+
privateAgentsUrl: null,
|
|
14
|
+
agents: { chat: false }
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
it('is empty when every optional service runs', () => {
|
|
18
|
+
assert.deepEqual(absentConfig(optional, ['events', 'clamav', 'agents']), {});
|
|
19
|
+
});
|
|
20
|
+
it('deep-merges overrides that touch the same object', () => {
|
|
21
|
+
assert.deepEqual(absentConfig({ a: { x: { one: 1 } }, b: { x: { two: 2 } } }, []), { x: { one: 1, two: 2 } });
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
//# sourceMappingURL=optional.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optional.test.js","sourceRoot":"","sources":["optional.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,QAAQ,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C,MAAM,QAAQ,GAAG;IACf,MAAM,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE;IAClC,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;IACrC,MAAM,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;CAC5D,CAAA;AAED,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,EAAE;YACpE,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;YACzB,gBAAgB,EAAE,IAAI;YACtB,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;SACxB,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC9E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;IAC/G,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { absentConfig } from './optional.js'\n\nconst optional = {\n events: { privateEventsUrl: null },\n clamav: { clamav: { active: false } },\n agents: { privateAgentsUrl: null, agents: { chat: false } }\n}\n\ndescribe('absentConfig', () => {\n it('merges the overrides of the optional services that are not running', () => {\n assert.deepEqual(absentConfig(optional, ['mongodb', 'es', 'events']), {\n clamav: { active: false },\n privateAgentsUrl: null,\n agents: { chat: false }\n })\n })\n\n it('is empty when every optional service runs', () => {\n assert.deepEqual(absentConfig(optional, ['events', 'clamav', 'agents']), {})\n })\n\n it('deep-merges overrides that touch the same object', () => {\n assert.deepEqual(absentConfig({ a: { x: { one: 1 } }, b: { x: { two: 2 } } }, []), { x: { one: 1, two: 2 } })\n })\n})\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@data-fair/lib-dev",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Dev environment tooling of the data-fair services: setup, zellij session, status, logs and restart for humans and agents.",
|
|
5
|
+
"files": [
|
|
6
|
+
"**/*.js",
|
|
7
|
+
"**/*.js.map",
|
|
8
|
+
"**/*.d.ts",
|
|
9
|
+
"mem-cap.sh",
|
|
10
|
+
"worktree.sh",
|
|
11
|
+
"worktree-delete.sh",
|
|
12
|
+
"status-pane.sh"
|
|
13
|
+
],
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"prepublishOnly": "cd .. && npm run prepublishOnly",
|
|
18
|
+
"build": "cd .. && npm run build"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^12.1.0"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"df-dev": "cli.js",
|
|
26
|
+
"df-mem-cap": "mem-cap.sh",
|
|
27
|
+
"df-worktree": "worktree.sh",
|
|
28
|
+
"df-worktree-delete": "worktree-delete.sh",
|
|
29
|
+
"df-status": "status-pane.sh"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/panes.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DevConfig } from './config.js';
|
|
2
|
+
import { type Exec } from './exec.js';
|
|
3
|
+
export declare const stripAnsi: (text: string) => string;
|
|
4
|
+
export declare const tailLines: (text: string, n: number) => string;
|
|
5
|
+
export declare const logs: (root: string, title: string, n: number, exec?: Exec) => string;
|
|
6
|
+
export type RestartResult = {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
message: string;
|
|
9
|
+
logs?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const checkReady: (url: string) => Promise<boolean>;
|
|
12
|
+
export declare const restart: (root: string, title: string, config: DevConfig, deps?: {
|
|
13
|
+
exec?: Exec;
|
|
14
|
+
ready?: (url: string) => Promise<boolean>;
|
|
15
|
+
sleep?: (ms: number) => Promise<void>;
|
|
16
|
+
now?: () => number;
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
}) => Promise<RestartResult>;
|