@navbook/cli 0.4.0 → 0.5.0

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 (78) hide show
  1. package/dist/args.d.ts +28 -0
  2. package/dist/args.js +48 -0
  3. package/dist/args.js.map +1 -0
  4. package/dist/commands/complete.d.ts +10 -0
  5. package/dist/commands/complete.js +59 -51
  6. package/dist/commands/complete.js.map +1 -1
  7. package/dist/commands/compose.d.ts +35 -0
  8. package/dist/commands/doctor.d.ts +15 -0
  9. package/dist/commands/entity.d.ts +73 -0
  10. package/dist/commands/entity.js +59 -44
  11. package/dist/commands/entity.js.map +1 -1
  12. package/dist/commands/init.d.ts +11 -0
  13. package/dist/commands/install.d.ts +17 -0
  14. package/dist/commands/install.js +6 -3
  15. package/dist/commands/install.js.map +1 -1
  16. package/dist/commands/issue.d.ts +35 -0
  17. package/dist/commands/issue.js +4 -5
  18. package/dist/commands/issue.js.map +1 -1
  19. package/dist/commands/plugin.d.ts +36 -0
  20. package/dist/commands/plugin.js +386 -0
  21. package/dist/commands/plugin.js.map +1 -0
  22. package/dist/commands/policy.d.ts +58 -0
  23. package/dist/commands/policy.js +13 -0
  24. package/dist/commands/policy.js.map +1 -1
  25. package/dist/commands/pr-elsewhere.d.ts +40 -0
  26. package/dist/commands/pr-elsewhere.js +209 -0
  27. package/dist/commands/pr-elsewhere.js.map +1 -0
  28. package/dist/commands/pr.d.ts +69 -0
  29. package/dist/commands/pr.js +86 -60
  30. package/dist/commands/pr.js.map +1 -1
  31. package/dist/context.d.ts +42 -0
  32. package/dist/context.js +2 -0
  33. package/dist/context.js.map +1 -1
  34. package/dist/editor.d.ts +23 -0
  35. package/dist/editor.js +4 -0
  36. package/dist/editor.js.map +1 -1
  37. package/dist/errors.d.ts +17 -0
  38. package/dist/install/completions.d.ts +19 -0
  39. package/dist/install/hook.d.ts +32 -0
  40. package/dist/install/hook.js +48 -4
  41. package/dist/install/hook.js.map +1 -1
  42. package/dist/main.d.ts +21 -0
  43. package/dist/main.js +112 -8
  44. package/dist/main.js.map +1 -1
  45. package/dist/plugins/commands.d.ts +69 -0
  46. package/dist/plugins/commands.js +148 -0
  47. package/dist/plugins/commands.js.map +1 -0
  48. package/dist/plugins/hint.d.ts +23 -0
  49. package/dist/plugins/hint.js +65 -0
  50. package/dist/plugins/hint.js.map +1 -0
  51. package/dist/plugins/host.d.ts +125 -0
  52. package/dist/plugins/host.js +18 -0
  53. package/dist/plugins/host.js.map +1 -0
  54. package/dist/plugins/loader.d.ts +54 -0
  55. package/dist/plugins/loader.js +165 -0
  56. package/dist/plugins/loader.js.map +1 -0
  57. package/dist/plugins/resolve.d.ts +50 -0
  58. package/dist/plugins/resolve.js +134 -0
  59. package/dist/plugins/resolve.js.map +1 -0
  60. package/dist/plugins/runtime.d.ts +69 -0
  61. package/dist/plugins/runtime.js +256 -0
  62. package/dist/plugins/runtime.js.map +1 -0
  63. package/dist/plugins/store.d.ts +96 -0
  64. package/dist/plugins/store.js +186 -0
  65. package/dist/plugins/store.js.map +1 -0
  66. package/dist/program.d.ts +49 -0
  67. package/dist/program.js +351 -113
  68. package/dist/program.js.map +1 -1
  69. package/dist/prompt.d.ts +31 -0
  70. package/dist/render/colors.d.ts +13 -0
  71. package/dist/render/detail.d.ts +22 -0
  72. package/dist/render/table.d.ts +32 -0
  73. package/dist/render/table.js +123 -13
  74. package/dist/render/table.js.map +1 -1
  75. package/dist/sort.d.ts +27 -0
  76. package/package.json +9 -2
  77. package/dist/commands/feature.js +0 -202
  78. package/dist/commands/feature.js.map +0 -1
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Saying what the review policy counts, and what it is short of.
3
+ *
4
+ * Shared by every verb that reads the policy, so a pull request short of two
5
+ * approvals is described in the same words wherever it is mentioned. Nothing
6
+ * here refuses anything: spec 02 §2.7 forbids acting on a review state, and
7
+ * §2.10 keeps a declared policy inside that. These are sentences, not gates.
8
+ */
9
+ import { type MarkerVersionFault, type MergePolicyReading, type ReviewPolicyReading, type ReviewSummary } from "@navbook/core";
10
+ import type { Ctx } from "../context.ts";
11
+ /** Warn about a marker whose policy could not be read; D15 is the full report. */
12
+ export declare function warnPolicyProblems(ctx: Ctx, reading: ReviewPolicyReading): void;
13
+ /**
14
+ * The same, for the merge policy (spec 02 §2.10).
15
+ *
16
+ * Kept separate from the review policy's rather than folded into it, because a
17
+ * merge is the one command that reads both and a caller reading only one
18
+ * should not be made to hold the other.
19
+ */
20
+ export declare function warnMergePolicyProblems(ctx: Ctx, reading: MergePolicyReading): void;
21
+ /**
22
+ * Say that the tree was written by a newer Navbook than this one (D16).
23
+ *
24
+ * Only the newer kind: a malformed `version` changes nothing a command does
25
+ * and is D15's to report, while a newer one means every answer that follows
26
+ * is this tool's guess at a format it has never seen — which is worth hearing
27
+ * before the answer rather than after a confusing one.
28
+ */
29
+ export declare function warnNewerFormat(ctx: Ctx, fault: MarkerVersionFault | null): void;
30
+ /**
31
+ * What stands between a pull request and its policy, or null when nothing does.
32
+ *
33
+ * Only asked of a repository that declared a policy: a decision of `pending`
34
+ * where nobody asked for anything is not a shortfall, and saying so on every
35
+ * merge would be noise about a project that never opted in.
36
+ */
37
+ export declare function describeShortfall(summary: ReviewSummary): string | null;
38
+ /** What a merge should do about a shortfall, before anything is written. */
39
+ export type MergeAction = "proceed" | "warn-and-proceed" | "ask";
40
+ export interface MergeSituation {
41
+ /** What the pull request is short of, or null when it is short of nothing. */
42
+ shortfall: string | null;
43
+ /** `--yes` was given. */
44
+ assumeYes: boolean;
45
+ /** There is somebody at a terminal to answer a question. */
46
+ interactive: boolean;
47
+ }
48
+ /**
49
+ * Decide how a merge should treat an unmet policy.
50
+ *
51
+ * Kept apart from the merge itself so every combination can be checked without
52
+ * a repository or a terminal, and so the one rule that matters is visible in
53
+ * one place: no combination returns a refusal. `--yes` proceeds, a terminal is
54
+ * asked, and everything else says its piece and proceeds — because a pipeline
55
+ * that stopped for a question nobody can answer would be the gate spec 02 §2.7
56
+ * forbids, arrived at by accident rather than on purpose.
57
+ */
58
+ export declare function mergeAction(situation: MergeSituation): MergeAction;
@@ -26,6 +26,19 @@ function warnMarkerProblems(ctx, problems) {
26
26
  ctx.stderr.write(`${ctx.colors.yellow("warning:")} ${ctx.navDir}/${NAV_MARKER}: ${problem}; using the default\n`);
27
27
  }
