@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/README.md CHANGED
@@ -1,3 +1,39 @@
1
- # Temporary Holding Version
1
+ # @data-fair/lib-dev
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Dev environment of a data-fair service, for humans and agents. Needs zellij >= 0.44 and docker compose v2.
4
+
5
+ ## In a service
6
+
7
+ `dev/dev.json` declares what is specific to the repo:
8
+
9
+ ```json
10
+ {
11
+ "ports": ["NGINX_PORT", "DEV_API_PORT", "DEV_UI_PORT", "MONGO_PORT"],
12
+ "panes": { "api": { "ready": "http://localhost:${DEV_API_PORT}/api/ping" } },
13
+ "url": "http://${DEV_HOST}:${NGINX_PORT}/portals-manager/dev",
14
+ "optional": { "events": { "privateEventsUrl": "" } }
15
+ }
16
+ ```
17
+
18
+ - `ports`: names of the port variables written to `.env`, each a free port drawn in 20000-29999, so worktrees never collide. `.env` also gets `DEV_HOST` (`<branch>.localhost`) and `SSRF_PUBLIC_IPS`; `init-env --force` draws them again and keeps the variables added by hand.
19
+ - `panes`: zellij pane titles; `ready` is polled by `restart`.
20
+ - `url`: the page to open, the UI's `/dev` page when it has one (its dev navigation bar), printed by `df-dev status` so an agent knows where to point a browser (the footer pane of the layout prints it with a plain `echo`, no node process).
21
+ - `setup`: repo-specific commands `df-dev setup` runs before build-types (e.g. `npm -w parquet-writer run build`).
22
+ - `optional`: one key per optional compose service (a service with a profile), its value the config overrides to apply **while that service does not run**, e.g. `{ "events": { "privateEventsUrl": "" }, "clamav": { "clamav": { "active": false } } }`: without events the API does not call it. Values must pass the config schema. `df-dev absent-config` merges the overrides of every stopped service; the api pane does `export NODE_CONFIG=$(df-dev absent-config)` (node-config merges it over the config files, no local file to write).
23
+ - No key for the UI: `setup` builds the `ui` workspace when `ui/dist/index.html` is missing (the API's serve-spa reads it at startup, and the build generates the auto-import declarations `check-types` needs).
24
+
25
+ ## Commands
26
+
27
+ | Command | |
28
+ |---|---|
29
+ | `npx --no df-worktree <branch>` / `npx --no df-worktree-delete <branch> [-f]` | from the main checkout: create `../<repo>_<branch>` (npm ci + setup) / delete it with its branch, zellij session, and containers and volumes when it has a compose file (`-f`: skip the uncommitted / unpushed checks, e.g. a squash-merged branch; the branch's commit is printed); branch names: letters, digits, `.`, `_`, `-`, no `/`, and `<repo>_<branch>` (the zellij session) at most 60 chars |
30
+ | `npx --no df-dev setup` | first install / new worktree: `.env` if missing, `setup` commands, build-types, the `ui` build when `ui/dist/index.html` is missing |
31
+ | `npx --no df-dev init-env [--force]` | `.env` only |
32
+ | `npm run dev-zellij -- --services agents,events` | attach or create the session of this worktree (`df-dev zellij`) |
33
+ | `npx --no df-dev status` | for agents: containers, panes (running / exited / not ready), dev processes, memory |
34
+ | `df-status [--start-deps]` | the status pane: containers like `compose up --wait` and one RAM line, every 3 s, in bash (~4 MB); `--start-deps` starts the containers first |
35
+ | `npx --no df-dev logs <pane> [-n 100]` | last lines of a pane |
36
+ | `npx --no df-dev restart <pane>` | Ctrl+C, rerun, wait for readiness |
37
+ | `npx --no df-dev up <service…>` / `down <service…>` | optional compose services |
38
+ | `npx --no df-dev wait-deps` | first command of the panes that need the containers |
39
+ | `df-mem-cap <cmd>` | run a dev server under a 4G cgroup cap (`DEV_MEMORY_MAX`) |
package/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/cli.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import { findRoot, readDevConfig, readEnvFile } from './config.js';
4
+ import { initEnv } from './env.js';
5
+ import { setup } from './setup.js';
6
+ import { launch } from './launch.js';
7
+ import { logs, restart } from './panes.js';
8
+ import { composeUp, composeDown, waitDeps } from './compose.js';
9
+ import { collectStatus, renderStatus, checkReadiness } from './status.js';
10
+ import { absentConfig } from './optional.js';
11
+ import { realExec } from './exec.js';
12
+ const root = () => findRoot(process.cwd());
13
+ program.name('df-dev').description('Dev environment of a data-fair service: setup, zellij session, status, logs and restart.');
14
+ program.command('setup').description('create .env if missing, build types, build the ui workspace if missing (first install and new worktrees)')
15
+ .action(async () => { const r = root(); await setup(r, readDevConfig(r)); });
16
+ program.command('init-env').description('write .env with free ports if missing').option('--force', 'draw the ports and DEV_HOST again, keep the variables added by hand')
17
+ .action(async (opts) => {
18
+ const r = root();
19
+ const res = await initEnv(r, readDevConfig(r), { force: opts.force });
20
+ console.log(res.written ? `wrote ${res.file}` : `${res.file} exists, use --force to regenerate it`);
21
+ });
22
+ program.command('zellij').description('attach to the session of this worktree, or create it').option('--services <list>', 'optional compose services to start, comma separated', '')
23
+ .action((opts) => { process.exitCode = launch(root(), opts.services.split(',').filter(Boolean)); });
24
+ program.command('status').description('containers, panes (with readiness) and dev processes memory, for agents (the status pane is df-status)')
25
+ .action(async () => {
26
+ const r = root();
27
+ const data = collectStatus(r);
28
+ try {
29
+ data.panes = await checkReadiness(data.panes, readDevConfig(r), readEnvFile(r));
30
+ }
31
+ catch (err) {
32
+ data.panesError = `readiness not checked: ${err.message}`;
33
+ }
34
+ console.log(renderStatus(data, false));
35
+ });
36
+ program.command('absent-config').description('JSON config overrides (dev.json "optional") for the optional services that do not run, for NODE_CONFIG')
37
+ .action(() => {
38
+ const r = root();
39
+ const running = realExec('docker', ['compose', '--project-directory', r, 'ps', '--services', '--status', 'running']).split('\n').filter(Boolean);
40
+ console.log(JSON.stringify(absentConfig(readDevConfig(r).optional ?? {}, running)));
41
+ });
42
+ program.command('logs <pane>').description('last lines of a zellij pane, plain text').option('-n, --lines <n>', 'number of lines', '100')
43
+ .action((pane, opts) => { console.log(logs(root(), pane, Number(opts.lines))); });
44
+ program.command('restart <pane>').description('stop and rerun a zellij pane, then wait for its readiness URL')
45
+ .action(async (pane) => {
46
+ const r = root();
47
+ const res = await restart(r, pane, readDevConfig(r));
48
+ console.log(res.message);
49
+ if (res.logs)
50
+ console.log('--- last logs ---\n' + res.logs);
51
+ if (!res.ok)
52
+ process.exitCode = 1;
53
+ });
54
+ program.command('up <services...>').description('start optional compose services in the running env')
55
+ .action((services) => { composeUp(root(), services); });
56
+ program.command('down <services...>').description('stop optional compose services')
57
+ .action((services) => { composeDown(root(), services); });
58
+ program.command('wait-deps').description('wait until the containers of the active profiles are healthy (first command of process panes; the status pane starts them)')
59
+ .action(async () => { await waitDeps(root()); });
60
+ program.parseAsync().catch((err) => {
61
+ console.error(`df-dev: ${err.message}`);
62
+ process.exitCode = 1;
63
+ });
64
+ //# sourceMappingURL=cli.js.map
package/cli.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { program } from 'commander'\nimport { findRoot, readDevConfig, readEnvFile } from './config.js'\nimport { initEnv } from './env.js'\nimport { setup } from './setup.js'\nimport { launch } from './launch.js'\nimport { logs, restart } from './panes.js'\nimport { composeUp, composeDown, waitDeps } from './compose.js'\nimport { collectStatus, renderStatus, checkReadiness } from './status.js'\nimport { absentConfig } from './optional.js'\nimport { realExec } from './exec.js'\n\nconst root = () => findRoot(process.cwd())\n\nprogram.name('df-dev').description('Dev environment of a data-fair service: setup, zellij session, status, logs and restart.')\n\nprogram.command('setup').description('create .env if missing, build types, build the ui workspace if missing (first install and new worktrees)')\n .action(async () => { const r = root(); await setup(r, readDevConfig(r)) })\n\nprogram.command('init-env').description('write .env with free ports if missing').option('--force', 'draw the ports and DEV_HOST again, keep the variables added by hand')\n .action(async (opts) => {\n const r = root()\n const res = await initEnv(r, readDevConfig(r), { force: opts.force })\n console.log(res.written ? `wrote ${res.file}` : `${res.file} exists, use --force to regenerate it`)\n })\n\nprogram.command('zellij').description('attach to the session of this worktree, or create it').option('--services <list>', 'optional compose services to start, comma separated', '')\n .action((opts) => { process.exitCode = launch(root(), opts.services.split(',').filter(Boolean)) })\n\nprogram.command('status').description('containers, panes (with readiness) and dev processes memory, for agents (the status pane is df-status)')\n .action(async () => {\n const r = root()\n const data = collectStatus(r)\n try {\n data.panes = await checkReadiness(data.panes, readDevConfig(r), readEnvFile(r))\n } catch (err: any) {\n data.panesError = `readiness not checked: ${err.message}`\n }\n console.log(renderStatus(data, false))\n })\n\nprogram.command('absent-config').description('JSON config overrides (dev.json \"optional\") for the optional services that do not run, for NODE_CONFIG')\n .action(() => {\n const r = root()\n const running = realExec('docker', ['compose', '--project-directory', r, 'ps', '--services', '--status', 'running']).split('\\n').filter(Boolean)\n console.log(JSON.stringify(absentConfig(readDevConfig(r).optional ?? {}, running)))\n })\n\nprogram.command('logs <pane>').description('last lines of a zellij pane, plain text').option('-n, --lines <n>', 'number of lines', '100')\n .action((pane, opts) => { console.log(logs(root(), pane, Number(opts.lines))) })\n\nprogram.command('restart <pane>').description('stop and rerun a zellij pane, then wait for its readiness URL')\n .action(async (pane) => {\n const r = root()\n const res = await restart(r, pane, readDevConfig(r))\n console.log(res.message)\n if (res.logs) console.log('--- last logs ---\\n' + res.logs)\n if (!res.ok) process.exitCode = 1\n })\n\nprogram.command('up <services...>').description('start optional compose services in the running env')\n .action((services) => { composeUp(root(), services) })\n\nprogram.command('down <services...>').description('stop optional compose services')\n .action((services) => { composeDown(root(), services) })\n\nprogram.command('wait-deps').description('wait until the containers of the active profiles are healthy (first command of process panes; the status pane starts them)')\n .action(async () => { await waitDeps(root()) })\n\nprogram.parseAsync().catch((err) => {\n console.error(`df-dev: ${err.message}`)\n process.exitCode = 1\n})\n"]}
package/cli.test.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/cli.test.js ADDED
@@ -0,0 +1,44 @@
1
+ import { describe, it } from 'node:test';
2
+ import { strict as assert } from 'assert';
3
+ import { execFileSync, spawnSync } from 'node:child_process';
4
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import path from 'node:path';
7
+ const cli = path.join(import.meta.dirname, 'cli.js');
8
+ describe('df-dev cli', () => {
9
+ it('lists its commands', () => {
10
+ const out = execFileSync('node', [cli, '--help'], { encoding: 'utf8' });
11
+ for (const cmd of ['setup', 'init-env', 'zellij', 'status', 'logs', 'restart', 'up', 'down', 'wait-deps'])
12
+ assert.match(out, new RegExp(cmd));
13
+ });
14
+ it('runs init-env from a sub-directory of a repo', () => {
15
+ const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'));
16
+ mkdirSync(path.join(root, 'dev'));
17
+ mkdirSync(path.join(root, 'api'));
18
+ writeFileSync(path.join(root, 'dev/dev.json'), JSON.stringify({ ports: ['NGINX_PORT'] }));
19
+ const res = spawnSync('node', [cli, 'init-env'], { cwd: path.join(root, 'api'), encoding: 'utf8' });
20
+ assert.match(readFileSync(path.join(root, '.env'), 'utf8'), /NGINX_PORT=2\d{4}/);
21
+ assert.equal(res.stderr, '', 'outside a git repo, git errors must not leak');
22
+ });
23
+ it('fails with a readable message, not a stack trace', () => {
24
+ try {
25
+ execFileSync('node', [cli, 'status'], { cwd: tmpdir(), encoding: 'utf8', stdio: 'pipe' });
26
+ assert.fail('should have failed');
27
+ }
28
+ catch (err) {
29
+ assert.match(err.stderr, /dev\/dev\.json not found/);
30
+ assert.ok(!err.stderr.includes(' at '));
31
+ }
32
+ });
33
+ it('absent-config prints the overrides of the optional services that do not run', () => {
34
+ const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'));
35
+ mkdirSync(path.join(root, 'dev'));
36
+ writeFileSync(path.join(root, 'dev/dev.json'), JSON.stringify({ optional: { events: { privateEventsUrl: null }, clamav: { clamav: { active: false } } } }));
37
+ const bin = mkdtempSync(path.join(tmpdir(), 'fakebin-'));
38
+ writeFileSync(path.join(bin, 'docker'), '#!/usr/bin/env bash\nprintf "mongodb\\nevents\\n"\n');
39
+ chmodSync(path.join(bin, 'docker'), 0o755);
40
+ const res = spawnSync('node', [cli, 'absent-config'], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: bin + ':' + process.env.PATH } });
41
+ assert.deepEqual(JSON.parse(res.stdout), { clamav: { active: false } });
42
+ });
43
+ });
44
+ //# sourceMappingURL=cli.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.test.js","sourceRoot":"","sources":["cli.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { execFileSync, spawnSync } from 'node:child_process'\nimport { mkdtempSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\n\nconst cli = path.join(import.meta.dirname, 'cli.js')\n\ndescribe('df-dev cli', () => {\n it('lists its commands', () => {\n const out = execFileSync('node', [cli, '--help'], { encoding: 'utf8' })\n for (const cmd of ['setup', 'init-env', 'zellij', 'status', 'logs', 'restart', 'up', 'down', 'wait-deps']) assert.match(out, new RegExp(cmd))\n })\n\n it('runs init-env from a sub-directory of a repo', () => {\n const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'))\n mkdirSync(path.join(root, 'dev'))\n mkdirSync(path.join(root, 'api'))\n writeFileSync(path.join(root, 'dev/dev.json'), JSON.stringify({ ports: ['NGINX_PORT'] }))\n const res = spawnSync('node', [cli, 'init-env'], { cwd: path.join(root, 'api'), encoding: 'utf8' })\n assert.match(readFileSync(path.join(root, '.env'), 'utf8'), /NGINX_PORT=2\\d{4}/)\n assert.equal(res.stderr, '', 'outside a git repo, git errors must not leak')\n })\n\n it('fails with a readable message, not a stack trace', () => {\n try {\n execFileSync('node', [cli, 'status'], { cwd: tmpdir(), encoding: 'utf8', stdio: 'pipe' })\n assert.fail('should have failed')\n } catch (err: any) {\n assert.match(err.stderr, /dev\\/dev\\.json not found/)\n assert.ok(!err.stderr.includes(' at '))\n }\n })\n\n it('absent-config prints the overrides of the optional services that do not run', () => {\n const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'))\n mkdirSync(path.join(root, 'dev'))\n writeFileSync(path.join(root, 'dev/dev.json'), JSON.stringify({ optional: { events: { privateEventsUrl: null }, clamav: { clamav: { active: false } } } }))\n const bin = mkdtempSync(path.join(tmpdir(), 'fakebin-'))\n writeFileSync(path.join(bin, 'docker'), '#!/usr/bin/env bash\\nprintf \"mongodb\\\\nevents\\\\n\"\\n')\n chmodSync(path.join(bin, 'docker'), 0o755)\n const res = spawnSync('node', [cli, 'absent-config'], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: bin + ':' + process.env.PATH } })\n assert.deepEqual(JSON.parse(res.stdout), { clamav: { active: false } })\n })\n})\n"]}
package/compose.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { type Exec } from './exec.js';
2
+ export declare const composeUp: (root: string, services: string[], runFn?: (cmd: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv) => void) => void;
3
+ export declare const composeDown: (root: string, services: string[], runFn?: (cmd: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv) => void) => void;
4
+ type PsRow = {
5
+ Service: string;
6
+ State: string;
7
+ Health?: string;
8
+ ExitCode?: number;
9
+ };
10
+ export declare const pendingDeps: (services: string[], rows: PsRow[]) => string[];
11
+ /** Dependencies that exited with an error code: they will not become ready by waiting. */
12
+ export declare const failedDeps: (services: string[], rows: PsRow[]) => string[];
13
+ /**
14
+ * First command of the panes that need the containers. It only waits: the status pane starts
15
+ * the containers, and two panes running "compose up" at the same time conflict on container names.
16
+ * The services are those of the active profiles (COMPOSE_PROFILES comes from the zellij session env).
17
+ */
18
+ export declare const waitDeps: (root: string, deps?: {
19
+ exec?: Exec;
20
+ sleep?: (ms: number) => Promise<void>;
21
+ now?: () => number;
22
+ log?: (msg: string) => void;
23
+ }) => Promise<void>;
24
+ export {};
package/compose.js ADDED
@@ -0,0 +1,61 @@
1
+ import { run as realRun, realExec, jsonLines } from './exec.js';
2
+ // optional services live in a compose profile named after them; naming a service starts it whatever its profile
3
+ export const composeUp = (root, services, runFn = realRun) => {
4
+ runFn('docker', ['compose', 'up', '-d', '--wait', ...services], root);
5
+ };
6
+ export const composeDown = (root, services, runFn = realRun) => {
7
+ if (!services.length)
8
+ throw new Error('name the services to stop, the whole stack is stopped by the user when closing zellij');
9
+ runFn('docker', ['compose', 'stop', ...services], root);
10
+ };
11
+ export const pendingDeps = (services, rows) => services.flatMap(service => {
12
+ const row = rows.find(r => r.Service === service);
13
+ if (!row)
14
+ return [`${service} (not created)`];
15
+ if (row.State === 'running' && (!row.Health || row.Health === 'healthy'))
16
+ return [];
17
+ if (row.State === 'exited' && row.ExitCode === 0)
18
+ return []; // one-shot init container (s3-init…) that completed
19
+ return [`${service} (${row.Health || row.State}${row.State === 'exited' ? ` ${row.ExitCode}` : ''})`];
20
+ });
21
+ /** Dependencies that exited with an error code: they will not become ready by waiting. */
22
+ export const failedDeps = (services, rows) => rows
23
+ .filter(r => services.includes(r.Service) && r.State === 'exited' && r.ExitCode !== 0)
24
+ .map(r => `${r.Service} (exited ${r.ExitCode})`);
25
+ const WAIT_TIMEOUT = 180_000;
26
+ // a container with a restart policy is briefly "exited" between two attempts: fail on a lasting exit only
27
+ const FAILED_POLLS = 3;
28
+ /**
29
+ * First command of the panes that need the containers. It only waits: the status pane starts
30
+ * the containers, and two panes running "compose up" at the same time conflict on container names.
31
+ * The services are those of the active profiles (COMPOSE_PROFILES comes from the zellij session env).
32
+ */
33
+ export const waitDeps = async (root, deps = {}) => {
34
+ const exec = deps.exec ?? realExec;
35
+ const sleep = deps.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
36
+ const now = deps.now ?? Date.now;
37
+ const log = deps.log ?? console.log;
38
+ const compose = ['compose', '--project-directory', root];
39
+ const services = exec('docker', [...compose, 'config', '--services']).split('\n').filter(Boolean).sort();
40
+ const start = now();
41
+ let lastLog = -Infinity;
42
+ let failedPolls = 0;
43
+ while (true) {
44
+ const rows = jsonLines(exec('docker', [...compose, 'ps', '-a', '--format', 'json']));
45
+ const pending = pendingDeps(services, rows);
46
+ if (!pending.length)
47
+ return;
48
+ const failed = failedDeps(services, rows);
49
+ failedPolls = failed.length ? failedPolls + 1 : 0;
50
+ if (failedPolls >= FAILED_POLLS)
51
+ throw new Error(`containers failed: ${failed.join(', ')} (see "docker compose logs ${failed.map(f => f.split(' ')[0]).join(' ')}")`);
52
+ if (now() - start >= WAIT_TIMEOUT)
53
+ throw new Error(`containers not ready after 180s: ${pending.join(', ')} (see the status pane)`);
54
+ if (now() - lastLog >= 10_000) {
55
+ log(`waiting for containers: ${pending.join(', ')}`);
56
+ lastLog = now();
57
+ }
58
+ await sleep(1000);
59
+ }
60
+ };
61
+ //# sourceMappingURL=compose.js.map
package/compose.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.js","sourceRoot":"","sources":["compose.ts"],"names":[],"mappings":"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","sourcesContent":["import { run as realRun, realExec, jsonLines, type Exec } from './exec.js'\n\n// optional services live in a compose profile named after them; naming a service starts it whatever its profile\nexport const composeUp = (root: string, services: string[], runFn = realRun) => {\n runFn('docker', ['compose', 'up', '-d', '--wait', ...services], root)\n}\n\nexport const composeDown = (root: string, services: string[], runFn = realRun) => {\n if (!services.length) throw new Error('name the services to stop, the whole stack is stopped by the user when closing zellij')\n runFn('docker', ['compose', 'stop', ...services], root)\n}\n\ntype PsRow = { Service: string, State: string, Health?: string, ExitCode?: number }\n\nexport const pendingDeps = (services: string[], rows: PsRow[]) => services.flatMap(service => {\n const row = rows.find(r => r.Service === service)\n if (!row) return [`${service} (not created)`]\n if (row.State === 'running' && (!row.Health || row.Health === 'healthy')) return []\n if (row.State === 'exited' && row.ExitCode === 0) return [] // one-shot init container (s3-init…) that completed\n return [`${service} (${row.Health || row.State}${row.State === 'exited' ? ` ${row.ExitCode}` : ''})`]\n})\n\n/** Dependencies that exited with an error code: they will not become ready by waiting. */\nexport const failedDeps = (services: string[], rows: PsRow[]) => rows\n .filter(r => services.includes(r.Service) && r.State === 'exited' && r.ExitCode !== 0)\n .map(r => `${r.Service} (exited ${r.ExitCode})`)\n\nconst WAIT_TIMEOUT = 180_000\n// a container with a restart policy is briefly \"exited\" between two attempts: fail on a lasting exit only\nconst FAILED_POLLS = 3\n\n/**\n * First command of the panes that need the containers. It only waits: the status pane starts\n * the containers, and two panes running \"compose up\" at the same time conflict on container names.\n * The services are those of the active profiles (COMPOSE_PROFILES comes from the zellij session env).\n */\nexport const waitDeps = async (root: string, deps: { exec?: Exec, sleep?: (ms: number) => Promise<void>, now?: () => number, log?: (msg: string) => void } = {}) => {\n const exec = deps.exec ?? realExec\n const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)))\n const now = deps.now ?? Date.now\n const log = deps.log ?? console.log\n const compose = ['compose', '--project-directory', root]\n const services = exec('docker', [...compose, 'config', '--services']).split('\\n').filter(Boolean).sort()\n const start = now()\n let lastLog = -Infinity\n let failedPolls = 0\n while (true) {\n const rows: PsRow[] = jsonLines(exec('docker', [...compose, 'ps', '-a', '--format', 'json']))\n const pending = pendingDeps(services, rows)\n if (!pending.length) return\n const failed = failedDeps(services, rows)\n failedPolls = failed.length ? failedPolls + 1 : 0\n if (failedPolls >= FAILED_POLLS) throw new Error(`containers failed: ${failed.join(', ')} (see \"docker compose logs ${failed.map(f => f.split(' ')[0]).join(' ')}\")`)\n if (now() - start >= WAIT_TIMEOUT) throw new Error(`containers not ready after 180s: ${pending.join(', ')} (see the status pane)`)\n if (now() - lastLog >= 10_000) {\n log(`waiting for containers: ${pending.join(', ')}`)\n lastLog = now()\n }\n await sleep(1000)\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { describe, it } from 'node:test';
2
+ import { strict as assert } from 'assert';
3
+ import { composeUp, composeDown, waitDeps, pendingDeps } from './compose.js';
4
+ const recorder = () => {
5
+ const calls = [];
6
+ return { calls, run: (cmd, args) => { calls.push([cmd, ...args].join(' ')); } };
7
+ };
8
+ describe('compose', () => {
9
+ it('starts named services and waits for them', () => {
10
+ const r = recorder();
11
+ composeUp('/x', ['agents', 'events'], r.run);
12
+ assert.deepEqual(r.calls, ['docker compose up -d --wait agents events']);
13
+ });
14
+ it('refuses to stop the whole stack', () => {
15
+ assert.throws(() => composeDown('/x', [], recorder().run), /name the services/);
16
+ });
17
+ it('stops only the named services', () => {
18
+ const r = recorder();
19
+ composeDown('/x', ['agents'], r.run);
20
+ assert.deepEqual(r.calls, ['docker compose stop agents']);
21
+ });
22
+ it('lists the services of the active profiles that are not ready yet', () => {
23
+ const rows = [
24
+ { Service: 'mongo', State: 'running', Health: 'healthy' },
25
+ { Service: 'elasticsearch', State: 'running', Health: 'starting' },
26
+ { Service: 'nginx', State: 'running', Health: '' }
27
+ ];
28
+ assert.deepEqual(pendingDeps(['elasticsearch', 'mongo', 'nginx', 'simple-directory'], rows), ['elasticsearch (starting)', 'simple-directory (not created)']);
29
+ });
30
+ it('counts a one-shot container that exited with 0 as done (s3-init)', () => {
31
+ assert.deepEqual(pendingDeps(['s3-init', 'job'], [{ Service: 's3-init', State: 'exited', ExitCode: 0 }, { Service: 'job', State: 'exited', ExitCode: 1 }]), ['job (exited 1)']);
32
+ });
33
+ it('waits for the containers without ever starting them (no race between panes)', async () => {
34
+ let t = 0;
35
+ const calls = [];
36
+ const exec = (cmd, args) => {
37
+ calls.push(args.join(' '));
38
+ if (args.includes('config'))
39
+ return 'mongo\n';
40
+ return t < 3000 ? '' : JSON.stringify({ Service: 'mongo', State: 'running', Health: 'healthy' }) + '\n';
41
+ };
42
+ await waitDeps('/x', { exec, sleep: async (ms) => { t += ms; }, now: () => t, log: () => { } });
43
+ assert.ok(t >= 3000);
44
+ assert.ok(!calls.some(c => c.includes(' up')), calls.join(' | '));
45
+ });
46
+ it('fails at once when a dependency stays exited with an error code', async () => {
47
+ let t = 0;
48
+ const exec = (cmd, args) => args.includes('config') ? 'mongo\n' : JSON.stringify({ Service: 'mongo', State: 'exited', ExitCode: 1 }) + '\n';
49
+ await assert.rejects(waitDeps('/x', { exec, sleep: async (ms) => { t += ms; }, now: () => t, log: () => { } }), /containers failed: mongo \(exited 1\).*docker compose logs mongo/);
50
+ assert.ok(t < 10_000, `waited ${t} ms`);
51
+ });
52
+ it('keeps waiting for a container that restarts after a failed attempt', async () => {
53
+ let t = 0;
54
+ const states = [{ State: 'exited', ExitCode: 1 }, { State: 'restarting' }, { State: 'running', Health: 'healthy' }];
55
+ const exec = (cmd, args) => {
56
+ if (args.includes('config'))
57
+ return 'data-fair\n';
58
+ return JSON.stringify({ Service: 'data-fair', ...states[Math.min(Math.floor(t / 1000), states.length - 1)] }) + '\n';
59
+ };
60
+ await waitDeps('/x', { exec, sleep: async (ms) => { t += ms; }, now: () => t, log: () => { } });
61
+ assert.equal(t, 2000);
62
+ });
63
+ it('gives up after 3 minutes and names what is not ready', async () => {
64
+ let t = 0;
65
+ const exec = (cmd, args) => args.includes('config') ? 'mongo\n' : '';
66
+ await assert.rejects(waitDeps('/x', { exec, sleep: async (ms) => { t += ms; }, now: () => t, log: () => { } }), /not ready after 180s: mongo \(not created\).*status pane/);
67
+ });
68
+ });
69
+ //# sourceMappingURL=compose.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.test.js","sourceRoot":"","sources":["compose.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { composeUp, composeDown, waitDeps, pendingDeps } from './compose.js'\nimport type { Exec } from './exec.js'\n\nconst recorder = () => {\n const calls: string[] = []\n return { calls, run: (cmd: string, args: string[]) => { calls.push([cmd, ...args].join(' ')) } }\n}\n\ndescribe('compose', () => {\n it('starts named services and waits for them', () => {\n const r = recorder()\n composeUp('/x', ['agents', 'events'], r.run)\n assert.deepEqual(r.calls, ['docker compose up -d --wait agents events'])\n })\n\n it('refuses to stop the whole stack', () => {\n assert.throws(() => composeDown('/x', [], recorder().run), /name the services/)\n })\n\n it('stops only the named services', () => {\n const r = recorder()\n composeDown('/x', ['agents'], r.run)\n assert.deepEqual(r.calls, ['docker compose stop agents'])\n })\n\n it('lists the services of the active profiles that are not ready yet', () => {\n const rows = [\n { Service: 'mongo', State: 'running', Health: 'healthy' },\n { Service: 'elasticsearch', State: 'running', Health: 'starting' },\n { Service: 'nginx', State: 'running', Health: '' }\n ]\n assert.deepEqual(pendingDeps(['elasticsearch', 'mongo', 'nginx', 'simple-directory'], rows), ['elasticsearch (starting)', 'simple-directory (not created)'])\n })\n\n it('counts a one-shot container that exited with 0 as done (s3-init)', () => {\n assert.deepEqual(pendingDeps(['s3-init', 'job'], [{ Service: 's3-init', State: 'exited', ExitCode: 0 }, { Service: 'job', State: 'exited', ExitCode: 1 }]), ['job (exited 1)'])\n })\n\n it('waits for the containers without ever starting them (no race between panes)', async () => {\n let t = 0\n const calls: string[] = []\n const exec: Exec = (cmd, args) => {\n calls.push(args.join(' '))\n if (args.includes('config')) return 'mongo\\n'\n return t < 3000 ? '' : JSON.stringify({ Service: 'mongo', State: 'running', Health: 'healthy' }) + '\\n'\n }\n await waitDeps('/x', { exec, sleep: async (ms) => { t += ms }, now: () => t, log: () => {} })\n assert.ok(t >= 3000)\n assert.ok(!calls.some(c => c.includes(' up')), calls.join(' | '))\n })\n\n it('fails at once when a dependency stays exited with an error code', async () => {\n let t = 0\n const exec: Exec = (cmd, args) => args.includes('config') ? 'mongo\\n' : JSON.stringify({ Service: 'mongo', State: 'exited', ExitCode: 1 }) + '\\n'\n await assert.rejects(waitDeps('/x', { exec, sleep: async (ms) => { t += ms }, now: () => t, log: () => {} }), /containers failed: mongo \\(exited 1\\).*docker compose logs mongo/)\n assert.ok(t < 10_000, `waited ${t} ms`)\n })\n\n it('keeps waiting for a container that restarts after a failed attempt', async () => {\n let t = 0\n const states = [{ State: 'exited', ExitCode: 1 }, { State: 'restarting' }, { State: 'running', Health: 'healthy' }]\n const exec: Exec = (cmd, args) => {\n if (args.includes('config')) return 'data-fair\\n'\n return JSON.stringify({ Service: 'data-fair', ...states[Math.min(Math.floor(t / 1000), states.length - 1)] }) + '\\n'\n }\n await waitDeps('/x', { exec, sleep: async (ms) => { t += ms }, now: () => t, log: () => {} })\n assert.equal(t, 2000)\n })\n\n it('gives up after 3 minutes and names what is not ready', async () => {\n let t = 0\n const exec: Exec = (cmd, args) => args.includes('config') ? 'mongo\\n' : ''\n await assert.rejects(waitDeps('/x', { exec, sleep: async (ms) => { t += ms }, now: () => t, log: () => {} }), /not ready after 180s: mongo \\(not created\\).*status pane/)\n })\n})\n"]}
package/config.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export type PaneOptions = {
2
+ ready?: string;
3
+ };
4
+ export type DevConfig = {
5
+ ports: string[];
6
+ panes: Record<string, PaneOptions>;
7
+ url?: string;
8
+ setup?: string[];
9
+ optional?: Record<string, Record<string, unknown>>;
10
+ };
11
+ export declare const findRoot: (start: string) => string;
12
+ export declare const readDevConfig: (root: string) => DevConfig;
13
+ export declare const interpolate: (template: string, env: Record<string, string | undefined>) => string;
14
+ export declare const readEnvFile: (root: string) => Record<string, string>;
package/config.js ADDED
@@ -0,0 +1,32 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseEnv } from 'node:util';
4
+ export const findRoot = (start) => {
5
+ let dir = path.resolve(start);
6
+ while (true) {
7
+ if (existsSync(path.join(dir, 'dev/dev.json')))
8
+ return dir;
9
+ const parent = path.dirname(dir);
10
+ if (parent === dir)
11
+ throw new Error(`dev/dev.json not found in ${start} or its parents: this repo does not use the df-dev convention`);
12
+ dir = parent;
13
+ }
14
+ };
15
+ export const readDevConfig = (root) => {
16
+ const raw = JSON.parse(readFileSync(path.join(root, 'dev/dev.json'), 'utf8'));
17
+ return { ports: raw.ports ?? [], panes: raw.panes ?? {}, url: raw.url, setup: raw.setup ?? [], optional: raw.optional ?? {} };
18
+ };
19
+ export const interpolate = (template, env) => template.replace(/\$\{(\w+)\}/g, (_, name) => {
20
+ const value = env[name];
21
+ if (value === undefined)
22
+ throw new Error(`variable ${name} is not defined in .env (added to dev/dev.json after .env was created?), regenerate it with "npx --no df-dev init-env --force"`);
23
+ return value;
24
+ });
25
+ export const readEnvFile = (root) => {
26
+ const file = path.join(root, '.env');
27
+ if (!existsSync(file))
28
+ return {};
29
+ // same parser as node --env-file, so panes and df-dev read the same values
30
+ return parseEnv(readFileSync(file, 'utf8'));
31
+ };
32
+ //# sourceMappingURL=config.js.map
package/config.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAWpC,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAa,EAAU,EAAE;IAChD,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IAC7B,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;YAAE,OAAO,GAAG,CAAA;QAC1D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAChC,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,KAAK,+DAA+D,CAAC,CAAA;QACtI,GAAG,GAAG,MAAM,CAAA;IACd,CAAC;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAY,EAAa,EAAE;IACvD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAAC,CAAA;IAC7E,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAA;AAC/H,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAgB,EAAE,GAAuC,EAAE,EAAE,CACvF,QAAQ,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE;IACnD,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,CAAA;IACvB,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,gIAAgI,CAAC,CAAA;IAC1L,OAAO,KAAK,CAAA;AACd,CAAC,CAAC,CAAA;AAEJ,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAY,EAA0B,EAAE;IAClE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACpC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAA;IAChC,2EAA2E;IAC3E,OAAO,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAA2B,CAAA;AACvE,CAAC,CAAA","sourcesContent":["import { readFileSync, existsSync } from 'node:fs'\nimport path from 'node:path'\nimport { parseEnv } from 'node:util'\n\nexport type PaneOptions = { ready?: string }\nexport type DevConfig = {\n ports: string[]\n panes: Record<string, PaneOptions>\n url?: string\n setup?: string[]\n optional?: Record<string, Record<string, unknown>>\n}\n\nexport const findRoot = (start: string): string => {\n let dir = path.resolve(start)\n while (true) {\n if (existsSync(path.join(dir, 'dev/dev.json'))) return dir\n const parent = path.dirname(dir)\n if (parent === dir) throw new Error(`dev/dev.json not found in ${start} or its parents: this repo does not use the df-dev convention`)\n dir = parent\n }\n}\n\nexport const readDevConfig = (root: string): DevConfig => {\n const raw = JSON.parse(readFileSync(path.join(root, 'dev/dev.json'), 'utf8'))\n return { ports: raw.ports ?? [], panes: raw.panes ?? {}, url: raw.url, setup: raw.setup ?? [], optional: raw.optional ?? {} }\n}\n\nexport const interpolate = (template: string, env: Record<string, string | undefined>) =>\n template.replace(/\\$\\{(\\w+)\\}/g, (_, name: string) => {\n const value = env[name]\n if (value === undefined) throw new Error(`variable ${name} is not defined in .env (added to dev/dev.json after .env was created?), regenerate it with \"npx --no df-dev init-env --force\"`)\n return value\n })\n\nexport const readEnvFile = (root: string): Record<string, string> => {\n const file = path.join(root, '.env')\n if (!existsSync(file)) return {}\n // same parser as node --env-file, so panes and df-dev read the same values\n return parseEnv(readFileSync(file, 'utf8')) as Record<string, string>\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
package/config.test.js ADDED
@@ -0,0 +1,44 @@
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 { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import path from 'node:path';
7
+ import { findRoot, readDevConfig, interpolate, readEnvFile } from './config.js';
8
+ const makeRepo = (devJson) => {
9
+ const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'));
10
+ mkdirSync(path.join(root, 'dev'));
11
+ mkdirSync(path.join(root, 'api'));
12
+ writeFileSync(path.join(root, 'dev/dev.json'), JSON.stringify(devJson));
13
+ return root;
14
+ };
15
+ describe('config', () => {
16
+ it('finds the repo root from a sub-directory', () => {
17
+ const root = makeRepo({});
18
+ assert.equal(findRoot(path.join(root, 'api')), root);
19
+ });
20
+ it('fails clearly outside a repo using the convention', () => {
21
+ assert.throws(() => findRoot(tmpdir()), /dev\/dev\.json not found/);
22
+ });
23
+ it('fills defaults for missing sections', () => {
24
+ const root = makeRepo({ ports: ['NGINX_PORT'] });
25
+ assert.deepEqual(readDevConfig(root), { ports: ['NGINX_PORT'], panes: {}, url: undefined, setup: [], optional: {} });
26
+ });
27
+ it('interpolates variables', () => {
28
+ assert.equal(interpolate('http://localhost:${DEV_API_PORT}/api/ping', { DEV_API_PORT: '21000' }), 'http://localhost:21000/api/ping');
29
+ });
30
+ it('names a variable missing from .env and suggests regenerating it', () => {
31
+ assert.throws(() => interpolate('http://localhost:${DEV_PORTAL_PORT}/', {}), /DEV_PORTAL_PORT.*df-dev init-env --force/);
32
+ });
33
+ it('reads .env, ignoring comments and blank lines', () => {
34
+ const root = makeRepo({});
35
+ writeFileSync(path.join(root, '.env'), '# comment\nA=1\n\nB=x=y\n');
36
+ assert.deepEqual(readEnvFile(root), { A: '1', B: 'x=y' });
37
+ });
38
+ it('parses .env like dotenv (quotes, inline comments)', () => {
39
+ const root = makeRepo({});
40
+ writeFileSync(path.join(root, '.env'), 'A="a b"\nB=x # note\nC=\'single\'\n');
41
+ assert.deepEqual(readEnvFile(root), { A: 'a b', B: 'x', C: 'single' });
42
+ });
43
+ });
44
+ //# sourceMappingURL=config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.js","sourceRoot":"","sources":["config.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 { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\nimport { findRoot, readDevConfig, interpolate, readEnvFile } from './config.js'\n\nconst makeRepo = (devJson: object) => {\n const root = mkdtempSync(path.join(tmpdir(), 'df-dev-'))\n mkdirSync(path.join(root, 'dev'))\n mkdirSync(path.join(root, 'api'))\n writeFileSync(path.join(root, 'dev/dev.json'), JSON.stringify(devJson))\n return root\n}\n\ndescribe('config', () => {\n it('finds the repo root from a sub-directory', () => {\n const root = makeRepo({})\n assert.equal(findRoot(path.join(root, 'api')), root)\n })\n\n it('fails clearly outside a repo using the convention', () => {\n assert.throws(() => findRoot(tmpdir()), /dev\\/dev\\.json not found/)\n })\n\n it('fills defaults for missing sections', () => {\n const root = makeRepo({ ports: ['NGINX_PORT'] })\n assert.deepEqual(readDevConfig(root), { ports: ['NGINX_PORT'], panes: {}, url: undefined, setup: [], optional: {} })\n })\n\n it('interpolates variables', () => {\n assert.equal(interpolate('http://localhost:${DEV_API_PORT}/api/ping', { DEV_API_PORT: '21000' }), 'http://localhost:21000/api/ping')\n })\n\n it('names a variable missing from .env and suggests regenerating it', () => {\n assert.throws(() => interpolate('http://localhost:${DEV_PORTAL_PORT}/', {}), /DEV_PORTAL_PORT.*df-dev init-env --force/)\n })\n\n it('reads .env, ignoring comments and blank lines', () => {\n const root = makeRepo({})\n writeFileSync(path.join(root, '.env'), '# comment\\nA=1\\n\\nB=x=y\\n')\n assert.deepEqual(readEnvFile(root), { A: '1', B: 'x=y' })\n })\n\n it('parses .env like dotenv (quotes, inline comments)', () => {\n const root = makeRepo({})\n writeFileSync(path.join(root, '.env'), 'A=\"a b\"\\nB=x # note\\nC=\\'single\\'\\n')\n assert.deepEqual(readEnvFile(root), { A: 'a b', B: 'x', C: 'single' })\n })\n})\n"]}
package/env.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { type DevConfig } from './config.js';
2
+ export declare const PORT_MIN = 20000;
3
+ export declare const PORT_MAX = 29999;
4
+ export declare const isPortFree: (port: number) => Promise<boolean>;
5
+ export declare const pickPorts: (ports: string[], isFree?: (port: number) => Promise<boolean>, random?: () => number) => Promise<Record<string, number>>;
6
+ export declare const otherWorktreePorts: (root: string, worktreeList?: (root: string) => string) => Set<number>;
7
+ export declare const formatEnvValue: (value: string | number) => string;
8
+ export declare const hostLabel: (branch: string) => string;
9
+ export declare const initEnv: (root: string, config: DevConfig, opts?: {
10
+ force?: boolean;
11
+ branch?: string;
12
+ isFree?: (port: number) => Promise<boolean>;
13
+ reserved?: Set<number>;
14
+ random?: () => number;
15
+ }) => Promise<{
16
+ written: boolean;
17
+ file: string;
18
+ }>;