@ree-auz/nook 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +422 -410
- package/bin/nk.mjs +3 -3
- package/docs/setup-hooks.md +107 -107
- package/integrations/linear/README.md +94 -84
- package/integrations/linear/coding-tools.json +7 -7
- package/integrations/linear/open-issue.mjs +193 -147
- package/package.json +34 -34
- package/skills/nook/SKILL.md +71 -71
- package/src/agents.mjs +92 -92
- package/src/backup.mjs +74 -74
- package/src/ci.mjs +378 -378
- package/src/cli.mjs +151 -151
- package/src/commands/check-hooks.mjs +128 -128
- package/src/commands/clean.mjs +175 -175
- package/src/commands/doctor.mjs +100 -91
- package/src/commands/forget.mjs +57 -57
- package/src/commands/hooks.mjs +34 -27
- package/src/commands/init.mjs +154 -154
- package/src/commands/ls.mjs +107 -107
- package/src/commands/rm.mjs +190 -190
- package/src/commands/workspace.mjs +163 -163
- package/src/commands/write-hooks.mjs +185 -185
- package/src/compose-plan.mjs +228 -228
- package/src/compose.mjs +123 -123
- package/src/config.mjs +115 -115
- package/src/context.mjs +189 -189
- package/src/errors.mjs +8 -8
- package/src/forge.mjs +75 -75
- package/src/generate-hooks.mjs +367 -367
- package/src/git.mjs +86 -86
- package/src/home.mjs +48 -48
- package/src/hooks.mjs +135 -135
- package/src/names.mjs +65 -65
- package/src/occupancy-windows.ps1 +59 -59
- package/src/occupancy.mjs +131 -131
- package/src/ports.mjs +89 -45
- package/src/prompt.mjs +100 -100
- package/src/shared.mjs +97 -97
- package/src/status.mjs +30 -30
- package/src/tools.mjs +23 -23
- package/src/ui.mjs +60 -60
package/src/context.mjs
CHANGED
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
import { existsSync, renameSync } from "node:fs";
|
|
2
|
-
import { basename, join, relative } from "node:path";
|
|
3
|
-
import { applyChanges, loadConfig, saveConfig, syncInPlace, withLock, withLockSync } from "./config.mjs";
|
|
4
|
-
import { NookError } from "./errors.mjs";
|
|
5
|
-
import { homePaths, isInside, nookHome, samePath } from "./home.mjs";
|
|
6
|
-
import { normalizeName } from "./names.mjs";
|
|
7
|
-
import { gitTry, listWorktrees } from "./git.mjs";
|
|
8
|
-
import { line, useColor } from "./ui.mjs";
|
|
9
|
-
|
|
10
|
-
/** Shared state for one command run. */
|
|
11
|
-
export function createContext({ env = process.env, cwd = process.cwd(), out = process.stdout, err = process.stderr } = {}) {
|
|
12
|
-
const paths = homePaths(nookHome(env));
|
|
13
|
-
const config = loadConfig(paths.config);
|
|
14
|
-
const moveFailed = moveOldWorkspaceFolder(paths, config);
|
|
15
|
-
// What this run changed is config minus base; on save it goes onto what is on disk now,
|
|
16
|
-
// so changes another nk made meanwhile are kept.
|
|
17
|
-
let base = structuredClone(config);
|
|
18
|
-
const merge = () => {
|
|
19
|
-
const disk = applyChanges(base, config, loadConfig(paths.config));
|
|
20
|
-
saveConfig(paths.config, disk);
|
|
21
|
-
syncInPlace(config, disk);
|
|
22
|
-
base = structuredClone(config);
|
|
23
|
-
};
|
|
24
|
-
const color = useColor(err, env);
|
|
25
|
-
const say = (kind) => (text) => err.write(`${line(kind, text, color)}\n`);
|
|
26
|
-
if (moveFailed)
|
|
27
|
-
say("warn")(`Could not rename ${paths.workspaces} to ${join(paths.home, "workspace")} (${moveFailed}); still using the old folder. Close programs open in it (terminals, editors, dev servers) and run nk again.`);
|
|
28
|
-
return {
|
|
29
|
-
env,
|
|
30
|
-
cwd,
|
|
31
|
-
paths,
|
|
32
|
-
config,
|
|
33
|
-
out,
|
|
34
|
-
err,
|
|
35
|
-
save() {
|
|
36
|
-
withLockSync(paths.config, merge);
|
|
37
|
-
},
|
|
38
|
-
/**
|
|
39
|
-
* Brings config up to date with disk, runs fn on it and saves, all under the lock: for choices
|
|
40
|
-
* that must not clash with another nk running at the same time, such as a workspace's ports.
|
|
41
|
-
*/
|
|
42
|
-
async update(fn) {
|
|
43
|
-
return withLock(paths.config, async () => {
|
|
44
|
-
merge();
|
|
45
|
-
const result = await fn(config);
|
|
46
|
-
merge();
|
|
47
|
-
return result;
|
|
48
|
-
});
|
|
49
|
-
},
|
|
50
|
-
print(text = "") {
|
|
51
|
-
out.write(`${text}\n`);
|
|
52
|
-
},
|
|
53
|
-
note(text = "") {
|
|
54
|
-
err.write(`${text}\n`);
|
|
55
|
-
},
|
|
56
|
-
step: say("step"),
|
|
57
|
-
ok: say("ok"),
|
|
58
|
-
warn: say("warn"),
|
|
59
|
-
info: say("info"),
|
|
60
|
-
item: say("item"),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Before 0.1 the folder was called "workspaces". Renames it once and tells git
|
|
66
|
-
* where the workspaces went. When it cannot (a file is open on Windows), keeps
|
|
67
|
-
* using the old folder and returns why.
|
|
68
|
-
*/
|
|
69
|
-
function moveOldWorkspaceFolder(paths, config) {
|
|
70
|
-
const old = join(paths.home, "workspaces");
|
|
71
|
-
if (!existsSync(old) || existsSync(paths.workspaces)) return;
|
|
72
|
-
const moves = [];
|
|
73
|
-
for (const repo of Object.values(config.repos ?? {}))
|
|
74
|
-
for (const ws of Object.values(repo.workspaces ?? {}))
|
|
75
|
-
if (isInside(ws.path, old)) moves.push({ repo, ws, path: join(paths.workspaces, relative(old, ws.path)) });
|
|
76
|
-
try {
|
|
77
|
-
renameSync(old, paths.workspaces);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
paths.workspaces = old;
|
|
80
|
-
return error.code ?? error.message;
|
|
81
|
-
}
|
|
82
|
-
for (const move of moves) move.ws.path = move.path;
|
|
83
|
-
for (const repo of new Set(moves.map((move) => move.repo)))
|
|
84
|
-
gitTry(["worktree", "repair", ...moves.filter((move) => move.repo === repo).map((move) => move.path)], { cwd: repo.path });
|
|
85
|
-
if (moves.length) saveConfig(paths.config, config);
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** The registered repository `cwd` belongs to, or null. */
|
|
90
|
-
export function repoFromCwd(ctx) {
|
|
91
|
-
for (const [name, repo] of Object.entries(ctx.config.repos)) {
|
|
92
|
-
if (isInside(ctx.cwd, join(ctx.paths.workspaces, name))) return name;
|
|
93
|
-
if (isInside(ctx.cwd, repo.path)) return name;
|
|
94
|
-
for (const ws of Object.values(repo.workspaces)) if (isInside(ctx.cwd, ws.path)) return name;
|
|
95
|
-
}
|
|
96
|
-
const top = gitTry(["rev-parse", "--show-toplevel"], { cwd: ctx.cwd });
|
|
97
|
-
if (top.ok) {
|
|
98
|
-
try {
|
|
99
|
-
const [main] = listWorktrees(ctx.cwd);
|
|
100
|
-
for (const [name, repo] of Object.entries(ctx.config.repos)) if (main && isInside(main.path, repo.path)) return name;
|
|
101
|
-
} catch {
|
|
102
|
-
/* Not a usable repository. */
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function requireRepo(ctx, name) {
|
|
109
|
-
const repo = ctx.config.repos[name];
|
|
110
|
-
if (!repo) throw new NookError(`Unknown repository "${name}". Registered: ${Object.keys(ctx.config.repos).join(", ") || "none"}`);
|
|
111
|
-
return { name, repo };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* The repository a command acts on: --repo, else the one the current folder
|
|
116
|
-
* belongs to (its main checkout or one of its workspaces). Like git and
|
|
117
|
-
* worktrunk, nothing is guessed outside a repository.
|
|
118
|
-
*/
|
|
119
|
-
export function resolveRepo(ctx, flag) {
|
|
120
|
-
if (flag) return requireRepo(ctx, flag);
|
|
121
|
-
const fromCwd = repoFromCwd(ctx);
|
|
122
|
-
if (fromCwd) return requireRepo(ctx, fromCwd);
|
|
123
|
-
if (!Object.keys(ctx.config.repos).length) throw new NookError("No repository yet. Run nk init inside a repository first");
|
|
124
|
-
throw new NookError(
|
|
125
|
-
`Not inside a registered repository. Run nk in the repository or one of its workspaces, or pass --repo (${Object.keys(ctx.config.repos).join(", ")})`,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Every worktree of a repository, as git lists them, joined with what Nook
|
|
131
|
-
* recorded (port, creation time). Git is the source of truth, like worktrunk:
|
|
132
|
-
* worktrees made by other tools show up too, without a port. The main
|
|
133
|
-
* checkout comes first with `main: true`; workspaces Nook recorded whose
|
|
134
|
-
* folder is gone come last.
|
|
135
|
-
*/
|
|
136
|
-
export function repoWorkspaces(repoName, repo) {
|
|
137
|
-
let trees = [];
|
|
138
|
-
try {
|
|
139
|
-
trees = listWorktrees(repo.path);
|
|
140
|
-
} catch {
|
|
141
|
-
/* Not a usable repository; nk doctor reports it. */
|
|
142
|
-
}
|
|
143
|
-
const items = [];
|
|
144
|
-
const recorded = new Set();
|
|
145
|
-
const taken = new Set(Object.keys(repo.workspaces));
|
|
146
|
-
trees.forEach((tree, index) => {
|
|
147
|
-
if (tree.bare) return;
|
|
148
|
-
const found = Object.entries(repo.workspaces).find(([, ws]) => samePath(ws.path, tree.path));
|
|
149
|
-
if (found) {
|
|
150
|
-
recorded.add(found[0]);
|
|
151
|
-
items.push({ repoName, repo, name: found[0], ws: { ...found[1], locked: tree.locked }, main: false, managed: true });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
let name = normalizeName(tree.branch ?? basename(tree.path));
|
|
155
|
-
if (index > 0 && (!name || taken.has(name))) name = normalizeName(basename(tree.path));
|
|
156
|
-
taken.add(name);
|
|
157
|
-
const ws = { path: tree.path, branch: tree.branch, locked: tree.locked };
|
|
158
|
-
items.push({ repoName, repo, name, ws, main: index === 0, managed: false });
|
|
159
|
-
});
|
|
160
|
-
for (const [name, ws] of Object.entries(repo.workspaces))
|
|
161
|
-
if (!recorded.has(name)) items.push({ repoName, repo, name, ws, main: false, managed: true });
|
|
162
|
-
return items;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** An existing workspace of the current repository (or --repo) by name: its branch or folder name, converted the same way. */
|
|
166
|
-
export function resolveWorkspace(ctx, name, flag) {
|
|
167
|
-
const { name: repoName, repo } = resolveRepo(ctx, flag);
|
|
168
|
-
const items = repoWorkspaces(repoName, repo);
|
|
169
|
-
const hit = items.find((item) => item.name === name) ?? items.find((item) => normalizeName(basename(item.ws.path)) === name);
|
|
170
|
-
if (hit) return hit;
|
|
171
|
-
throw new NookError(`No workspace named "${name}" in ${repoName}. Create it with: nk -b ${name}`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** The git branch of a workspace: its name, unless the branch had characters a name cannot use. */
|
|
175
|
-
export function branchOf(name, ws) {
|
|
176
|
-
return "branch" in ws ? ws.branch : name;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Variables hooks and agents see. */
|
|
180
|
-
export function workspaceEnv(ctx, { repoName, repo, name, ws }) {
|
|
181
|
-
return {
|
|
182
|
-
...ctx.env,
|
|
183
|
-
NOOK_WORKSPACE: name,
|
|
184
|
-
NOOK_PATH: ws.path,
|
|
185
|
-
NOOK_REPO: repo.path,
|
|
186
|
-
NOOK_SHARED: join(ctx.paths.shared, repoName),
|
|
187
|
-
NOOK_PORT: String(ws.port ?? ""),
|
|
188
|
-
};
|
|
189
|
-
}
|
|
1
|
+
import { existsSync, renameSync } from "node:fs";
|
|
2
|
+
import { basename, join, relative } from "node:path";
|
|
3
|
+
import { applyChanges, loadConfig, saveConfig, syncInPlace, withLock, withLockSync } from "./config.mjs";
|
|
4
|
+
import { NookError } from "./errors.mjs";
|
|
5
|
+
import { homePaths, isInside, nookHome, samePath } from "./home.mjs";
|
|
6
|
+
import { normalizeName } from "./names.mjs";
|
|
7
|
+
import { gitTry, listWorktrees } from "./git.mjs";
|
|
8
|
+
import { line, useColor } from "./ui.mjs";
|
|
9
|
+
|
|
10
|
+
/** Shared state for one command run. */
|
|
11
|
+
export function createContext({ env = process.env, cwd = process.cwd(), out = process.stdout, err = process.stderr } = {}) {
|
|
12
|
+
const paths = homePaths(nookHome(env));
|
|
13
|
+
const config = loadConfig(paths.config);
|
|
14
|
+
const moveFailed = moveOldWorkspaceFolder(paths, config);
|
|
15
|
+
// What this run changed is config minus base; on save it goes onto what is on disk now,
|
|
16
|
+
// so changes another nk made meanwhile are kept.
|
|
17
|
+
let base = structuredClone(config);
|
|
18
|
+
const merge = () => {
|
|
19
|
+
const disk = applyChanges(base, config, loadConfig(paths.config));
|
|
20
|
+
saveConfig(paths.config, disk);
|
|
21
|
+
syncInPlace(config, disk);
|
|
22
|
+
base = structuredClone(config);
|
|
23
|
+
};
|
|
24
|
+
const color = useColor(err, env);
|
|
25
|
+
const say = (kind) => (text) => err.write(`${line(kind, text, color)}\n`);
|
|
26
|
+
if (moveFailed)
|
|
27
|
+
say("warn")(`Could not rename ${paths.workspaces} to ${join(paths.home, "workspace")} (${moveFailed}); still using the old folder. Close programs open in it (terminals, editors, dev servers) and run nk again.`);
|
|
28
|
+
return {
|
|
29
|
+
env,
|
|
30
|
+
cwd,
|
|
31
|
+
paths,
|
|
32
|
+
config,
|
|
33
|
+
out,
|
|
34
|
+
err,
|
|
35
|
+
save() {
|
|
36
|
+
withLockSync(paths.config, merge);
|
|
37
|
+
},
|
|
38
|
+
/**
|
|
39
|
+
* Brings config up to date with disk, runs fn on it and saves, all under the lock: for choices
|
|
40
|
+
* that must not clash with another nk running at the same time, such as a workspace's ports.
|
|
41
|
+
*/
|
|
42
|
+
async update(fn) {
|
|
43
|
+
return withLock(paths.config, async () => {
|
|
44
|
+
merge();
|
|
45
|
+
const result = await fn(config);
|
|
46
|
+
merge();
|
|
47
|
+
return result;
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
print(text = "") {
|
|
51
|
+
out.write(`${text}\n`);
|
|
52
|
+
},
|
|
53
|
+
note(text = "") {
|
|
54
|
+
err.write(`${text}\n`);
|
|
55
|
+
},
|
|
56
|
+
step: say("step"),
|
|
57
|
+
ok: say("ok"),
|
|
58
|
+
warn: say("warn"),
|
|
59
|
+
info: say("info"),
|
|
60
|
+
item: say("item"),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Before 0.1 the folder was called "workspaces". Renames it once and tells git
|
|
66
|
+
* where the workspaces went. When it cannot (a file is open on Windows), keeps
|
|
67
|
+
* using the old folder and returns why.
|
|
68
|
+
*/
|
|
69
|
+
function moveOldWorkspaceFolder(paths, config) {
|
|
70
|
+
const old = join(paths.home, "workspaces");
|
|
71
|
+
if (!existsSync(old) || existsSync(paths.workspaces)) return;
|
|
72
|
+
const moves = [];
|
|
73
|
+
for (const repo of Object.values(config.repos ?? {}))
|
|
74
|
+
for (const ws of Object.values(repo.workspaces ?? {}))
|
|
75
|
+
if (isInside(ws.path, old)) moves.push({ repo, ws, path: join(paths.workspaces, relative(old, ws.path)) });
|
|
76
|
+
try {
|
|
77
|
+
renameSync(old, paths.workspaces);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
paths.workspaces = old;
|
|
80
|
+
return error.code ?? error.message;
|
|
81
|
+
}
|
|
82
|
+
for (const move of moves) move.ws.path = move.path;
|
|
83
|
+
for (const repo of new Set(moves.map((move) => move.repo)))
|
|
84
|
+
gitTry(["worktree", "repair", ...moves.filter((move) => move.repo === repo).map((move) => move.path)], { cwd: repo.path });
|
|
85
|
+
if (moves.length) saveConfig(paths.config, config);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The registered repository `cwd` belongs to, or null. */
|
|
90
|
+
export function repoFromCwd(ctx) {
|
|
91
|
+
for (const [name, repo] of Object.entries(ctx.config.repos)) {
|
|
92
|
+
if (isInside(ctx.cwd, join(ctx.paths.workspaces, name))) return name;
|
|
93
|
+
if (isInside(ctx.cwd, repo.path)) return name;
|
|
94
|
+
for (const ws of Object.values(repo.workspaces)) if (isInside(ctx.cwd, ws.path)) return name;
|
|
95
|
+
}
|
|
96
|
+
const top = gitTry(["rev-parse", "--show-toplevel"], { cwd: ctx.cwd });
|
|
97
|
+
if (top.ok) {
|
|
98
|
+
try {
|
|
99
|
+
const [main] = listWorktrees(ctx.cwd);
|
|
100
|
+
for (const [name, repo] of Object.entries(ctx.config.repos)) if (main && isInside(main.path, repo.path)) return name;
|
|
101
|
+
} catch {
|
|
102
|
+
/* Not a usable repository. */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function requireRepo(ctx, name) {
|
|
109
|
+
const repo = ctx.config.repos[name];
|
|
110
|
+
if (!repo) throw new NookError(`Unknown repository "${name}". Registered: ${Object.keys(ctx.config.repos).join(", ") || "none"}`);
|
|
111
|
+
return { name, repo };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The repository a command acts on: --repo, else the one the current folder
|
|
116
|
+
* belongs to (its main checkout or one of its workspaces). Like git and
|
|
117
|
+
* worktrunk, nothing is guessed outside a repository.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveRepo(ctx, flag) {
|
|
120
|
+
if (flag) return requireRepo(ctx, flag);
|
|
121
|
+
const fromCwd = repoFromCwd(ctx);
|
|
122
|
+
if (fromCwd) return requireRepo(ctx, fromCwd);
|
|
123
|
+
if (!Object.keys(ctx.config.repos).length) throw new NookError("No repository yet. Run nk init inside a repository first");
|
|
124
|
+
throw new NookError(
|
|
125
|
+
`Not inside a registered repository. Run nk in the repository or one of its workspaces, or pass --repo (${Object.keys(ctx.config.repos).join(", ")})`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Every worktree of a repository, as git lists them, joined with what Nook
|
|
131
|
+
* recorded (port, creation time). Git is the source of truth, like worktrunk:
|
|
132
|
+
* worktrees made by other tools show up too, without a port. The main
|
|
133
|
+
* checkout comes first with `main: true`; workspaces Nook recorded whose
|
|
134
|
+
* folder is gone come last.
|
|
135
|
+
*/
|
|
136
|
+
export function repoWorkspaces(repoName, repo) {
|
|
137
|
+
let trees = [];
|
|
138
|
+
try {
|
|
139
|
+
trees = listWorktrees(repo.path);
|
|
140
|
+
} catch {
|
|
141
|
+
/* Not a usable repository; nk doctor reports it. */
|
|
142
|
+
}
|
|
143
|
+
const items = [];
|
|
144
|
+
const recorded = new Set();
|
|
145
|
+
const taken = new Set(Object.keys(repo.workspaces));
|
|
146
|
+
trees.forEach((tree, index) => {
|
|
147
|
+
if (tree.bare) return;
|
|
148
|
+
const found = Object.entries(repo.workspaces).find(([, ws]) => samePath(ws.path, tree.path));
|
|
149
|
+
if (found) {
|
|
150
|
+
recorded.add(found[0]);
|
|
151
|
+
items.push({ repoName, repo, name: found[0], ws: { ...found[1], locked: tree.locked }, main: false, managed: true });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let name = normalizeName(tree.branch ?? basename(tree.path));
|
|
155
|
+
if (index > 0 && (!name || taken.has(name))) name = normalizeName(basename(tree.path));
|
|
156
|
+
taken.add(name);
|
|
157
|
+
const ws = { path: tree.path, branch: tree.branch, locked: tree.locked };
|
|
158
|
+
items.push({ repoName, repo, name, ws, main: index === 0, managed: false });
|
|
159
|
+
});
|
|
160
|
+
for (const [name, ws] of Object.entries(repo.workspaces))
|
|
161
|
+
if (!recorded.has(name)) items.push({ repoName, repo, name, ws, main: false, managed: true });
|
|
162
|
+
return items;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** An existing workspace of the current repository (or --repo) by name: its branch or folder name, converted the same way. */
|
|
166
|
+
export function resolveWorkspace(ctx, name, flag) {
|
|
167
|
+
const { name: repoName, repo } = resolveRepo(ctx, flag);
|
|
168
|
+
const items = repoWorkspaces(repoName, repo);
|
|
169
|
+
const hit = items.find((item) => item.name === name) ?? items.find((item) => normalizeName(basename(item.ws.path)) === name);
|
|
170
|
+
if (hit) return hit;
|
|
171
|
+
throw new NookError(`No workspace named "${name}" in ${repoName}. Create it with: nk -b ${name}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The git branch of a workspace: its name, unless the branch had characters a name cannot use. */
|
|
175
|
+
export function branchOf(name, ws) {
|
|
176
|
+
return "branch" in ws ? ws.branch : name;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Variables hooks and agents see. */
|
|
180
|
+
export function workspaceEnv(ctx, { repoName, repo, name, ws }) {
|
|
181
|
+
return {
|
|
182
|
+
...ctx.env,
|
|
183
|
+
NOOK_WORKSPACE: name,
|
|
184
|
+
NOOK_PATH: ws.path,
|
|
185
|
+
NOOK_REPO: repo.path,
|
|
186
|
+
NOOK_SHARED: join(ctx.paths.shared, repoName),
|
|
187
|
+
NOOK_PORT: String(ws.port ?? ""),
|
|
188
|
+
};
|
|
189
|
+
}
|
package/src/errors.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/** An expected failure: printed as one line, without a stack trace. */
|
|
2
|
-
export class NookError extends Error {
|
|
3
|
-
constructor(message, { code = 1 } = {}) {
|
|
4
|
-
super(message);
|
|
5
|
-
this.name = "NookError";
|
|
6
|
-
this.code = code;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
1
|
+
/** An expected failure: printed as one line, without a stack trace. */
|
|
2
|
+
export class NookError extends Error {
|
|
3
|
+
constructor(message, { code = 1 } = {}) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "NookError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
package/src/forge.mjs
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
import spawn from "cross-spawn";
|
|
2
|
-
import { which } from "./agents.mjs";
|
|
3
|
-
|
|
4
|
-
const FAILED = new Set(["FAILURE", "ERROR", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"]);
|
|
5
|
-
const PENDING = new Set(["PENDING", "EXPECTED", "QUEUED", "IN_PROGRESS", "WAITING", "REQUESTED"]);
|
|
6
|
-
|
|
7
|
-
/** One word for a PR's checks: passed, failed, running or no-ci. */
|
|
8
|
-
export function ciState(checks = []) {
|
|
9
|
-
if (!checks.length) return "no-ci";
|
|
10
|
-
let running = false;
|
|
11
|
-
for (const check of checks) {
|
|
12
|
-
const result = check.conclusion || check.state || "";
|
|
13
|
-
if (FAILED.has(result)) return "failed";
|
|
14
|
-
if ((check.status && check.status !== "COMPLETED") || PENDING.has(result)) running = true;
|
|
15
|
-
}
|
|
16
|
-
return running ? "running" : "passed";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Newest PR per head branch, from `gh pr list --json` output (newest first). */
|
|
20
|
-
export function parsePrList(text) {
|
|
21
|
-
const prs = new Map();
|
|
22
|
-
for (const pr of JSON.parse(text)) {
|
|
23
|
-
if (prs.has(pr.headRefName)) continue;
|
|
24
|
-
prs.set(pr.headRefName, {
|
|
25
|
-
number: pr.number,
|
|
26
|
-
state: pr.state.toLowerCase(),
|
|
27
|
-
head: pr.headRefOid,
|
|
28
|
-
url: pr.url,
|
|
29
|
-
ci: pr.state === "OPEN" ? ciState(pr.statusCheckRollup) : null,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
return prs;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function explain(output) {
|
|
36
|
-
if (/auth login|not logged|authentication/i.test(output)) return "gh is not logged in (run gh auth login)";
|
|
37
|
-
if (/none of the git remotes|not a git repository|no git remotes/i.test(output)) return null;
|
|
38
|
-
return `gh failed: ${output.split(/\r?\n/)[0] || "no output"}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* PRs of a repository from GitHub, keyed by branch, through the gh CLI.
|
|
43
|
-
* Resolves to { prs } or { error } (null error: the repository is not on GitHub).
|
|
44
|
-
*/
|
|
45
|
-
export function githubPrs(repoPath, env = process.env) {
|
|
46
|
-
const gh = which("gh", env);
|
|
47
|
-
if (!gh) return Promise.resolve({ error: "gh is not installed" });
|
|
48
|
-
const fields = "number,state,headRefName,headRefOid,url,statusCheckRollup";
|
|
49
|
-
return new Promise((done) => {
|
|
50
|
-
const child = spawn(gh, ["pr", "list", "--state", "all", "--limit", "200", "--json", fields], {
|
|
51
|
-
cwd: repoPath,
|
|
52
|
-
env: { ...env, GH_PROMPT_DISABLED: "1", NO_COLOR: "1" },
|
|
53
|
-
windowsHide: true,
|
|
54
|
-
});
|
|
55
|
-
let out = "";
|
|
56
|
-
let err = "";
|
|
57
|
-
const timer = setTimeout(() => child.kill(), 20000);
|
|
58
|
-
child.stdout.on("data", (chunk) => (out += chunk));
|
|
59
|
-
child.stderr.on("data", (chunk) => (err += chunk));
|
|
60
|
-
child.on("error", (error) => {
|
|
61
|
-
clearTimeout(timer);
|
|
62
|
-
done({ error: `gh failed: ${error.message}` });
|
|
63
|
-
});
|
|
64
|
-
child.on("close", (code, signal) => {
|
|
65
|
-
clearTimeout(timer);
|
|
66
|
-
if (signal) return done({ error: "gh timed out" });
|
|
67
|
-
if (code !== 0) return done({ error: explain(err.trim()) });
|
|
68
|
-
try {
|
|
69
|
-
done({ prs: parsePrList(out) });
|
|
70
|
-
} catch {
|
|
71
|
-
done({ error: "gh returned output nk could not read" });
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
}
|
|
1
|
+
import spawn from "cross-spawn";
|
|
2
|
+
import { which } from "./agents.mjs";
|
|
3
|
+
|
|
4
|
+
const FAILED = new Set(["FAILURE", "ERROR", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"]);
|
|
5
|
+
const PENDING = new Set(["PENDING", "EXPECTED", "QUEUED", "IN_PROGRESS", "WAITING", "REQUESTED"]);
|
|
6
|
+
|
|
7
|
+
/** One word for a PR's checks: passed, failed, running or no-ci. */
|
|
8
|
+
export function ciState(checks = []) {
|
|
9
|
+
if (!checks.length) return "no-ci";
|
|
10
|
+
let running = false;
|
|
11
|
+
for (const check of checks) {
|
|
12
|
+
const result = check.conclusion || check.state || "";
|
|
13
|
+
if (FAILED.has(result)) return "failed";
|
|
14
|
+
if ((check.status && check.status !== "COMPLETED") || PENDING.has(result)) running = true;
|
|
15
|
+
}
|
|
16
|
+
return running ? "running" : "passed";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Newest PR per head branch, from `gh pr list --json` output (newest first). */
|
|
20
|
+
export function parsePrList(text) {
|
|
21
|
+
const prs = new Map();
|
|
22
|
+
for (const pr of JSON.parse(text)) {
|
|
23
|
+
if (prs.has(pr.headRefName)) continue;
|
|
24
|
+
prs.set(pr.headRefName, {
|
|
25
|
+
number: pr.number,
|
|
26
|
+
state: pr.state.toLowerCase(),
|
|
27
|
+
head: pr.headRefOid,
|
|
28
|
+
url: pr.url,
|
|
29
|
+
ci: pr.state === "OPEN" ? ciState(pr.statusCheckRollup) : null,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return prs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function explain(output) {
|
|
36
|
+
if (/auth login|not logged|authentication/i.test(output)) return "gh is not logged in (run gh auth login)";
|
|
37
|
+
if (/none of the git remotes|not a git repository|no git remotes/i.test(output)) return null;
|
|
38
|
+
return `gh failed: ${output.split(/\r?\n/)[0] || "no output"}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* PRs of a repository from GitHub, keyed by branch, through the gh CLI.
|
|
43
|
+
* Resolves to { prs } or { error } (null error: the repository is not on GitHub).
|
|
44
|
+
*/
|
|
45
|
+
export function githubPrs(repoPath, env = process.env) {
|
|
46
|
+
const gh = which("gh", env);
|
|
47
|
+
if (!gh) return Promise.resolve({ error: "gh is not installed" });
|
|
48
|
+
const fields = "number,state,headRefName,headRefOid,url,statusCheckRollup";
|
|
49
|
+
return new Promise((done) => {
|
|
50
|
+
const child = spawn(gh, ["pr", "list", "--state", "all", "--limit", "200", "--json", fields], {
|
|
51
|
+
cwd: repoPath,
|
|
52
|
+
env: { ...env, GH_PROMPT_DISABLED: "1", NO_COLOR: "1" },
|
|
53
|
+
windowsHide: true,
|
|
54
|
+
});
|
|
55
|
+
let out = "";
|
|
56
|
+
let err = "";
|
|
57
|
+
const timer = setTimeout(() => child.kill(), 20000);
|
|
58
|
+
child.stdout.on("data", (chunk) => (out += chunk));
|
|
59
|
+
child.stderr.on("data", (chunk) => (err += chunk));
|
|
60
|
+
child.on("error", (error) => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
done({ error: `gh failed: ${error.message}` });
|
|
63
|
+
});
|
|
64
|
+
child.on("close", (code, signal) => {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
if (signal) return done({ error: "gh timed out" });
|
|
67
|
+
if (code !== 0) return done({ error: explain(err.trim()) });
|
|
68
|
+
try {
|
|
69
|
+
done({ prs: parsePrList(out) });
|
|
70
|
+
} catch {
|
|
71
|
+
done({ error: "gh returned output nk could not read" });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|