28
28
  }
29
+ /**
30
+ * Say that the tree was written by a newer Navbook than this one (D16).
31
+ *
32
+ * Only the newer kind: a malformed `version` changes nothing a command does
33
+ * and is D15's to report, while a newer one means every answer that follows
34
+ * is this tool's guess at a format it has never seen — which is worth hearing
35
+ * before the answer rather than after a confusing one.
36
+ */
37
+ export function warnNewerFormat(ctx, fault) {
38
+ if (fault?.kind !== "newer")
39
+ return;
40
+ ctx.stderr.write(`${ctx.colors.yellow("warning:")} ${ctx.navDir}/${NAV_MARKER}: ${fault.message}\n`);
41
+ }
29
42
  /**
30
43
  * What stands between a pull request and its policy, or null when nothing does.
31
44
  *
@@ -1 +1 @@
1
- {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/commands/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEL,UAAU,GAGX,MAAM,eAAe,CAAC;AAGvB,kFAAkF;AAClF,MAAM,UAAU,kBAAkB,CAAC,GAAQ,EAAE,OAA4B;IACvE,kBAAkB,CAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,GAAQ,EAAE,OAA2B;IAC3E,kBAAkB,CAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,kBAAkB,CAAC,GAAQ,EAAE,QAA2B;IAC/D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,GAAG,CAAC,MAAM,CAAC,KAAK,CACd,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,UAAU,KAAK,OAAO,uBAAuB,CAChG,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAsB;IACtD,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS;SAC/B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,iBAAiB,CAAC;SACpD,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,wBAAwB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAE9E,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IAC9C,IAAI,KAAK,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,GAAG,KAAK,OAAO,QAAQ,qBAAqB,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AACjF,CAAC;AAcD;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,SAAyB;IACnD,IAAI,SAAS,CAAC,SAAS,KAAK,IAAI,IAAI,SAAS,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1E,OAAO,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,kBAAkB,CAAC;AAC5D,CAAC"}
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/commands/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAGL,UAAU,GAGX,MAAM,eAAe,CAAC;AAGvB,kFAAkF;AAClF,MAAM,UAAU,kBAAkB,CAAC,GAAQ,EAAE,OAA4B;IACvE,kBAAkB,CAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,GAAQ,EAAE,OAA2B;IAC3E,kBAAkB,CAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,kBAAkB,CAAC,GAAQ,EAAE,QAA2B;IAC/D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,GAAG,CAAC,MAAM,CAAC,KAAK,CACd,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,UAAU,KAAK,OAAO,uBAAuB,CAChG,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,GAAQ,EAAE,KAAgC;IACxE,IAAI,KAAK,EAAE,IAAI,KAAK,OAAO;QAAE,OAAO;IACpC,GAAG,CAAC,MAAM,CAAC,KAAK,CACd,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,UAAU,KAAK,KAAK,CAAC,OAAO,IAAI,CACnF,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAsB;IACtD,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS;SAC/B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,iBAAiB,CAAC;SACpD,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,wBAAwB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAE9E,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IAC9C,IAAI,KAAK,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,GAAG,KAAK,OAAO,QAAQ,qBAAqB,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AACjF,CAAC;AAcD;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,SAAyB;IACnD,IAAI,SAAS,CAAC,SAAS,KAAK,IAAI,IAAI,SAAS,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1E,OAAO,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,kBAAkB,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Writing to a pull request whose branch is not checked out here.
3
+ *
4
+ * A pull request's files live on its source branch (spec 03 §3.5), so the
5
+ * verbs that write into its directory refuse from any other checkout — core's
6
+ * `findPrToWrite` names the branch, and the worktree that has it. Everything
7
+ * needed to perform the write anyway is already known at that moment, so when
8
+ * somebody can be asked, this asks instead of only pointing:
9
+ *
10
+ * - When a clean worktree already has the branch, the write happens there.
11
+ * - When none has it, the branch is checked out into a temporary worktree,
12
+ * the write happens there, and the worktree goes away again once it holds
13
+ * nothing the user would lose.
14
+ *
15
+ * No directory changes either way. A child process cannot move its parent
16
+ * shell's working directory, and nothing is spawned in any case: a second
17
+ * `Ctx` rooted at the other worktree is enough, and the verb runs against it
18
+ * unchanged.
19
+ */
20
+ import { type EntityRecord } from "@navbook/core";
21
+ import type { Ctx } from "../context.ts";
22
+ /**
23
+ * Help for `-y`, which answers the question in advance. The same letter
24
+ * answers `nav pr merge` and `nav install` in advance, and means the same
25
+ * thing here: this verb's one question, whichever of the two it would be.
26
+ */
27
+ export declare const YES_HELP = "accept the offer to write in a worktree on the source branch \u2014 the clean one that has it, or a temporary one";
28
+ export interface PrWriteOptions {
29
+ /** `-y`: accept either offer in advance, and ask nobody. */
30
+ yes?: boolean;
31
+ }
32
+ /**
33
+ * Run a pull-request write where it belongs, asking first when that is not here.
34
+ *
35
+ * `write` gets the context to write in and the pull request as that context
36
+ * sees it. When the write cannot or may not move, this is core's refusal
37
+ * naming where the pull request lives — declining the question included: the
38
+ * answer was "not there", not "do it here".
39
+ */
40
+ export declare function withPrWriteSite<T>(ctx: Ctx, prefix: string, opts: PrWriteOptions, write: (at: Ctx, entity: EntityRecord) => T): T;
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Writing to a pull request whose branch is not checked out here.
3
+ *
4
+ * A pull request's files live on its source branch (spec 03 §3.5), so the
5
+ * verbs that write into its directory refuse from any other checkout — core's
6
+ * `findPrToWrite` names the branch, and the worktree that has it. Everything
7
+ * needed to perform the write anyway is already known at that moment, so when
8
+ * somebody can be asked, this asks instead of only pointing:
9
+ *
10
+ * - When a clean worktree already has the branch, the write happens there.
11
+ * - When none has it, the branch is checked out into a temporary worktree,
12
+ * the write happens there, and the worktree goes away again once it holds
13
+ * nothing the user would lose.
14
+ *
15
+ * No directory changes either way. A child process cannot move its parent
16
+ * shell's working directory, and nothing is spawned in any case: a second
17
+ * `Ctx` rooted at the other worktree is enough, and the verb runs against it
18
+ * unchanged.
19
+ */
20
+ import { mkdtempSync, rmSync } from "node:fs";
21
+ import { tmpdir } from "node:os";
22
+ import { join } from "node:path";
23
+ import { addWorktree, findPrToWrite, gitRun, isTreeClean, locatePrToWrite, refusePrWrite, removeWorktree, } from "@navbook/core";
24
+ import { makeContext } from "../context.js";
25
+ import { fail } from "../errors.js";
26
+ import { askYesNo, isInteractive } from "../prompt.js";
27
+ /**
28
+ * Help for `-y`, which answers the question in advance. The same letter
29
+ * answers `nav pr merge` and `nav install` in advance, and means the same
30
+ * thing here: this verb's one question, whichever of the two it would be.
31
+ */
32
+ export const YES_HELP = "accept the offer to write in a worktree on the source branch — the clean one that has it, or a temporary one";
33
+ /**
34
+ * Where the write could go instead of here, or null when nowhere can take it.
35
+ *
36
+ * An existing worktree must be clean: without `--commit` a write leaves a
37
+ * staged change behind, and one mixed into somebody's work in progress where
38
+ * they are not looking is not a favour; with it, the commit would be refused
39
+ * over the unrelated staged paths anyway. A branch no worktree has can be
40
+ * checked out into a new one — but only a local branch: checking out a
41
+ * remote-tracking one would create a local branch as a side effect, which is
42
+ * more than a comment should do.
43
+ */
44
+ function destination(where) {
45
+ if (where.sourceRemote)
46
+ return null;
47
+ if (where.worktree === null)
48
+ return { branch: where.branch, worktree: null };
49
+ return where.worktreeClean ? { branch: where.branch, worktree: where.worktree } : null;
50
+ }
51
+ /** Ask whether to write there, unless `-y` already said yes. */
52
+ function agreed(ctx, entity, to, opts) {
53
+ if (opts.yes)
54
+ return true;
55
+ // Only ever asked, never assumed. A run with nothing to answer the question
56
+ // keeps the refusal it has always had: a pipeline that started writing into
57
+ // a checkout nobody mentioned would be a worse surprise than exit 1.
58
+ if (!isInteractive(ctx))
59
+ return false;
60
+ if (to.worktree !== null) {
61
+ ctx.stdout.write(`#${entity.id} is on '${to.branch}', checked out in ${to.worktree} ${ctx.colors.dim("(clean)")}\n`);
62
+ return askYesNo(ctx, "Write it there? [y/N] ");
63
+ }
64
+ ctx.stdout.write(`#${entity.id} is on '${to.branch}', which no worktree has checked out\n`);
65
+ return askYesNo(ctx, "Check it out in a temporary worktree and write it there? [y/N] ");
66
+ }
67
+ /**
68
+ * A context rooted at `worktree`, reporting to the terminal we already have.
69
+ *
70
+ * The Navbook directory is the one already resolved here rather than found
71
+ * again: the pull request was located under it, and a second discovery from
72
+ * another root is one more chance to land somewhere it is not. The plugin
73
+ * extensions carry over for the same reason: the other checkout's tree is
74
+ * the same format, and read without them its registered keys would be raw.
75
+ */
76
+ function contextIn(ctx, worktree) {
77
+ return makeContext({
78
+ cwd: worktree,
79
+ env: ctx.env,
80
+ stdout: ctx.stdout,
81
+ stderr: ctx.stderr,
82
+ navDir: ctx.navDir,
83
+ ext: ctx.ext,
84
+ });
85
+ }
86
+ /**
87
+ * Run `write` in `worktree`, refusing if that checkout does not hold the pull
88
+ * request after all — the branch moved since we looked, say. Without this the
89
+ * write would land beside no `pr.md`, the stranded comment the refusal exists
90
+ * to prevent.
91
+ */
92
+ function writeIn(ctx, worktree, prefix, write) {
93
+ const there = contextIn(ctx, worktree);
94
+ return write(there, findPrToWrite(there, prefix));
95
+ }
96
+ /**
97
+ * A new worktree on `branch` in a fresh temporary directory.
98
+ *
99
+ * `mkdtemp` rather than a fixed name, so two runs never collide and nothing a
100
+ * user made is ever in the way; under `os.tmpdir()`, so `TMPDIR` says where.
101
+ * The branch only flavours the name, so it is cut short: a directory name has
102
+ * a length limit and a branch name has none.
103
+ */
104
+ function checkOutTemporarily(ctx, branch) {
105
+ const safe = branch.replace(/[^A-Za-z0-9._-]+/g, "-").slice(0, 40);
106
+ let dir = null;
107
+ try {
108
+ dir = mkdtempSync(join(tmpdir(), `nav-${safe}-`));
109
+ addWorktree(ctx.repoRoot, dir, branch);
110
+ return dir;
111
+ }
112
+ catch (error) {
113
+ if (dir !== null)
114
+ rmSync(dir, { recursive: true, force: true });
115
+ fail(`could not check '${branch}' out into a temporary worktree`, [
116
+ error instanceof Error ? error.message.trim() : String(error),
117
+ ]);
118
+ }
119
+ }
120
+ /**
121
+ * Take a temporary worktree away again, unless it holds what the user would lose.
122
+ *
123
+ * After `--commit` the write is on the branch and the worktree holds nothing.
124
+ * Without it — or when the commit itself failed, a hook refusing it, say — the
125
+ * write is staged there and nowhere else, so the worktree stays and says where
126
+ * it is: removing it would throw away a review somebody just wrote. A run that
127
+ * failed before writing anything leaves it clean, and it goes. Untracked files
128
+ * do not count: nothing but this run has been in there.
129
+ */
130
+ function release(ctx, dir, branch, failed) {
131
+ if (isTreeClean(dir, { untracked: false })) {
132
+ try {
133
+ removeWorktree(ctx.repoRoot, dir, { force: true });
134
+ }
135
+ catch {
136
+ // Never mask the error that brought us here with one about tidying up.
137
+ // Deleting the directory alone would leave git recording the branch as
138
+ // checked out at a path that no longer exists, so prune that too.
139
+ rmSync(dir, { recursive: true, force: true });
140
+ gitRun(["worktree", "prune"], { cwd: ctx.repoRoot });
141
+ }
142
+ if (!failed) {
143
+ ctx.stderr.write(`${ctx.colors.dim(`written in a temporary worktree on '${branch}', since removed`)}\n`);
144
+ }
145
+ return;
146
+ }
147
+ ctx.stderr.write(`${ctx.colors.yellow("note:")} the change is staged in ${dir}, a temporary worktree on '${branch}'\n` +
148
+ `commit it there, then remove it: 'git worktree remove ${dir}'\n`);
149
+ }
150
+ /** Terminal signals, which reach the editor and git as well as us. */
151
+ const TERMINAL_SIGNALS = ["SIGINT", "SIGQUIT", "SIGHUP"];
152
+ /**
153
+ * Run `body` with terminal signals left to the children.
154
+ *
155
+ * Ctrl-C while `$EDITOR` is open goes to the whole foreground process group.
156
+ * By default it would kill us mid-`spawnSync`, before any `finally` runs, and
157
+ * leave the temporary worktree registered with the branch checked out. Git
158
+ * ignores these signals while its own editor runs, for the same reason; so
159
+ * does this. The editor still gets the signal, exits, and the command fails
160
+ * through its ordinary path — which is the one that tidies up.
161
+ */
162
+ function sheltered(body) {
163
+ const ignore = () => { };
164
+ for (const signal of TERMINAL_SIGNALS)
165
+ process.on(signal, ignore);
166
+ try {
167
+ return body();
168
+ }
169
+ finally {
170
+ for (const signal of TERMINAL_SIGNALS)
171
+ process.off(signal, ignore);
172
+ }
173
+ }
174
+ /**
175
+ * Run a pull-request write where it belongs, asking first when that is not here.
176
+ *
177
+ * `write` gets the context to write in and the pull request as that context
178
+ * sees it. When the write cannot or may not move, this is core's refusal
179
+ * naming where the pull request lives — declining the question included: the
180
+ * answer was "not there", not "do it here".
181
+ */
182
+ export function withPrWriteSite(ctx, prefix, opts, write) {
183
+ const { entity, elsewhere } = locatePrToWrite(ctx, prefix);
184
+ if (elsewhere === null)
185
+ return write(ctx, entity);
186
+ const to = destination(elsewhere);
187
+ // Built from what was just found, rather than scanning every ref again.
188
+ if (to === null || !agreed(ctx, entity, to, opts))
189
+ refusePrWrite(entity, elsewhere);
190
+ if (to.worktree !== null) {
191
+ const result = writeIn(ctx, to.worktree, prefix, write);
192
+ ctx.stderr.write(`${ctx.colors.dim(`written in ${to.worktree}`)}\n`);
193
+ return result;
194
+ }
195
+ const { branch } = to;
196
+ return sheltered(() => {
197
+ const dir = checkOutTemporarily(ctx, branch);
198
+ let failed = true;
199
+ try {
200
+ const result = writeIn(ctx, dir, prefix, write);
201
+ failed = false;
202
+ return result;
203
+ }
204
+ finally {
205
+ release(ctx, dir, branch, failed);
206
+ }
207
+ });
208
+ }
209
+ //# sourceMappingURL=pr-elsewhere.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pr-elsewhere.js","sourceRoot":"","sources":["../../src/commands/pr-elsewhere.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `nav pr <verb>` — spec 04 §4.3.
3
+ *
4
+ * The eight shared verbs come from `entity.ts`; this module adds the four that
5
+ * only pull requests have (`update`, `request`, `review`, `merge`) and the
6
+ * parts of the shared verbs that must reach across branches, because a PR's
7
+ * files live on the branch it proposes to merge (spec 03 §3.5).
8
+ */
9
+ import type { Ctx } from "../context.ts";
10
+ import { type CloseOptions, type GlobalFlags, type ListOptions } from "./entity.ts";
11
+ import { type PrWriteOptions } from "./pr-elsewhere.ts";
12
+ export interface PrOpenOptions extends GlobalFlags {
13
+ target?: string;
14
+ title?: string;
15
+ message?: string;
16
+ draft?: boolean;
17
+ label?: string[];
18
+ assignee?: string[];
19
+ reviewer?: string[];
20
+ milestone?: string;
21
+ /**
22
+ * Frontmatter a plugin contributed, from an option it declared (§2.12).
23
+ *
24
+ * Written by the same composer that writes the format's own keys, so a
25
+ * plugin's value lands in the file exactly as a built-in one would.
26
+ */
27
+ ext?: Record<string, string | readonly string[]>;
28
+ }
29
+ export declare function cmdPrOpen(ctx: Ctx, opts: PrOpenOptions): void;
30
+ export interface PrUpdateOptions extends GlobalFlags, PrWriteOptions {
31
+ }
32
+ export declare function cmdPrUpdate(ctx: Ctx, prefix: string, opts: PrUpdateOptions): void;
33
+ export interface RequestOptions extends GlobalFlags, PrWriteOptions {
34
+ remove?: boolean;
35
+ }
36
+ export declare function cmdPrRequest(ctx: Ctx, prefix: string, people: string[], opts: RequestOptions): void;
37
+ export interface ReviewOptions extends GlobalFlags, PrWriteOptions {
38
+ approve?: boolean;
39
+ requestChanges?: boolean;
40
+ comment?: boolean;
41
+ message?: string;
42
+ revision?: string;
43
+ file?: string;
44
+ line?: string;
45
+ }
46
+ export declare function cmdPrReview(ctx: Ctx, prefix: string, opts: ReviewOptions): void;
47
+ export interface PrListOptions extends ListOptions {
48
+ allRefs?: boolean;
49
+ }
50
+ export declare function cmdPrList(ctx: Ctx, terms: string[], opts: PrListOptions): void;
51
+ export interface MergeOptions extends GlobalFlags {
52
+ /** `--method`: land by this method instead of the marker's (spec 02 §2.10). */
53
+ method?: string;
54
+ continue?: boolean;
55
+ /** Answer the review-policy question in advance. */
56
+ yes?: boolean;
57
+ /** `--no-sync-source`: leave the source branch where it is. */
58
+ syncSource?: boolean;
59
+ }
60
+ export declare function cmdPrMerge(ctx: Ctx, prefix: string | undefined, opts: MergeOptions): void;
61
+ /**
62
+ * Decline a pull request.
63
+ *
64
+ * Its files usually live on the source branch, so when the ID is not in the
65
+ * current tree the directory is first checked out from the ref that has it —
66
+ * which is exactly the manual recipe spec 02 §2.8 describes for keeping a
67
+ * durable record of a declined PR on the default branch.
68
+ */
69
+ export declare function cmdPrClose(ctx: Ctx, prefix: string, opts: CloseOptions): void;
@@ -6,12 +6,13 @@
6
6
  * parts of the shared verbs that must reach across branches, because a PR's
