@ree-auz/nook 0.1.0 → 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 (42) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +422 -410
  3. package/bin/nk.mjs +3 -3
  4. package/docs/setup-hooks.md +107 -107
  5. package/integrations/linear/README.md +84 -84
  6. package/integrations/linear/coding-tools.json +7 -7
  7. package/integrations/linear/open-issue.mjs +147 -147
  8. package/package.json +34 -34
  9. package/skills/nook/SKILL.md +71 -71
  10. package/src/agents.mjs +92 -92
  11. package/src/backup.mjs +74 -74
  12. package/src/ci.mjs +378 -378
  13. package/src/cli.mjs +151 -151
  14. package/src/commands/check-hooks.mjs +128 -128
  15. package/src/commands/clean.mjs +175 -175
  16. package/src/commands/doctor.mjs +100 -91
  17. package/src/commands/forget.mjs +57 -57
  18. package/src/commands/hooks.mjs +34 -27
  19. package/src/commands/init.mjs +154 -154
  20. package/src/commands/ls.mjs +107 -107
  21. package/src/commands/rm.mjs +190 -190
  22. package/src/commands/workspace.mjs +163 -163
  23. package/src/commands/write-hooks.mjs +185 -185
  24. package/src/compose-plan.mjs +228 -228
  25. package/src/compose.mjs +123 -123
  26. package/src/config.mjs +115 -115
  27. package/src/context.mjs +189 -189
  28. package/src/errors.mjs +8 -8
  29. package/src/forge.mjs +75 -75
  30. package/src/generate-hooks.mjs +367 -367
  31. package/src/git.mjs +86 -86
  32. package/src/home.mjs +48 -48
  33. package/src/hooks.mjs +135 -135
  34. package/src/names.mjs +65 -65
  35. package/src/occupancy-windows.ps1 +59 -59
  36. package/src/occupancy.mjs +131 -131
  37. package/src/ports.mjs +89 -45
  38. package/src/prompt.mjs +100 -100
  39. package/src/shared.mjs +97 -97
  40. package/src/status.mjs +30 -30
  41. package/src/tools.mjs +23 -23
  42. package/src/ui.mjs +60 -60
