@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/worktree.sh ADDED
@@ -0,0 +1,164 @@
1
+ #!/bin/bash
2
+ # Create a git worktree for the given branch with full dev setup.
3
+ #
4
+ # Handles four cases:
5
+ # 1. Branch is currently checked out HERE (with or without local changes):
6
+ # stash the changes, switch this checkout back to the main branch, move
7
+ # the requested branch into the worktree, then re-apply the stash inside
8
+ # the worktree.
9
+ # 2. Branch exists locally but is not active here: just check it out in the
10
+ # worktree (e.g. coming back to a branch after a previous worktree was
11
+ # deleted).
12
+ # 3. Branch exists only on origin: create a tracking local branch in the
13
+ # worktree.
14
+ # 4. Brand new branch name: create from the current branch (or main if
15
+ # current is detached).
16
+
17
+ set -e
18
+
19
+ BRANCH_NAME=$1
20
+
21
+ if [ -z "$BRANCH_NAME" ]; then
22
+ echo "Error: Please provide a branch name." >&2
23
+ echo "Usage: df-worktree feat-xyz" >&2
24
+ exit 1
25
+ fi
26
+
27
+ # the branch name becomes a directory, a zellij session and a DEV_HOST label
28
+ if ! [[ "$BRANCH_NAME" =~ ^[A-Za-z0-9._-]{1,60}$ ]]; then
29
+ echo "Error: invalid branch name \"$BRANCH_NAME\": letters, digits, '.', '_' and '-' only, no \"/\", at most 60 characters (e.g. feat-xyz)" >&2
30
+ exit 1
31
+ fi
32
+
33
+ # the worktree goes next to the main checkout: refuse a subfolder or another worktree
34
+ TOPLEVEL=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "Error: not inside a git repository." >&2; exit 1; }
35
+ if [ "$TOPLEVEL" != "$(pwd -P)" ]; then
36
+ echo "Error: run df-worktree from the root of the repository: cd $TOPLEVEL" >&2
37
+ exit 1
38
+ fi
39
+ COMMON_DIR=$(git rev-parse --path-format=absolute --git-common-dir)
40
+ if [ "$(git rev-parse --absolute-git-dir)" != "$COMMON_DIR" ]; then
41
+ echo "Error: run df-worktree from the main checkout, not from a worktree: cd $(dirname "$COMMON_DIR")" >&2
42
+ exit 1
43
+ fi
44
+
45
+ REPO_NAME=$(basename "$PWD")
46
+ TARGET_DIR="../${REPO_NAME}_${BRANCH_NAME}"
47
+ ORIGIN_DIR="$PWD"
48
+
49
+ # the zellij session is named after the worktree folder and its socket path is capped at 107 bytes
50
+ # (/run/user/<uid>/zellij/contract_version_1/<session>): 60 characters leave room for the prefix
51
+ SESSION_MAX=60
52
+ if [ $(( ${#REPO_NAME} + 1 + ${#BRANCH_NAME} )) -gt $SESSION_MAX ]; then
53
+ echo "Error: branch name \"$BRANCH_NAME\" too long for this repository: the zellij session \"${REPO_NAME}_${BRANCH_NAME}\" must fit in $SESSION_MAX characters, so at most $(( SESSION_MAX - ${#REPO_NAME} - 1 )) characters for the branch" >&2
54
+ exit 1
55
+ fi
56
+
57
+ if [ -e "$TARGET_DIR" ]; then
58
+ echo "Error: $TARGET_DIR already exists."
59
+ echo "Run npx --no df-worktree-delete $BRANCH_NAME first if you want to recreate it."
60
+ exit 1
61
+ fi
62
+
63
+ # Detect main branch (main vs master) — prefer origin/HEAD, fall back to local refs
64
+ MAIN_BRANCH=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||' || true)
65
+ if [ -z "$MAIN_BRANCH" ]; then
66
+ if git show-ref --verify --quiet refs/heads/main; then
67
+ MAIN_BRANCH=main
68
+ elif git show-ref --verify --quiet refs/heads/master; then
69
+ MAIN_BRANCH=master
70
+ else
71
+ echo "Error: could not detect main branch (neither 'main' nor 'master' found)."
72
+ exit 1
73
+ fi
74
+ fi
75
+
76
+ echo "Fetching from origin"
77
+ git fetch origin --prune
78
+
79
+ CURRENT_BRANCH=$(git branch --show-current || true)
80
+ LOCAL_EXISTS=no
81
+ REMOTE_EXISTS=no
82
+ git show-ref --verify --quiet "refs/heads/$BRANCH_NAME" && LOCAL_EXISTS=yes
83
+ git show-ref --verify --quiet "refs/remotes/origin/$BRANCH_NAME" && REMOTE_EXISTS=yes
84
+
85
+ STASH_SHA=""
86
+ STASH_RESTORED=no
87
+ # a failure after the stash must not leave the user's changes buried in the shared stash stack
88
+ restore_stash() {
89
+ local code=$?
90
+ if [ "$code" -ne 0 ] && [ -n "$STASH_SHA" ] && [ "$STASH_RESTORED" != yes ]; then
91
+ if [ "$(git -C "$ORIGIN_DIR" branch --show-current)" = "$BRANCH_NAME" ] && git -C "$ORIGIN_DIR" stash apply -q "$STASH_SHA"; then
92
+ drop_stash
93
+ echo "Error: worktree setup failed, your local changes are back in $ORIGIN_DIR" >&2
94
+ else
95
+ echo "Error: worktree setup failed, your local changes are saved in the stash $STASH_SHA: git stash apply $STASH_SHA" >&2
96
+ fi
97
+ fi
98
+ }
99
+ drop_stash() {
100
+ local ref
101
+ ref=$(git -C "$ORIGIN_DIR" stash list --format='%gd %H' | awk -v sha="$STASH_SHA" '$2 == sha { print $1 }')
102
+ [ -n "$ref" ] && git -C "$ORIGIN_DIR" stash drop -q "$ref"
103
+ return 0
104
+ }
105
+ trap restore_stash EXIT
106
+
107
+ if [ "$LOCAL_EXISTS" = "yes" ] && [ "$CURRENT_BRANCH" = "$BRANCH_NAME" ]; then
108
+ echo "Branch '$BRANCH_NAME' is currently checked out in $PWD — migrating it to the worktree."
109
+ if [ -n "$(git status --porcelain)" ]; then
110
+ STASH_LABEL="worktree-migration-${BRANCH_NAME}-$(date +%s)"
111
+ echo "Stashing local changes (incl. untracked) as '$STASH_LABEL'"
112
+ git stash push --include-untracked --message "$STASH_LABEL"
113
+ # the stash stack is shared by every worktree: keep the sha, a stash@{n} index moves
114
+ STASH_SHA=$(git stash list --format='%H %gs' | grep -F "$STASH_LABEL" | head -1 | awk '{print $1}')
115
+ fi
116
+ echo "Switching this checkout to $MAIN_BRANCH"
117
+ git checkout "$MAIN_BRANCH"
118
+ fi
119
+
120
+ if [ "$LOCAL_EXISTS" = "yes" ]; then
121
+ echo "Reusing existing local branch '$BRANCH_NAME' in $TARGET_DIR"
122
+ git worktree add "$TARGET_DIR" "$BRANCH_NAME"
123
+ elif [ "$REMOTE_EXISTS" = "yes" ]; then
124
+ echo "Creating local branch '$BRANCH_NAME' tracking origin/$BRANCH_NAME in $TARGET_DIR"
125
+ git worktree add -b "$BRANCH_NAME" --track "$TARGET_DIR" "origin/$BRANCH_NAME"
126
+ else
127
+ SOURCE_BRANCH="${CURRENT_BRANCH:-$MAIN_BRANCH}"
128
+ echo "Creating new branch '$BRANCH_NAME' from '$SOURCE_BRANCH' in $TARGET_DIR"
129
+ git worktree add -b "$BRANCH_NAME" "$TARGET_DIR" "$SOURCE_BRANCH"
130
+ fi
131
+
132
+ if [ -n "$STASH_SHA" ]; then
133
+ echo "Re-applying stashed changes inside the worktree"
134
+ ( cd "$TARGET_DIR" && git stash apply "$STASH_SHA" )
135
+ STASH_RESTORED=yes
136
+ drop_stash
137
+ fi
138
+
139
+ cd "$TARGET_DIR"
140
+
141
+ if [ -d "$OLDPWD/.claude" ]; then
142
+ echo "Copy local Claude settings"
143
+ mkdir -p .claude
144
+ for f in "$OLDPWD/.claude"/*.json; do
145
+ [ -f "$f" ] && cp "$f" .claude/
146
+ done
147
+ fi
148
+
149
+ echo "npm ci"
150
+ npm ci
151
+
152
+ echo "df-dev setup (.env, types, UI build)"
153
+ ./node_modules/.bin/df-dev setup # local bin only, never fetched from the registry
154
+
155
+ echo "-----------------------------------------------"
156
+ echo "✅ Setup Complete!"
157
+ echo "Location: $TARGET_DIR"
158
+ echo "Branch: $BRANCH_NAME"
159
+ if [ -n "$STASH_SHA" ]; then
160
+ echo "(Local changes from your previous checkout were migrated.)"
161
+ fi
162
+ echo "-----------------------------------------------"
163
+ echo "Next step:"
164
+ echo " cd $TARGET_DIR && npm run dev-zellij"
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,145 @@
1
+ import { describe, it } from 'node:test';
2
+ import { strict as assert } from 'assert';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, realpathSync, existsSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import path from 'node:path';
7
+ const script = (name) => path.join(import.meta.dirname, name);
8
+ // git and the scripts must only see the throwaway repositories: a git hook (pre-push runs the tests)
9
+ // exports GIT_DIR and friends, which would point every command below at the real repository
10
+ const isolatedEnv = {
11
+ ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_'))),
12
+ GIT_AUTHOR_NAME: 't',
13
+ GIT_AUTHOR_EMAIL: 't@t',
14
+ GIT_COMMITTER_NAME: 't',
15
+ GIT_COMMITTER_EMAIL: 't@t',
16
+ GIT_CONFIG_GLOBAL: '/dev/null',
17
+ GIT_CONFIG_NOSYSTEM: '1',
18
+ GIT_CEILING_DIRECTORIES: tmpdir()
19
+ };
20
+ const run = (name, args, cwd = mkdtempSync(path.join(tmpdir(), 'wt-'))) => spawnSync('bash', [script(name), ...args], { cwd, encoding: 'utf8', env: isolatedEnv });
21
+ const git = (cwd, ...args) => {
22
+ const res = spawnSync('git', args, { cwd, encoding: 'utf8', env: isolatedEnv });
23
+ assert.equal(res.status, 0, `git ${args.join(' ')}: ${res.stderr}`);
24
+ return res.stdout.trim();
25
+ };
26
+ // a main checkout <tmp>/<repo> cloned from a bare origin, on master with one commit
27
+ const checkout = (repo = 'portals') => {
28
+ const tmp = mkdtempSync(path.join(tmpdir(), 'wt-repo-'));
29
+ git(tmp, 'init', '-q', '--bare', '-b', 'master', 'origin.git');
30
+ git(tmp, 'clone', '-q', path.join(tmp, 'origin.git'), repo);
31
+ const root = path.join(tmp, repo);
32
+ // never commit anywhere but in the throwaway clone
33
+ assert.equal(git(root, 'rev-parse', '--show-toplevel'), realpathSync(root));
34
+ writeFileSync(path.join(root, 'README.md'), 'x\n');
35
+ git(root, 'add', '.');
36
+ git(root, 'commit', '-q', '-m', 'init');
37
+ git(root, 'push', '-q', 'origin', 'master');
38
+ return { tmp, root };
39
+ };
40
+ describe('worktree scripts', () => {
41
+ for (const name of ['worktree.sh', 'worktree-delete.sh']) {
42
+ it(`${name} rejects a branch name with a slash`, () => {
43
+ const res = run(name, ['feat/x']);
44
+ assert.equal(res.status, 1);
45
+ assert.match(res.stderr, /invalid branch name "feat\/x".*no "\/"/);
46
+ });
47
+ it(`${name} rejects a branch name longer than 60 characters`, () => {
48
+ const res = run(name, ['a'.repeat(61)]);
49
+ assert.equal(res.status, 1);
50
+ assert.match(res.stderr, /invalid branch name/);
51
+ });
52
+ }
53
+ it('worktree.sh refuses a branch that makes the zellij session name too long for this repository', () => {
54
+ const { root } = checkout('simple-directory');
55
+ const res = run('worktree.sh', ['a'.repeat(50)], root);
56
+ assert.equal(res.status, 1);
57
+ assert.match(res.stderr, /zellij session .* must fit in 60 characters, so at most 43 characters/);
58
+ });
59
+ for (const name of ['worktree.sh', 'worktree-delete.sh']) {
60
+ it(`${name} refuses to run from a subfolder of the repository`, () => {
61
+ const { root } = checkout();
62
+ mkdirSync(path.join(root, 'api'));
63
+ const res = run(name, ['feat-x'], path.join(root, 'api'));
64
+ assert.equal(res.status, 1);
65
+ assert.match(res.stderr, /from the root of the repository/);
66
+ });
67
+ it(`${name} refuses to run from a linked worktree`, () => {
68
+ const { tmp, root } = checkout();
69
+ git(root, 'worktree', 'add', '-q', '-b', 'feat-a', path.join(tmp, 'portals_feat-a'));
70
+ const res = run(name, ['feat-x'], path.join(tmp, 'portals_feat-a'));
71
+ assert.equal(res.status, 1);
72
+ assert.match(res.stderr, /from the main checkout, not from a worktree/);
73
+ });
74
+ }
75
+ it('worktree.sh puts the local changes back when the setup fails after stashing them', () => {
76
+ const { tmp, root } = checkout();
77
+ git(root, 'checkout', '-q', '-b', 'feat-x');
78
+ writeFileSync(path.join(root, 'README.md'), 'work in progress\n');
79
+ writeFileSync(path.join(root, 'notes.txt'), 'untracked\n');
80
+ // master is checked out by another worktree: switching the main checkout back to it fails
81
+ git(root, 'worktree', 'add', '-q', path.join(tmp, 'portals_other'), 'master');
82
+ const res = run('worktree.sh', ['feat-x'], root);
83
+ assert.notEqual(res.status, 0);
84
+ assert.match(res.stderr, /your local changes are back in/);
85
+ assert.equal(git(root, 'branch', '--show-current'), 'feat-x');
86
+ assert.equal(readFileSync(path.join(root, 'README.md'), 'utf8'), 'work in progress\n');
87
+ assert.equal(readFileSync(path.join(root, 'notes.txt'), 'utf8'), 'untracked\n');
88
+ assert.equal(git(root, 'stash', 'list'), '');
89
+ });
90
+ describe('worktree-delete.sh', () => {
91
+ // a fake docker on PATH records its calls: the tests never touch the real one
92
+ const withFakeDocker = (tmp) => {
93
+ const bin = path.join(tmp, 'bin');
94
+ mkdirSync(bin);
95
+ writeFileSync(path.join(bin, 'docker'), `#!/bin/sh\necho "$@" >> ${path.join(tmp, 'docker-calls')}\n`, { mode: 0o755 });
96
+ return { ...isolatedEnv, PATH: `${bin}:${process.env.PATH}` };
97
+ };
98
+ const dockerCalls = (tmp) => existsSync(path.join(tmp, 'docker-calls')) ? readFileSync(path.join(tmp, 'docker-calls'), 'utf8') : '';
99
+ const worktree = (pushed) => {
100
+ const { tmp, root } = checkout();
101
+ const dir = path.join(tmp, 'portals_feat-a');
102
+ git(root, 'worktree', 'add', '-q', '-b', 'feat-a', dir);
103
+ writeFileSync(path.join(dir, 'a.txt'), 'a\n');
104
+ git(dir, 'add', '.');
105
+ git(dir, 'commit', '-q', '-m', 'a');
106
+ if (pushed)
107
+ git(dir, 'push', '-q', 'origin', 'feat-a');
108
+ return { tmp, root, dir, env: withFakeDocker(tmp) };
109
+ };
110
+ const del = (root, env, ...args) => spawnSync('bash', [script('worktree-delete.sh'), ...args], { cwd: root, encoding: 'utf8', env });
111
+ const branches = (root) => git(root, 'branch', '--format=%(refname:short)').split('\n');
112
+ it('deletes a worktree without a compose file, never calling docker, and its branch', () => {
113
+ const { tmp, root, dir, env } = worktree(true);
114
+ const res = del(root, env, 'feat-a');
115
+ assert.equal(res.status, 0, res.stderr);
116
+ assert.equal(existsSync(dir), false);
117
+ assert.deepEqual(branches(root), ['master']);
118
+ assert.equal(dockerCalls(tmp), '');
119
+ });
120
+ it('takes the containers down when the worktree has a compose file', () => {
121
+ const { tmp, root, dir, env } = worktree(true);
122
+ writeFileSync(path.join(dir, 'docker-compose.yml'), 'services: {}\n');
123
+ const res = del(root, env, 'feat-a', '-f');
124
+ assert.equal(res.status, 0, res.stderr);
125
+ assert.match(dockerCalls(tmp), /^compose --profile \* down -v --remove-orphans --rmi local$/m);
126
+ });
127
+ it('refuses unpushed commits and keeps the worktree and its branch', () => {
128
+ const { root, dir, env } = worktree(false);
129
+ const res = del(root, env, 'feat-a');
130
+ assert.equal(res.status, 1);
131
+ assert.match(res.stdout, /1 commit\(s\) not present on any origin ref/);
132
+ assert.equal(existsSync(dir), true);
133
+ assert.ok(branches(root).includes('feat-a'));
134
+ });
135
+ it('with --force, deletes an unpushed branch and prints its commit', () => {
136
+ const { root, dir, env } = worktree(false);
137
+ const sha = git(dir, 'rev-parse', '--short', 'HEAD');
138
+ const res = del(root, env, 'feat-a', '--force');
139
+ assert.equal(res.status, 0, res.stderr);
140
+ assert.match(res.stdout, new RegExp(`Deleting branch feat-a \\(was ${sha}\\)`));
141
+ assert.deepEqual(branches(root), ['master']);
142
+ });
143
+ });
144
+ });
145
+ //# sourceMappingURL=worktree.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree.test.js","sourceRoot":"","sources":["worktree.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { spawnSync } from 'node:child_process'\nimport { mkdtempSync, mkdirSync, writeFileSync, readFileSync, realpathSync, existsSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\n\nconst script = (name: string) => path.join(import.meta.dirname, name)\n// git and the scripts must only see the throwaway repositories: a git hook (pre-push runs the tests)\n// exports GIT_DIR and friends, which would point every command below at the real repository\nconst isolatedEnv = {\n ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_'))),\n GIT_AUTHOR_NAME: 't',\n GIT_AUTHOR_EMAIL: 't@t',\n GIT_COMMITTER_NAME: 't',\n GIT_COMMITTER_EMAIL: 't@t',\n GIT_CONFIG_GLOBAL: '/dev/null',\n GIT_CONFIG_NOSYSTEM: '1',\n GIT_CEILING_DIRECTORIES: tmpdir()\n}\nconst run = (name: string, args: string[], cwd = mkdtempSync(path.join(tmpdir(), 'wt-'))) => spawnSync('bash', [script(name), ...args], { cwd, encoding: 'utf8', env: isolatedEnv })\n\nconst git = (cwd: string, ...args: string[]) => {\n const res = spawnSync('git', args, { cwd, encoding: 'utf8', env: isolatedEnv })\n assert.equal(res.status, 0, `git ${args.join(' ')}: ${res.stderr}`)\n return res.stdout.trim()\n}\n// a main checkout <tmp>/<repo> cloned from a bare origin, on master with one commit\nconst checkout = (repo = 'portals') => {\n const tmp = mkdtempSync(path.join(tmpdir(), 'wt-repo-'))\n git(tmp, 'init', '-q', '--bare', '-b', 'master', 'origin.git')\n git(tmp, 'clone', '-q', path.join(tmp, 'origin.git'), repo)\n const root = path.join(tmp, repo)\n // never commit anywhere but in the throwaway clone\n assert.equal(git(root, 'rev-parse', '--show-toplevel'), realpathSync(root))\n writeFileSync(path.join(root, 'README.md'), 'x\\n')\n git(root, 'add', '.')\n git(root, 'commit', '-q', '-m', 'init')\n git(root, 'push', '-q', 'origin', 'master')\n return { tmp, root }\n}\n\ndescribe('worktree scripts', () => {\n for (const name of ['worktree.sh', 'worktree-delete.sh']) {\n it(`${name} rejects a branch name with a slash`, () => {\n const res = run(name, ['feat/x'])\n assert.equal(res.status, 1)\n assert.match(res.stderr, /invalid branch name \"feat\\/x\".*no \"\\/\"/)\n })\n\n it(`${name} rejects a branch name longer than 60 characters`, () => {\n const res = run(name, ['a'.repeat(61)])\n assert.equal(res.status, 1)\n assert.match(res.stderr, /invalid branch name/)\n })\n }\n\n it('worktree.sh refuses a branch that makes the zellij session name too long for this repository', () => {\n const { root } = checkout('simple-directory')\n const res = run('worktree.sh', ['a'.repeat(50)], root)\n assert.equal(res.status, 1)\n assert.match(res.stderr, /zellij session .* must fit in 60 characters, so at most 43 characters/)\n })\n\n for (const name of ['worktree.sh', 'worktree-delete.sh']) {\n it(`${name} refuses to run from a subfolder of the repository`, () => {\n const { root } = checkout()\n mkdirSync(path.join(root, 'api'))\n const res = run(name, ['feat-x'], path.join(root, 'api'))\n assert.equal(res.status, 1)\n assert.match(res.stderr, /from the root of the repository/)\n })\n\n it(`${name} refuses to run from a linked worktree`, () => {\n const { tmp, root } = checkout()\n git(root, 'worktree', 'add', '-q', '-b', 'feat-a', path.join(tmp, 'portals_feat-a'))\n const res = run(name, ['feat-x'], path.join(tmp, 'portals_feat-a'))\n assert.equal(res.status, 1)\n assert.match(res.stderr, /from the main checkout, not from a worktree/)\n })\n }\n\n it('worktree.sh puts the local changes back when the setup fails after stashing them', () => {\n const { tmp, root } = checkout()\n git(root, 'checkout', '-q', '-b', 'feat-x')\n writeFileSync(path.join(root, 'README.md'), 'work in progress\\n')\n writeFileSync(path.join(root, 'notes.txt'), 'untracked\\n')\n // master is checked out by another worktree: switching the main checkout back to it fails\n git(root, 'worktree', 'add', '-q', path.join(tmp, 'portals_other'), 'master')\n const res = run('worktree.sh', ['feat-x'], root)\n assert.notEqual(res.status, 0)\n assert.match(res.stderr, /your local changes are back in/)\n assert.equal(git(root, 'branch', '--show-current'), 'feat-x')\n assert.equal(readFileSync(path.join(root, 'README.md'), 'utf8'), 'work in progress\\n')\n assert.equal(readFileSync(path.join(root, 'notes.txt'), 'utf8'), 'untracked\\n')\n assert.equal(git(root, 'stash', 'list'), '')\n })\n\n describe('worktree-delete.sh', () => {\n // a fake docker on PATH records its calls: the tests never touch the real one\n const withFakeDocker = (tmp: string) => {\n const bin = path.join(tmp, 'bin')\n mkdirSync(bin)\n writeFileSync(path.join(bin, 'docker'), `#!/bin/sh\\necho \"$@\" >> ${path.join(tmp, 'docker-calls')}\\n`, { mode: 0o755 })\n return { ...isolatedEnv, PATH: `${bin}:${process.env.PATH}` }\n }\n const dockerCalls = (tmp: string) => existsSync(path.join(tmp, 'docker-calls')) ? readFileSync(path.join(tmp, 'docker-calls'), 'utf8') : ''\n const worktree = (pushed: boolean) => {\n const { tmp, root } = checkout()\n const dir = path.join(tmp, 'portals_feat-a')\n git(root, 'worktree', 'add', '-q', '-b', 'feat-a', dir)\n writeFileSync(path.join(dir, 'a.txt'), 'a\\n')\n git(dir, 'add', '.')\n git(dir, 'commit', '-q', '-m', 'a')\n if (pushed) git(dir, 'push', '-q', 'origin', 'feat-a')\n return { tmp, root, dir, env: withFakeDocker(tmp) }\n }\n const del = (root: string, env: NodeJS.ProcessEnv, ...args: string[]) => spawnSync('bash', [script('worktree-delete.sh'), ...args], { cwd: root, encoding: 'utf8', env })\n const branches = (root: string) => git(root, 'branch', '--format=%(refname:short)').split('\\n')\n\n it('deletes a worktree without a compose file, never calling docker, and its branch', () => {\n const { tmp, root, dir, env } = worktree(true)\n const res = del(root, env, 'feat-a')\n assert.equal(res.status, 0, res.stderr)\n assert.equal(existsSync(dir), false)\n assert.deepEqual(branches(root), ['master'])\n assert.equal(dockerCalls(tmp), '')\n })\n\n it('takes the containers down when the worktree has a compose file', () => {\n const { tmp, root, dir, env } = worktree(true)\n writeFileSync(path.join(dir, 'docker-compose.yml'), 'services: {}\\n')\n const res = del(root, env, 'feat-a', '-f')\n assert.equal(res.status, 0, res.stderr)\n assert.match(dockerCalls(tmp), /^compose --profile \\* down -v --remove-orphans --rmi local$/m)\n })\n\n it('refuses unpushed commits and keeps the worktree and its branch', () => {\n const { root, dir, env } = worktree(false)\n const res = del(root, env, 'feat-a')\n assert.equal(res.status, 1)\n assert.match(res.stdout, /1 commit\\(s\\) not present on any origin ref/)\n assert.equal(existsSync(dir), true)\n assert.ok(branches(root).includes('feat-a'))\n })\n\n it('with --force, deletes an unpushed branch and prints its commit', () => {\n const { root, dir, env } = worktree(false)\n const sha = git(dir, 'rev-parse', '--short', 'HEAD')\n const res = del(root, env, 'feat-a', '--force')\n assert.equal(res.status, 0, res.stderr)\n assert.match(res.stdout, new RegExp(`Deleting branch feat-a \\\\(was ${sha}\\\\)`))\n assert.deepEqual(branches(root), ['master'])\n })\n })\n})\n"]}
package/zellij.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { Exec } from './exec.js';
2
+ export type SessionState = 'running' | 'exited' | 'absent';
3
+ export type Pane = {
4
+ id: number;
5
+ title: string;
6
+ exited: boolean;
7
+ exit_status: number | null;
8
+ is_plugin: boolean;
9
+ terminal_command: string | null;
10
+ };
11
+ export declare const sessionName: (root: string) => string;
12
+ export declare const parseSessions: (out: string) => Record<string, "running" | "exited">;
13
+ export declare const sessionState: (exec: Exec, name: string) => SessionState;
14
+ export declare const checkVersion: (exec: Exec) => void;
15
+ export declare const requireSession: (exec: Exec, root: string) => string;
16
+ export declare const listPanes: (exec: Exec, session: string) => Pane[];
17
+ export declare const findPane: (exec: Exec, session: string, title: string) => Pane;
18
+ export declare const dumpScreen: (exec: Exec, session: string, pane: Pane) => string;
19
+ export declare const sendKeys: (exec: Exec, session: string, pane: Pane, keys: string) => void;
package/zellij.js ADDED
@@ -0,0 +1,50 @@
1
+ import path from 'node:path';
2
+ export const sessionName = (root) => path.basename(root);
3
+ export const parseSessions = (out) => {
4
+ const sessions = {};
5
+ for (const line of out.split('\n')) {
6
+ const name = line.trim().split(/\s+/)[0];
7
+ if (name)
8
+ sessions[name] = line.includes('EXITED') ? 'exited' : 'running';
9
+ }
10
+ return sessions;
11
+ };
12
+ export const sessionState = (exec, name) => {
13
+ let out;
14
+ try {
15
+ out = exec('zellij', ['list-sessions', '--no-formatting']);
16
+ }
17
+ catch {
18
+ return 'absent'; // zellij exits 1 when there is no session at all
19
+ }
20
+ return parseSessions(out)[name] ?? 'absent';
21
+ };
22
+ // list-panes --json, dump-screen -p and send-keys -p appeared in zellij 0.44
23
+ export const checkVersion = (exec) => {
24
+ const version = exec('zellij', ['--version']).trim().split(' ')[1] ?? '0.0.0';
25
+ const [major, minor] = version.split('.').map(Number);
26
+ if (major === 0 && minor < 44)
27
+ throw new Error(`zellij ${version} is too old, df-dev needs zellij >= 0.44`);
28
+ };
29
+ export const requireSession = (exec, root) => {
30
+ checkVersion(exec);
31
+ const name = sessionName(root);
32
+ const state = sessionState(exec, name);
33
+ if (state !== 'running')
34
+ throw new Error(`zellij session "${name}" is not running (${state}): ask the user to start it with "npm run dev-zellij"`);
35
+ return name;
36
+ };
37
+ export const listPanes = (exec, session) => JSON.parse(exec('zellij', ['--session', session, 'action', 'list-panes', '--json'])).filter(p => !p.is_plugin);
38
+ export const findPane = (exec, session, title) => {
39
+ const all = listPanes(exec, session);
40
+ const pane = all.find(p => p.title === title);
41
+ if (!pane)
42
+ throw new Error(`no pane "${title}" in session ${session}, available panes: ${all.map(p => p.title).join(', ')}`);
43
+ return pane;
44
+ };
45
+ const paneRef = (pane) => `terminal_${pane.id}`;
46
+ export const dumpScreen = (exec, session, pane) => exec('zellij', ['--session', session, 'action', 'dump-screen', '-p', paneRef(pane), '--full']);
47
+ export const sendKeys = (exec, session, pane, keys) => {
48
+ exec('zellij', ['--session', session, 'action', 'send-keys', '-p', paneRef(pane), keys]);
49
+ };
50
+ //# sourceMappingURL=zellij.js.map
package/zellij.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zellij.js","sourceRoot":"","sources":["zellij.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path'\nimport type { Exec } from './exec.js'\n\nexport type SessionState = 'running' | 'exited' | 'absent'\nexport type Pane = { id: number, title: string, exited: boolean, exit_status: number | null, is_plugin: boolean, terminal_command: string | null }\n\nexport const sessionName = (root: string) => path.basename(root)\n\nexport const parseSessions = (out: string) => {\n const sessions: Record<string, 'running' | 'exited'> = {}\n for (const line of out.split('\\n')) {\n const name = line.trim().split(/\\s+/)[0]\n if (name) sessions[name] = line.includes('EXITED') ? 'exited' : 'running'\n }\n return sessions\n}\n\nexport const sessionState = (exec: Exec, name: string): SessionState => {\n let out: string\n try {\n out = exec('zellij', ['list-sessions', '--no-formatting'])\n } catch {\n return 'absent' // zellij exits 1 when there is no session at all\n }\n return parseSessions(out)[name] ?? 'absent'\n}\n\n// list-panes --json, dump-screen -p and send-keys -p appeared in zellij 0.44\nexport const checkVersion = (exec: Exec) => {\n const version = exec('zellij', ['--version']).trim().split(' ')[1] ?? '0.0.0'\n const [major, minor] = version.split('.').map(Number)\n if (major === 0 && minor < 44) throw new Error(`zellij ${version} is too old, df-dev needs zellij >= 0.44`)\n}\n\nexport const requireSession = (exec: Exec, root: string) => {\n checkVersion(exec)\n const name = sessionName(root)\n const state = sessionState(exec, name)\n if (state !== 'running') throw new Error(`zellij session \"${name}\" is not running (${state}): ask the user to start it with \"npm run dev-zellij\"`)\n return name\n}\n\nexport const listPanes = (exec: Exec, session: string): Pane[] =>\n (JSON.parse(exec('zellij', ['--session', session, 'action', 'list-panes', '--json'])) as Pane[]).filter(p => !p.is_plugin)\n\nexport const findPane = (exec: Exec, session: string, title: string) => {\n const all = listPanes(exec, session)\n const pane = all.find(p => p.title === title)\n if (!pane) throw new Error(`no pane \"${title}\" in session ${session}, available panes: ${all.map(p => p.title).join(', ')}`)\n return pane\n}\n\nconst paneRef = (pane: Pane) => `terminal_${pane.id}`\n\nexport const dumpScreen = (exec: Exec, session: string, pane: Pane) =>\n exec('zellij', ['--session', session, 'action', 'dump-screen', '-p', paneRef(pane), '--full'])\n\nexport const sendKeys = (exec: Exec, session: string, pane: Pane, keys: string) => {\n exec('zellij', ['--session', session, 'action', 'send-keys', '-p', paneRef(pane), keys])\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
package/zellij.test.js ADDED
@@ -0,0 +1,50 @@
1
+ import { describe, it } from 'node:test';
2
+ import { strict as assert } from 'assert';
3
+ import { parseSessions, sessionState, requireSession, findPane, checkVersion, sessionName } from './zellij.js';
4
+ const panes = [
5
+ { id: 0, title: 'status', exited: false, exit_status: null, is_plugin: false, terminal_command: 'npx --no df-dev status --watch' },
6
+ { id: 1, title: 'api', exited: false, exit_status: null, is_plugin: false, terminal_command: 'bash -c ...' },
7
+ { id: 0, title: 'tab-bar', exited: false, exit_status: null, is_plugin: true, terminal_command: null }
8
+ ];
9
+ const fakeExec = (outputs) => (cmd, args) => {
10
+ const key = [cmd, ...args].join(' ');
11
+ const match = Object.keys(outputs).find(k => key.includes(k));
12
+ if (!match)
13
+ throw new Error(`unexpected call: ${key}`);
14
+ const out = outputs[match];
15
+ if (out instanceof Error)
16
+ throw out;
17
+ return out;
18
+ };
19
+ describe('zellij sessions', () => {
20
+ it('names the session after the worktree dir', () => {
21
+ assert.equal(sessionName('/home/u/Documents/portals_feat-x'), 'portals_feat-x');
22
+ });
23
+ it('parses running and exited sessions', () => {
24
+ const out = 'portals [Created 2m ago] (current)\nportals_feat-x [Created 1h ago] (EXITED - attach to resurrect)\n';
25
+ assert.deepEqual(parseSessions(out), { portals: 'running', 'portals_feat-x': 'exited' });
26
+ });
27
+ it('reports absent when zellij has no session at all', () => {
28
+ const exec = fakeExec({ 'list-sessions': new Error('No active zellij sessions found.') });
29
+ assert.equal(sessionState(exec, 'portals'), 'absent');
30
+ });
31
+ it('tells the agent to ask the user when the session is not running', () => {
32
+ const exec = fakeExec({ '--version': 'zellij 0.45.1', 'list-sessions': 'portals [Created 1h ago] (EXITED - attach to resurrect)\n' });
33
+ assert.throws(() => requireSession(exec, '/x/portals'), /not running.*npm run dev-zellij/);
34
+ });
35
+ it('refuses zellij older than 0.44', () => {
36
+ assert.throws(() => checkVersion(fakeExec({ '--version': 'zellij 0.40.1' })), /0\.44/);
37
+ checkVersion(fakeExec({ '--version': 'zellij 0.45.1' }));
38
+ });
39
+ });
40
+ describe('zellij panes', () => {
41
+ it('finds a terminal pane by title', () => {
42
+ const exec = fakeExec({ 'list-panes': JSON.stringify(panes) });
43
+ assert.equal(findPane(exec, 'portals', 'api').id, 1);
44
+ });
45
+ it('lists the available titles when a pane is not found', () => {
46
+ const exec = fakeExec({ 'list-panes': JSON.stringify(panes) });
47
+ assert.throws(() => findPane(exec, 'portals', 'worker'), /no pane "worker".*status, api/);
48
+ });
49
+ });
50
+ //# sourceMappingURL=zellij.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zellij.test.js","sourceRoot":"","sources":["zellij.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it } from 'node:test'\nimport { strict as assert } from 'assert'\nimport { parseSessions, sessionState, requireSession, findPane, checkVersion, sessionName } from './zellij.js'\nimport type { Exec } from './exec.js'\n\nconst panes = [\n { id: 0, title: 'status', exited: false, exit_status: null, is_plugin: false, terminal_command: 'npx --no df-dev status --watch' },\n { id: 1, title: 'api', exited: false, exit_status: null, is_plugin: false, terminal_command: 'bash -c ...' },\n { id: 0, title: 'tab-bar', exited: false, exit_status: null, is_plugin: true, terminal_command: null }\n]\n\nconst fakeExec = (outputs: Record<string, string | Error>): Exec => (cmd, args) => {\n const key = [cmd, ...args].join(' ')\n const match = Object.keys(outputs).find(k => key.includes(k))\n if (!match) throw new Error(`unexpected call: ${key}`)\n const out = outputs[match]\n if (out instanceof Error) throw out\n return out\n}\n\ndescribe('zellij sessions', () => {\n it('names the session after the worktree dir', () => {\n assert.equal(sessionName('/home/u/Documents/portals_feat-x'), 'portals_feat-x')\n })\n\n it('parses running and exited sessions', () => {\n const out = 'portals [Created 2m ago] (current)\\nportals_feat-x [Created 1h ago] (EXITED - attach to resurrect)\\n'\n assert.deepEqual(parseSessions(out), { portals: 'running', 'portals_feat-x': 'exited' })\n })\n\n it('reports absent when zellij has no session at all', () => {\n const exec = fakeExec({ 'list-sessions': new Error('No active zellij sessions found.') })\n assert.equal(sessionState(exec, 'portals'), 'absent')\n })\n\n it('tells the agent to ask the user when the session is not running', () => {\n const exec = fakeExec({ '--version': 'zellij 0.45.1', 'list-sessions': 'portals [Created 1h ago] (EXITED - attach to resurrect)\\n' })\n assert.throws(() => requireSession(exec, '/x/portals'), /not running.*npm run dev-zellij/)\n })\n\n it('refuses zellij older than 0.44', () => {\n assert.throws(() => checkVersion(fakeExec({ '--version': 'zellij 0.40.1' })), /0\\.44/)\n checkVersion(fakeExec({ '--version': 'zellij 0.45.1' }))\n })\n})\n\ndescribe('zellij panes', () => {\n it('finds a terminal pane by title', () => {\n const exec = fakeExec({ 'list-panes': JSON.stringify(panes) })\n assert.equal(findPane(exec, 'portals', 'api').id, 1)\n })\n\n it('lists the available titles when a pane is not found', () => {\n const exec = fakeExec({ 'list-panes': JSON.stringify(panes) })\n assert.throws(() => findPane(exec, 'portals', 'worker'), /no pane \"worker\".*status, api/)\n })\n})\n"]}