7
7
  * files live on the branch it proposes to merge (spec 03 §3.5).
8
8
  */
9
- import { applyComment, bindReviewRevision, commitReport, continuePrMerge, countOpenPrsOnOtherRefs, currentAuthor, entityJson, executePrMerge, findPrToWrite, isMergeMethod, listEntities, listPrsAcrossRefs, MERGE_METHODS, materializePrIfAbsent, newCommentFile, newPrFile, openPr, parseListQuery, parsePerson, planPrMerge, preparePrOpen, readReviewers, readReviewPolicy, requestReview, reviewSummary, sameEmail, stringField, toNdjson, updatePr, validateComment, validatePr, } from "@navbook/core";
9
+ import { applyComment, bindReviewRevision, commitReport, continuePrMerge, countOpenPrsOnOtherRefs, currentAuthor, entityJson, executePrMerge, isMergeMethod, listEntities, listPrsAcrossRefs, MERGE_METHODS, materializePrIfAbsent, newCommentFile, newPrFile, openPr, parseListQuery, parsePerson, planPrMerge, preparePrOpen, readReviewers, readReviewPolicy, requestReview, reviewSummary, sameEmail, stringField, toNdjson, updatePr, validateComment, validatePr, } from "@navbook/core";
10
10
  import { fail } from "../errors.js";