@@ -1,147 +1,147 @@
1
- #!/usr/bin/env node
2
- // Opens a Linear issue in a Nook workspace, in a new terminal tab.
3
- // Linear runs this file through ~/.linear/coding-tools.json
4
- // (Work on issue -> Custom script). See README.md next to it.
5
- //
6
- // node open-issue.mjs [agent] started by Linear (no window)
7
- // node open-issue.mjs --run <file> ... inside the new tab
8
- import { spawn, spawnSync } from "node:child_process";
9
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
- import { homedir, tmpdir } from "node:os";
11
- import { delimiter, dirname, join, resolve } from "node:path";
12
- import { fileURLToPath } from "node:url";
13
- import { normalizeName } from "../../src/names.mjs";
14
-
15
- const self = fileURLToPath(import.meta.url);
16
- const nk = fileURLToPath(new URL("../../bin/nk.mjs", import.meta.url));
17
-
18
- /**
19
- * Puts this Node's folder first on PATH. With a version manager such as fnm
20
- * or nvm, Linear starts without the shell setup that adds it, and global npm
21
- * commands (the agent, pnpm for hooks) live next to node.exe.
22
- */
23
- function withNodeOnPath(env = process.env) {
24
- const key = Object.keys(env).find((name) => name.toUpperCase() === "PATH") ?? "PATH";
25
- return { ...env, [key]: [dirname(process.execPath), env[key]].filter(Boolean).join(delimiter) };
26
- }
27
-
28
- /** Runs nk with this Node, so the prompt never passes through cmd.exe. */
29
- function runNk(args, opts = {}) {
30
- return spawnSync(process.execPath, [nk, ...args], { stdio: "inherit", env: withNodeOnPath(), ...opts });
31
- }
32
-
33
- /** Creates the issue's workspace and starts the agent, or reopens it. */
34
- function startWork({ branch, agent, prompt }) {
35
- const listed = spawnSync(process.execPath, [nk, "ls", "--json"], { encoding: "utf8", env: withNodeOnPath() });
36
- const existing = listed.status === 0 ? JSON.parse(listed.stdout).find((ws) => !ws.main && (ws.branch === branch || ws.name === normalizeName(branch))) : null;
37
- // Clicked again for the same issue: reopen the workspace without restarting the task.
38
- // -y: the branch name Linear suggests is converted without asking.
39
- const result = existing ? runNk([existing.name, agent]) : runNk(["-b", branch, agent, "-y", "-p", prompt]);
40
- process.exitCode = result.status ?? 1;
41
- }
42
-
43
- /** Inside a tab another terminal opened: the prompt comes in a file. */
44
- export function insideTab([file, branch, agent, ...more]) {
45
- const prompt = readFileSync(file, "utf8");
46
- rmSync(file, { force: true });
47
- // A Warp tab config written only for this launch.
48
- if (more[0] === "--cleanup" && more[1]) rmSync(more[1], { force: true });
49
- startWork({ branch, agent, prompt });
50
- }
51
-
52
- /** Windows Terminal: -w 0 opens a tab in the most recent window (or a new window). */
53
- function openInWindowsTerminal({ dir, id, inner }) {
54
- spawn("wt.exe", ["-w", "0", "new-tab", "-d", dir, "--title", id, ...inner], { detached: true, stdio: "ignore" }).unref();
55
- }
56
-
57
- /** A PowerShell argument in single quotes. */
58
- const psQuote = (text) => `'${String(text).replaceAll("'", "''")}'`;
59
-
60
- /**
61
- * Warp: its URI scheme cannot run a command, so write a tab config that
62
- * does and open it with warp://tab_config/<name>, as Warp's docs advise.
63
- * The tab deletes the config once it starts.
64
- */
65
- export function openInWarp({ dir, id, inner, env = process.env, start = openUri }) {
66
- const folder = join(env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "warp", "Warp", "data", "tab_configs");
67
- mkdirSync(folder, { recursive: true });
68
- const name = `nk-${id.toLowerCase().replace(/[^a-z0-9-]/g, "-")}-${process.pid}`;
69
- const config = join(folder, `${name}.toml`);
70
- const [node, ...args] = [...inner, "--cleanup", config];
71
- const shell = which("pwsh") ? "pwsh" : "powershell";
72
- const command = `& ${[node, ...args].map(psQuote).join(" ")}`;
73
- writeFileSync(
74
- config,
75
- [`name = ${JSON.stringify(name)}`, `title = ${JSON.stringify(id)}`, "", "[[panes]]", 'id = "main"', 'type = "terminal"', `shell = ${JSON.stringify(shell)}`, `directory = ${JSON.stringify(dir)}`, `commands = [${JSON.stringify(command)}]`, ""].join("\n"),
76
- );
77
- start(`warp://tab_config/${name}`);
78
- return config;
79
- }
80
-
81
- /** Opens a URI with its registered app, like PowerShell's Start-Process. */
82
- function openUri(uri) {
83
- spawn("cmd.exe", ["/d", "/c", "start", '""', uri], { detached: true, stdio: "ignore", windowsVerbatimArguments: true }).unref();
84
- }
85
-
86
- /** A command on PATH, without running it. */
87
- function which(command) {
88
- const result = spawnSync("where.exe", [command], { encoding: "utf8", windowsHide: true });
89
- return result.status === 0;
90
- }
91
-
92
- const TERMINALS = { wt: openInWindowsTerminal, warp: openInWarp };
93
-
94
- /**
95
- * Closes the terminal window Linear opened for this script, once the work
96
- * runs in another terminal: its shell is this process's parent.
97
- */
98
- function closeLinearWindow() {
99
- const parent = spawnSync("tasklist.exe", ["/FI", `PID eq ${process.ppid}`, "/FO", "CSV", "/NH"], { encoding: "utf8", windowsHide: true });
100
- if (/^"(powershell|pwsh|cmd)\.exe"/i.test(parent.stdout ?? "")) process.kill(process.ppid);
101
- }
102
-
103
- /**
104
- * The agent named by a label in Linear's prompt, which lists the issue's
105
- * labels as <label>Group › Name</label>: with group "Agent", an issue
106
- * labelled "Agent › Claude" gets `claude`.
107
- */
108
- export function agentFromLabels(prompt, group) {
109
- if (!group) return null;
110
- for (const [, text] of prompt.matchAll(/<label>([^<]*)<\/label>/g)) {
111
- const [labelGroup, name] = text.split("›").map((part) => part.trim());
112
- if (name && labelGroup === group) return name.toLowerCase().replace(/\s+/g, "-");
113
- }
114
- return null;
115
- }
116
-
117
- /** `--name value` from the arguments Linear passes. */
118
- function option(argv, name) {
119
- const at = argv.indexOf(name);
120
- return at >= 0 ? argv[at + 1] : undefined;
121
- }
122
-
123
- function fromLinear(argv) {
124
- const terminal = option(argv, "--terminal");
125
- const agent = agentFromLabels(process.env.LINEAR_PROMPT ?? "", option(argv, "--agent-label")) ?? option(argv, "--agent") ?? "codex";
126
- const { LINEAR_ISSUE_BRANCH_NAME: branch, LINEAR_WORK_DIR: dir, LINEAR_ISSUE_IDENTIFIER: id = "issue", LINEAR_PROMPT: prompt = "" } = process.env;
127
- if (!branch || !dir) throw new Error("LINEAR_ISSUE_BRANCH_NAME and LINEAR_WORK_DIR are missing; list them under env in coding-tools.json");
128
- // Default: work in the terminal Linear opened for the script.
129
- if (!terminal) {
130
- process.chdir(dir);
131
- return startWork({ branch, agent, prompt });
132
- }
133
- const open = TERMINALS[terminal];
134
- if (!open) throw new Error(`Unknown terminal "${terminal}"; use ${Object.keys(TERMINALS).join(" or ")}, or leave --terminal out`);
135
- if (process.platform !== "win32") throw new Error("--terminal only works on Windows so far");
136
- // The prompt is long Markdown with quotes and semicolons, so it travels in a file.
137
- const file = join(tmpdir(), `nk-linear-${id}-${process.pid}.md`);
138
- writeFileSync(file, prompt);
139
- open({ dir, id, inner: [process.execPath, self, "--run", file, branch, agent] });
140
- closeLinearWindow();
141
- }
142
-
143
- if (process.argv[1] && resolve(process.argv[1]) === self) {
144
- const [first, ...rest] = process.argv.slice(2);
145
- if (first === "--run") insideTab(rest);
146
- else fromLinear(process.argv.slice(2));
147
- }
1
+ #!/usr/bin/env node
2
+ // Opens a Linear issue in a Nook workspace, in a new terminal tab.
3
+ // Linear runs this file through ~/.linear/coding-tools.json
4
+ // (Work on issue -> Custom script). See README.md next to it.
5
+ //
6
+ // node open-issue.mjs [agent] started by Linear (no window)
7
+ // node open-issue.mjs --run <file> ... inside the new tab
8
+ import { spawn, spawnSync } from "node:child_process";
9
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
+ import { homedir, tmpdir } from "node:os";
11
+ import { delimiter, dirname, join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { normalizeName } from "../../src/names.mjs";
14
+
15
+ const self = fileURLToPath(import.meta.url);
16
+ const nk = fileURLToPath(new URL("../../bin/nk.mjs", import.meta.url));
17
+
18
+ /**
19
+ * Puts this Node's folder first on PATH. With a version manager such as fnm
20
+ * or nvm, Linear starts without the shell setup that adds it, and global npm
21
+ * commands (the agent, pnpm for hooks) live next to node.exe.
22
+ */
23
+ function withNodeOnPath(env = process.env) {
24
+ const key = Object.keys(env).find((name) => name.toUpperCase() === "PATH") ?? "PATH";
25
+ return { ...env, [key]: [dirname(process.execPath), env[key]].filter(Boolean).join(delimiter) };
26
+ }
27
+
28
+ /** Runs nk with this Node, so the prompt never passes through cmd.exe. */
29
+ function runNk(args, opts = {}) {
30
+ return spawnSync(process.execPath, [nk, ...args], { stdio: "inherit", env: withNodeOnPath(), ...opts });
31
+ }
32
+
33
+ /** Creates the issue's workspace and starts the agent, or reopens it. */
34
+ function startWork({ branch, agent, prompt }) {
35
+ const listed = spawnSync(process.execPath, [nk, "ls", "--json"], { encoding: "utf8", env: withNodeOnPath() });
36
+ const existing = listed.status === 0 ? JSON.parse(listed.stdout).find((ws) => !ws.main && (ws.branch === branch || ws.name === normalizeName(branch))) : null;
37
+ // Clicked again for the same issue: reopen the workspace without restarting the task.
38
+ // -y: the branch name Linear suggests is converted without asking.
39
+ const result = existing ? runNk([existing.name, agent]) : runNk(["-b", branch, agent, "-y", "-p", prompt]);
40
+ process.exitCode = result.status ?? 1;
41
+ }
42
+
43
+ /** Inside a tab another terminal opened: the prompt comes in a file. */
44
+ export function insideTab([file, branch, agent, ...more]) {
45
+ const prompt = readFileSync(file, "utf8");
46
+ rmSync(file, { force: true });
47
+ // A Warp tab config written only for this launch.
48
+ if (more[0] === "--cleanup" && more[1]) rmSync(more[1], { force: true });
49
+ startWork({ branch, agent, prompt });
50
+ }
51
+
52
+ /** Windows Terminal: -w 0 opens a tab in the most recent window (or a new window). */
53
+ function openInWindowsTerminal({ dir, id, inner }) {
54
+ spawn("wt.exe", ["-w", "0", "new-tab", "-d", dir, "--title", id, ...inner], { detached: true, stdio: "ignore" }).unref();
55
+ }
56
+
57
+ /** A PowerShell argument in single quotes. */
58
+ const psQuote = (text) => `'${String(text).replaceAll("'", "''")}'`;
59
+
60
+ /**
61
+ * Warp: its URI scheme cannot run a command, so write a tab config that
62
+ * does and open it with warp://tab_config/<name>, as Warp's docs advise.
63
+ * The tab deletes the config once it starts.
64
+ */
65
+ export function openInWarp({ dir, id, inner, env = process.env, start = openUri }) {
66
+ const folder = join(env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "warp", "Warp", "data", "tab_configs");
67
+ mkdirSync(folder, { recursive: true });
68
+ const name = `nk-${id.toLowerCase().replace(/[^a-z0-9-]/g, "-")}-${process.pid}`;
69
+ const config = join(folder, `${name}.toml`);
70
+ const [node, ...args] = [...inner, "--cleanup", config];
71
+ const shell = which("pwsh") ? "pwsh" : "powershell";
72
+ const command = `& ${[node, ...args].map(psQuote).join(" ")}`;
73
+ writeFileSync(
74
+ config,
75
+ [`name = ${JSON.stringify(name)}`, `title = ${JSON.stringify(id)}`, "", "[[panes]]", 'id = "main"', 'type = "terminal"', `shell = ${JSON.stringify(shell)}`, `directory = ${JSON.stringify(dir)}`, `commands = [${JSON.stringify(command)}]`, ""].join("\n"),
76
+ );
77
+ start(`warp://tab_config/${name}`);
78
+ return config;
79
+ }
80
+
81
+ /** Opens a URI with its registered app, like PowerShell's Start-Process. */
82
+ function openUri(uri) {
83
+ spawn("cmd.exe", ["/d", "/c", "start", '""', uri], { detached: true, stdio: "ignore", windowsVerbatimArguments: true }).unref();
84
+ }
85
+
86
+ /** A command on PATH, without running it. */
87
+ function which(command) {
88
+ const result = spawnSync("where.exe", [command], { encoding: "utf8", windowsHide: true });
89
+ return result.status === 0;
90
+ }
91
+
92
+ const TERMINALS = { wt: openInWindowsTerminal, warp: openInWarp };
93
+
94
+ /**
95
+ * Closes the terminal window Linear opened for this script, once the work
96
+ * runs in another terminal: its shell is this process's parent.
97
+ */
98
+ function closeLinearWindow() {
99
+ const parent = spawnSync("tasklist.exe", ["/FI", `PID eq ${process.ppid}`, "/FO", "CSV", "/NH"], { encoding: "utf8", windowsHide: true });
100
+ if (/^"(powershell|pwsh|cmd)\.exe"/i.test(parent.stdout ?? "")) process.kill(process.ppid);
101
+ }
102
+
103
+ /**
104
+ * The agent named by a label in Linear's prompt, which lists the issue's
105
+ * labels as <label>Group › Name</label>: with group "Agent", an issue
106
+ * labelled "Agent › Claude" gets `claude`.
107
+ */
108
+ export function agentFromLabels(prompt, group) {
109
+ if (!group) return null;
110
+ for (const [, text] of prompt.matchAll(/<label>([^<]*)<\/label>/g)) {
111
+ const [labelGroup, name] = text.split("›").map((part) => part.trim());
112
+ if (name && labelGroup === group) return name.toLowerCase().replace(/\s+/g, "-");
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /** `--name value` from the arguments Linear passes. */
118
+ function option(argv, name) {
119
+ const at = argv.indexOf(name);
120
+ return at >= 0 ? argv[at + 1] : undefined;
121
+ }
122
+
123
+ function fromLinear(argv) {
124
+ const terminal = option(argv, "--terminal");
125
+ const agent = agentFromLabels(process.env.LINEAR_PROMPT ?? "", option(argv, "--agent-label")) ?? option(argv, "--agent") ?? "codex";
126
+ const { LINEAR_ISSUE_BRANCH_NAME: branch, LINEAR_WORK_DIR: dir, LINEAR_ISSUE_IDENTIFIER: id = "issue", LINEAR_PROMPT: prompt = "" } = process.env;
127
+ if (!branch || !dir) throw new Error("LINEAR_ISSUE_BRANCH_NAME and LINEAR_WORK_DIR are missing; list them under env in coding-tools.json");
128
+ // Default: work in the terminal Linear opened for the script.
129
+ if (!terminal) {
130
+ process.chdir(dir);
131
+ return startWork({ branch, agent, prompt });
132
+ }
133
+ const open = TERMINALS[terminal];
134
+ if (!open) throw new Error(`Unknown terminal "${terminal}"; use ${Object.keys(TERMINALS).join(" or ")}, or leave --terminal out`);
135
+ if (process.platform !== "win32") throw new Error("--terminal only works on Windows so far");
136
+ // The prompt is long Markdown with quotes and semicolons, so it travels in a file.
137
+ const file = join(tmpdir(), `nk-linear-${id}-${process.pid}.md`);
138
+ writeFileSync(file, prompt);
139
+ open({ dir, id, inner: [process.execPath, self, "--run", file, branch, agent] });
140
+ closeLinearWindow();
141
+ }
142
+
143
+ if (process.argv[1] && resolve(process.argv[1]) === self) {
144
+ const [first, ...rest] = process.argv.slice(2);
145
+ if (first === "--run") insideTab(rest);
146
+ else fromLinear(process.argv.slice(2));
147
+ }
package/package.json CHANGED
@@ -1,34 +1,34 @@
1
- {
2
- "name": "@ree-auz/nook",
3
- "version": "0.1.0",
4
- "description": "One home folder for your git worktrees: create, open and remove workspaces with shared files, ports and hooks.",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "nk": "bin/nk.mjs",
9
- "nook": "bin/nk.mjs"
10
- },
11
- "files": [
12
- "bin",
13
- "docs",
14
- "integrations",
15
- "skills",
16
- "src"
17
- ],
18
- "engines": {
19
- "node": ">=20.12"
20
- },
21
- "scripts": {
22
- "test": "node --test --test-concurrency=1",
23
- "corpus": "node scripts/corpus.mjs"
24
- },
25
- "dependencies": {
26
- "cross-spawn": "7.0.6",
27
- "smol-toml": "1.9.1",
28
- "yaml": "2.9.1"
29
- },
30
- "repository": {
31
- "type": "git",
32
- "url": "git+https://github.com/ree-auz/nook.git"
33
- }
34
- }
1
+ {
2
+ "name": "@ree-auz/nook",
3
+ "version": "0.1.1",
4
+ "description": "One home folder for your git worktrees: create, open and remove workspaces with shared files, ports and hooks.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "nk": "bin/nk.mjs",
9
+ "nook": "bin/nk.mjs"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "docs",
14
+ "integrations",
15
+ "skills",
16
+ "src"
17
+ ],
18
+ "engines": {
19
+ "node": ">=20.12"
20
+ },
21
+ "scripts": {
22
+ "test": "node --test --test-concurrency=1",
23
+ "corpus": "node scripts/corpus.mjs"
24
+ },
25
+ "dependencies": {
26
+ "cross-spawn": "7.0.6",
27
+ "smol-toml": "1.9.1",
28
+ "yaml": "2.9.1"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/ree-auz/nook.git"
33
+ }
34
+ }
@@ -1,71 +1,71 @@
1
- ---
2
- name: nook
3
- description: Use when working in a git worktree managed by nk (Nook), when asked to create, list, open, remove or clean up workspaces with nk, or to set up a repository's .nook hooks.
4
- ---
5
-
6
- # Working with Nook (`nk`)
7
-
8
- Nook keeps one git worktree per task under `~/nook/workspace/<repo>/<name>`.
9
- The workspace name is also the branch name and the folder name.
10
-
11
- ## Am I in a workspace?
12
-
13
- `NOOK_WORKSPACE` is set when nk started you. These variables are also set:
14
-
15
- | Variable | Meaning |
16
- | --- | --- |
17
- | `NOOK_WORKSPACE` | workspace and branch name |
18
- | `NOOK_PATH` | this workspace's folder |
19
- | `NOOK_REPO` | the main checkout of the repository |
20
- | `NOOK_SHARED` | the repository's shared folder (holds files such as `.env`) |
21
- | `NOOK_PORT` | first of ten ports reserved for this workspace (`NOOK_PORT` to `NOOK_PORT + 9`) |
22
-
23
- Without the variables, `nk ls --json` lists every workspace with its `path`;
24
- you are in one when your working directory is inside that path.
25
-
26
- ## Rules
27
-
28
- - Work and commit only inside your own workspace folder. Do not edit the main
29
- checkout (`NOOK_REPO`) or another workspace.
30
- - Use ports from `NOOK_PORT` to `NOOK_PORT + 9` for servers you start, so
31
- workspaces do not collide.
32
- - Files copied from the shared folder (such as `.env`) are local settings.
33
- Do not commit them and do not print their contents.
34
- - Do not run `nk rm` or `nk clean` on your own workspace: nk refuses while
35
- you are inside it. Leave removal to the person or the tool that started you.
36
- - Hooks in `.nook/` run with the person's approval. Do not edit them to get
37
- past a failure; report the failure and the log path nk printed. Never pass
38
- `--allow-hooks` yourself: the person reads and allows the hooks.
39
-
40
- ## Setting up hooks
41
-
42
- When asked to set up Nook hooks: if you were started by `nk hooks write
43
- <agent>`, nk has already generated hooks from CI and the Compose file; finish what its prompt lists
44
- as not translated. Otherwise run `nk hooks write` first. Follow
45
- [docs/setup-hooks.md](https://github.com/ree-auz/nook/blob/main/docs/setup-hooks.md),
46
- run `nk hooks test` in the folder you work in and fix the hooks until it
47
- passes. Delete the first line of a generated hook you edit. Do not commit or
48
- push unless asked.
49
-
50
- ## Commands
51
-
52
- | Command | Use |
53
- | --- | --- |
54
- | `nk -b <name> [agent] [-p <prompt>]` | Create a workspace from the default branch (or an existing branch of that name) |
55
- | `nk <name>` | Print a workspace's path; with an agent, start it there |
56
- | `nk ls --json` | Every workspace of this repository: `name`, `repo`, `path`, `port`, `changes`, `ahead`, `behind`, `pr` (`number`, `state`, `ci`) |
57
- | `nk rm <name>... -n` | Show what removing would back up and delete, change nothing |
58
- | `nk clean -n` | Show which workspaces are merged and would be removed |
59
- | `nk hooks test` | Run this folder's hooks in a temporary workspace; fails on leftovers |
60
- | `nk hooks` | The repository's hooks: allowed, tested |
61
- | `nk doctor` | Check nk's tools; exit 1 when something is broken |
62
-
63
- Pass `-y` to `nk rm` or `nk clean` only when the person asked for the
64
- removal. Both back up uncommitted changes and unpushed commits to
65
- `~/nook/backup/<repo>/` first.
66
-
67
- ## Reading results
68
-
69
- Exit code 0 means success. On failure, stderr starts with `✗ what failed`,
70
- followed by `○` lines saying why and what to do next. Follow those lines
71
- instead of guessing.
1
+ ---
2
+ name: nook
3
+ description: Use when working in a git worktree managed by nk (Nook), when asked to create, list, open, remove or clean up workspaces with nk, or to set up a repository's .nook hooks.
4
+ ---
5
+
6
+ # Working with Nook (`nk`)
7
+
8
+ Nook keeps one git worktree per task under `~/nook/workspace/<repo>/<name>`.
9
+ The workspace name is also the branch name and the folder name.
10
+
11
+ ## Am I in a workspace?
12
+
13
+ `NOOK_WORKSPACE` is set when nk started you. These variables are also set:
14
+
15
+ | Variable | Meaning |
16
+ | --- | --- |
17
+ | `NOOK_WORKSPACE` | workspace and branch name |
18
+ | `NOOK_PATH` | this workspace's folder |
19
+ | `NOOK_REPO` | the main checkout of the repository |
20
+ | `NOOK_SHARED` | the repository's shared folder (holds files such as `.env`) |
21
+ | `NOOK_PORT` | first of ten ports reserved for this workspace (`NOOK_PORT` to `NOOK_PORT + 9`) |
22
+
23
+ Without the variables, `nk ls --json` lists every workspace with its `path`;
24
+ you are in one when your working directory is inside that path.
25
+
26
+ ## Rules
27
+
28
+ - Work and commit only inside your own workspace folder. Do not edit the main
29
+ checkout (`NOOK_REPO`) or another workspace.
30
+ - Use ports from `NOOK_PORT` to `NOOK_PORT + 9` for servers you start, so
31
+ workspaces do not collide.
32
+ - Files copied from the shared folder (such as `.env`) are local settings.
33
+ Do not commit them and do not print their contents.
34
+ - Do not run `nk rm` or `nk clean` on your own workspace: nk refuses while
35
+ you are inside it. Leave removal to the person or the tool that started you.
36
+ - Hooks in `.nook/` run with the person's approval. Do not edit them to get
37
+ past a failure; report the failure and the log path nk printed. Never pass
38
+ `--allow-hooks` yourself: the person reads and allows the hooks.
39
+
40
+ ## Setting up hooks
41
+
42
+ When asked to set up Nook hooks: if you were started by `nk hooks write
43
+ <agent>`, nk has already generated hooks from CI and the Compose file; finish what its prompt lists
44
+ as not translated. Otherwise run `nk hooks write` first. Follow
45
+ [docs/setup-hooks.md](https://github.com/ree-auz/nook/blob/main/docs/setup-hooks.md),
46
+ run `nk hooks test` in the folder you work in and fix the hooks until it
47
+ passes. Delete the first line of a generated hook you edit. Do not commit or
48
+ push unless asked.
49
+
50
+ ## Commands
51
+
52
+ | Command | Use |
53
+ | --- | --- |
54
+ | `nk -b <name> [agent] [-p <prompt>]` | Create a workspace from the default branch (or an existing branch of that name) |
55
+ | `nk <name>` | Print a workspace's path; with an agent, start it there |
56
+ | `nk ls --json` | Every workspace of this repository: `name`, `repo`, `path`, `port`, `changes`, `ahead`, `behind`, `pr` (`number`, `state`, `ci`) |
57
+ | `nk rm <name>... -n` | Show what removing would back up and delete, change nothing |
58
+ | `nk clean -n` | Show which workspaces are merged and would be removed |
59
+ | `nk hooks test` | Run this folder's hooks in a temporary workspace; fails on leftovers |
60
+ | `nk hooks` | The repository's hooks: allowed, tested |
61
+ | `nk doctor` | Check nk's tools; exit 1 when something is broken |
62
+
63
+ Pass `-y` to `nk rm` or `nk clean` only when the person asked for the
64
+ removal. Both back up uncommitted changes and unpushed commits to
65
+ `~/nook/backup/<repo>/` first.
66
+
67
+ ## Reading results
68
+
69
+ Exit code 0 means success. On failure, stderr starts with `✗ what failed`,
70
+ followed by `○` lines saying why and what to do next. Follow those lines
71
+ instead of guessing.