11
11
  import { askYesNo, isInteractive } from "../prompt.js";
12
12
  import { composeFile } from "./compose.js";
13
13
  import { cmdClose, reportList, } from "./entity.js";
14
14
  import { describeShortfall, mergeAction, warnMergePolicyProblems, warnPolicyProblems, } from "./policy.js";
15
+ import { withPrWriteSite } from "./pr-elsewhere.js";
15
16
  export function cmdPrOpen(ctx, opts) {
16
17
  const draft = preparePrOpen(ctx, { target: opts.target, title: opts.title });
17
18
  const composed = composeFile(ctx, {
@@ -31,9 +32,9 @@ export function cmdPrOpen(ctx, opts) {
31
32
  labels: opts.label,
32
33
  assignee: opts.assignee,
33
34
  milestone: opts.milestone,
34
- features: opts.feature,
35
+ ...(opts.ext ? { ext: opts.ext } : {}),
35
36
  }),
36
- validate: validatePr,
37
+ validate: (parsed) => validatePr(parsed, ctx.ext),
37
38
  });
38
39
  const { id, dirPath, run } = openPr(ctx, { content: composed.content, fallbackTitle: draft.title }, { commit: opts.commit });
39
40
  ctx.stdout.write(`Created ${ctx.navDir}/${dirPath}/ (#${id})\n`);
@@ -41,72 +42,83 @@ export function cmdPrOpen(ctx, opts) {
41
42
  if (opts.commit)
42
43
  ctx.stdout.write(`${commitReport(run)}\n`);
43
44
  }
44
- /* ------------------------------------------------------------------- update */
45
45
  export function cmdPrUpdate(ctx, prefix, opts) {
46
- const { entity, head, revisionCount, run } = updatePr(ctx, prefix, { commit: opts.commit });
47
- ctx.stdout.write(`Recorded revision ${revisionCount} of #${entity.id} head ${head.slice(0, 12)}\n`);
48
- if (opts.commit)
49
- ctx.stdout.write(`${commitReport(run)}\n`);
46
+ // The revision pinned is the HEAD of wherever this runs, so moving the write
47
+ // to a worktree on the source branch is what pins the right one — this
48
+ // checkout's HEAD was never the head under review.
49
+ withPrWriteSite(ctx, prefix, opts, (at) => {
50
+ const { entity, head, revisionCount, run } = updatePr(at, prefix, { commit: opts.commit });
51
+ ctx.stdout.write(`Recorded revision ${revisionCount} of #${entity.id} head ${head.slice(0, 12)}\n`);
52
+ if (opts.commit)
53
+ ctx.stdout.write(`${commitReport(run)}\n`);
54
+ });
50
55
  }
51
56
  export function cmdPrRequest(ctx, prefix, people, opts) {
52
- const { entity, changed, unchanged, run } = requestReview(ctx, prefix, people, {
53
- commit: opts.commit,
54
- remove: opts.remove,
57
+ withPrWriteSite(ctx, prefix, opts, (at) => {
58
+ const { entity, changed, unchanged, run } = requestReview(at, prefix, people, {
59
+ commit: opts.commit,
60
+ remove: opts.remove,
61
+ });
62
+ const verb = opts.remove ? "No longer reviewing" : "Asked to review";
63
+ ctx.stdout.write(`${verb} #${entity.id}: ${changed.join(", ")}\n`);
64
+ // Naming who was already there matters most when only some of a list moved:
65
+ // the count alone would leave the caller counting names themselves.
66
+ for (const person of unchanged) {
67
+ ctx.stdout.write(`${ctx.colors.dim(opts.remove ? "not listed:" : "already listed:")} ${person}\n`);
68
+ }
69
+ if (opts.commit)
70
+ ctx.stdout.write(`${commitReport(run)}\n`);
55
71
  });
56
- const verb = opts.remove ? "No longer reviewing" : "Asked to review";
57
- ctx.stdout.write(`${verb} #${entity.id}: ${changed.join(", ")}\n`);
58
- // Naming who was already there matters most when only some of a list moved:
59
- // the count alone would leave the caller counting names themselves.
60
- for (const person of unchanged) {
61
- ctx.stdout.write(`${ctx.colors.dim(opts.remove ? "not listed:" : "already listed:")} ${person}\n`);
62
- }
63
- if (opts.commit)
64
- ctx.stdout.write(`${commitReport(run)}\n`);
65
72
  }
66
73
  export function cmdPrReview(ctx, prefix, opts) {
67
- const entity = findPrToWrite(ctx, prefix);
74
+ // Checked before anything is resolved, so a mistyped flag never costs a
75
+ // temporary checkout.
68
76
  const chosen = [opts.approve, opts.requestChanges, opts.comment].filter(Boolean).length;
69
77
  if (chosen > 1) {
70
78
  fail("choose one of --approve, --request-changes or --comment, not several");
71
79
  }
72
- const revision = bindReviewRevision(entity, opts.revision);
73
80
  if (opts.line && !opts.file)
74
81
  fail("--line needs --file");
75
- // This verb files reviews, so with no flag the verdict is the one that judges
76
- // nothing (spec 04 §4.3). The exception is an inline anchor: a note about one
77
- // line is discussion, and recording it as a review would say its author had
78
- // read the whole revision. `nav pr comment` remains the unbound comment.
79
- const verdict = opts.approve
80
- ? "approve"
81
- : opts.requestChanges
82
- ? "request-changes"
83
- : opts.file === undefined || opts.comment
84
- ? "comment"
85
- : undefined;
86
- const isReview = verdict !== undefined || opts.file !== undefined;
87
- const base = {
88
- author: currentAuthor(ctx),
89
- body: "",
90
- ...(verdict ? { verdict } : {}),
91
- ...(isReview ? { revision } : {}),
92
- ...(opts.file ? { file: opts.file } : {}),
93
- ...(opts.line ? { line: opts.line } : {}),
94
- };
95
- const composed = composeFile(ctx, {
96
- message: opts.message,
97
- bufferName: "NAVBOOK_REVIEW.md",
98
- noun: verdict ? "review" : "comment",
99
- render: (body) => newCommentFile({ ...base, body }),
100
- validate: (parsed) => validateComment(parsed, { onPr: true }),
82
+ withPrWriteSite(ctx, prefix, opts, (at, entity) => {
83
+ const revision = bindReviewRevision(entity, opts.revision);
84
+ // This verb files reviews, so with no flag the verdict is the one that judges
85
+ // nothing (spec 04 §4.3). The exception is an inline anchor: a note about one
86
+ // line is discussion, and recording it as a review would say its author had
87
+ // read the whole revision. `nav pr comment` remains the unbound comment.
88
+ const verdict = opts.approve
89
+ ? "approve"
90
+ : opts.requestChanges
91
+ ? "request-changes"
92
+ : opts.file === undefined || opts.comment
93
+ ? "comment"
94
+ : undefined;
95
+ const isReview = verdict !== undefined || opts.file !== undefined;
96
+ const base = {
97
+ author: currentAuthor(at),
98
+ body: "",
99
+ ...(verdict ? { verdict } : {}),
100
+ ...(isReview ? { revision } : {}),
101
+ ...(opts.file ? { file: opts.file } : {}),
102
+ ...(opts.line ? { line: opts.line } : {}),
103
+ };
104
+ const composed = composeFile(ctx, {
105
+ message: opts.message,
106
+ bufferName: "NAVBOOK_REVIEW.md",
107
+ noun: verdict ? "review" : "comment",
108
+ render: (body) => newCommentFile({ ...base, body }),
109
+ validate: (parsed) => validateComment(parsed, { onPr: true }),
110
+ });
111
+ const { id, path, run } = applyComment(at, entity, { content: composed.content, review: isReview }, { commit: opts.commit });
112
+ const label = verdict ? `Reviewed (${verdict})` : "Commented on";
113
+ ctx.stdout.write(`${label} #${entity.id} ${at.navDir}/${path} (#${id})\n`);
114
+ if (isReview)
115
+ ctx.stdout.write(`Bound to revision ${revision.slice(0, 12)}\n`);
116
+ if (opts.commit)
117
+ ctx.stdout.write(`${commitReport(run)}\n`);
118
+ // The policy that decides whether a verdict counts is the one in the tree
119
+ // the review was written into.
120
+ warnIfOwnVerdict(at, entity, verdict);
101
121
  });
102
- const { id, path, run } = applyComment(ctx, entity, { content: composed.content, review: isReview }, { commit: opts.commit });
103
- const label = verdict ? `Reviewed (${verdict})` : "Commented on";
104
- ctx.stdout.write(`${label} #${entity.id} ${ctx.navDir}/${path} (#${id})\n`);
105
- if (isReview)
106
- ctx.stdout.write(`Bound to revision ${revision.slice(0, 12)}\n`);
107
- if (opts.commit)
108
- ctx.stdout.write(`${commitReport(run)}\n`);
109
- warnIfOwnVerdict(ctx, entity, verdict);
110
122
  }
111
123
  /**
112
124
  * Warn an author that their own verdict will not be counted.
@@ -161,10 +173,13 @@ export function cmdPrList(ctx, terms, opts) {
161
173
  when: (entities) => entities.some((entity) => summaryOf(entity).reviewers.length > 0),
162
174
  },
163
175
  ];
176
+ // The listing's own columns first, then whatever plugins contributed: the
177
+ // caller's are added to these, never replaced by them.
178
+ const columns = [...extraColumns, ...(opts.extraColumns ?? [])];
164
179
  const query = parseListQuery(ctx, terms, "pr");
165
180
  if (!opts.allRefs) {
166
181
  const here = listEntities(ctx, "pr", query);
167
- reportList(ctx, "pr", here, { ...opts, extraColumns });
182
+ reportList(ctx, "pr", here, { ...opts, extraColumns: columns });
168
183
  if (here.length === 0)
169
184
  hintOtherRefs(ctx, opts);
170
185
  return;
@@ -173,7 +188,10 @@ export function cmdPrList(ctx, terms, opts) {
173
188
  if (opts.json) {
174
189
  if (matched.length === 0)
175
190
  return;
176
- ctx.stdout.write(`${toNdjson(matched.map((entry) => entityJson(ctx.navDir, entry.entity, { refs: entry.refs.map((ref) => ref.short) })))}\n`);
191
+ ctx.stdout.write(`${toNdjson(matched.map((entry) => entityJson(ctx.navDir, entry.entity, {
192
+ ...(opts.jsonExtra?.(entry.entity) ?? {}),
193
+ refs: entry.refs.map((ref) => ref.short),
194
+ })))}\n`);
177
195
  return;
178
196
  }
179
197
  if (matched.length === 0) {
@@ -183,7 +201,7 @@ export function cmdPrList(ctx, terms, opts) {
183
201
  reportList(ctx, "pr", listEntities(ctx, "pr", query, { entities: matched.map((e) => e.entity) }), {
184
202
  ...opts,
185
203
  extraColumns: [
186
- ...extraColumns,
204
+ ...columns,
187
205
  {
188
206
  header: "refs",
189
207
  value: (entity) => (matched.find((entry) => entry.entity.id === entity.id)?.refs ?? [])
@@ -208,7 +226,15 @@ export function cmdPrList(ctx, terms, opts) {
208
226
  function hintOtherRefs(ctx, opts) {
209
227
  if (opts.json)
210
228
  return;
211
- const count = countOpenPrsOnOtherRefs(ctx);
229
+ let count;
230
+ try {
231
+ count = countOpenPrsOnOtherRefs(ctx);
232
+ }
233
+ catch {
234
+ // The listing this follows has already been printed and is right; a branch
235
+ // git cannot read is `--all-refs`'s to report, not a reason to fail this.
236
+ return;
237
+ }
212
238
  if (count === 0)
213
239
  return;
214
240
  ctx.stderr.write(`${ctx.colors.dim(`${count} open pull ${count === 1 ? "request" : "requests"} on other branches; ` +