@navbook/cli 0.1.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 (135) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +165 -0
  3. package/dist/cli/commands/complete.d.ts +9 -0
  4. package/dist/cli/commands/complete.js +72 -0
  5. package/dist/cli/commands/complete.js.map +1 -0
  6. package/dist/cli/commands/compose.d.ts +27 -0
  7. package/dist/cli/commands/compose.js +44 -0
  8. package/dist/cli/commands/compose.js.map +1 -0
  9. package/dist/cli/commands/doctor.d.ts +14 -0
  10. package/dist/cli/commands/doctor.js +127 -0
  11. package/dist/cli/commands/doctor.js.map +1 -0
  12. package/dist/cli/commands/entity.d.ts +55 -0
  13. package/dist/cli/commands/entity.js +209 -0
  14. package/dist/cli/commands/entity.js.map +1 -0
  15. package/dist/cli/commands/init.d.ts +11 -0
  16. package/dist/cli/commands/init.js +39 -0
  17. package/dist/cli/commands/init.js.map +1 -0
  18. package/dist/cli/commands/install.d.ts +17 -0
  19. package/dist/cli/commands/install.js +137 -0
  20. package/dist/cli/commands/install.js.map +1 -0
  21. package/dist/cli/commands/issue.d.ts +12 -0
  22. package/dist/cli/commands/issue.js +42 -0
  23. package/dist/cli/commands/issue.js.map +1 -0
  24. package/dist/cli/commands/pr.d.ts +64 -0
  25. package/dist/cli/commands/pr.js +466 -0
  26. package/dist/cli/commands/pr.js.map +1 -0
  27. package/dist/cli/commit-flow.d.ts +27 -0
  28. package/dist/cli/commit-flow.js +47 -0
  29. package/dist/cli/commit-flow.js.map +1 -0
  30. package/dist/cli/context.d.ts +39 -0
  31. package/dist/cli/context.js +93 -0
  32. package/dist/cli/context.js.map +1 -0
  33. package/dist/cli/editor.d.ts +23 -0
  34. package/dist/cli/editor.js +67 -0
  35. package/dist/cli/editor.js.map +1 -0
  36. package/dist/cli/errors.d.ts +17 -0
  37. package/dist/cli/errors.js +23 -0
  38. package/dist/cli/errors.js.map +1 -0
  39. package/dist/cli/history-checks.d.ts +21 -0
  40. package/dist/cli/history-checks.js +134 -0
  41. package/dist/cli/history-checks.js.map +1 -0
  42. package/dist/cli/install/completions.d.ts +19 -0
  43. package/dist/cli/install/completions.js +94 -0
  44. package/dist/cli/install/completions.js.map +1 -0
  45. package/dist/cli/install/hook.d.ts +20 -0
  46. package/dist/cli/install/hook.js +78 -0
  47. package/dist/cli/install/hook.js.map +1 -0
  48. package/dist/cli/main.d.ts +14 -0
  49. package/dist/cli/main.js +88 -0
  50. package/dist/cli/main.js.map +1 -0
  51. package/dist/cli/program.d.ts +23 -0
  52. package/dist/cli/program.js +204 -0
  53. package/dist/cli/program.js.map +1 -0
  54. package/dist/cli/prompt.d.ts +22 -0
  55. package/dist/cli/prompt.js +51 -0
  56. package/dist/cli/prompt.js.map +1 -0
  57. package/dist/cli/render/colors.d.ts +13 -0
  58. package/dist/cli/render/colors.js +19 -0
  59. package/dist/cli/render/colors.js.map +1 -0
  60. package/dist/cli/render/detail.d.ts +10 -0
  61. package/dist/cli/render/detail.js +105 -0
  62. package/dist/cli/render/detail.js.map +1 -0
  63. package/dist/cli/render/table.d.ts +19 -0
  64. package/dist/cli/render/table.js +58 -0
  65. package/dist/cli/render/table.js.map +1 -0
  66. package/dist/cli/resolve.d.ts +17 -0
  67. package/dist/cli/resolve.js +68 -0
  68. package/dist/cli/resolve.js.map +1 -0
  69. package/dist/cli/workspace.d.ts +52 -0
  70. package/dist/cli/workspace.js +178 -0
  71. package/dist/cli/workspace.js.map +1 -0
  72. package/dist/core/comments.d.ts +31 -0
  73. package/dist/core/comments.js +80 -0
  74. package/dist/core/comments.js.map +1 -0
  75. package/dist/core/files.d.ts +88 -0
  76. package/dist/core/files.js +441 -0
  77. package/dist/core/files.js.map +1 -0
  78. package/dist/core/frontmatter.d.ts +63 -0
  79. package/dist/core/frontmatter.js +149 -0
  80. package/dist/core/frontmatter.js.map +1 -0
  81. package/dist/core/id.d.ts +46 -0
  82. package/dist/core/id.js +66 -0
  83. package/dist/core/id.js.map +1 -0
  84. package/dist/core/index.d.ts +19 -0
  85. package/dist/core/index.js +20 -0
  86. package/dist/core/index.js.map +1 -0
  87. package/dist/core/json.d.ts +18 -0
  88. package/dist/core/json.js +51 -0
  89. package/dist/core/json.js.map +1 -0
  90. package/dist/core/ops.d.ts +92 -0
  91. package/dist/core/ops.js +173 -0
  92. package/dist/core/ops.js.map +1 -0
  93. package/dist/core/person.d.ts +20 -0
  94. package/dist/core/person.js +50 -0
  95. package/dist/core/person.js.map +1 -0
  96. package/dist/core/query.d.ts +30 -0
  97. package/dist/core/query.js +106 -0
  98. package/dist/core/query.js.map +1 -0
  99. package/dist/core/refs.d.ts +12 -0
  100. package/dist/core/refs.js +36 -0
  101. package/dist/core/refs.js.map +1 -0
  102. package/dist/core/slug.d.ts +22 -0
  103. package/dist/core/slug.js +40 -0
  104. package/dist/core/slug.js.map +1 -0
  105. package/dist/core/time.d.ts +14 -0
  106. package/dist/core/time.js +37 -0
  107. package/dist/core/time.js.map +1 -0
  108. package/dist/core/tree.d.ts +94 -0
  109. package/dist/core/tree.js +237 -0
  110. package/dist/core/tree.js.map +1 -0
  111. package/dist/core/validate.d.ts +55 -0
  112. package/dist/core/validate.js +312 -0
  113. package/dist/core/validate.js.map +1 -0
  114. package/dist/git/config.d.ts +12 -0
  115. package/dist/git/config.js +27 -0
  116. package/dist/git/config.js.map +1 -0
  117. package/dist/git/exec.d.ts +35 -0
  118. package/dist/git/exec.js +58 -0
  119. package/dist/git/exec.js.map +1 -0
  120. package/dist/git/history.d.ts +35 -0
  121. package/dist/git/history.js +82 -0
  122. package/dist/git/history.js.map +1 -0
  123. package/dist/git/index-ops.d.ts +20 -0
  124. package/dist/git/index-ops.js +45 -0
  125. package/dist/git/index-ops.js.map +1 -0
  126. package/dist/git/merge.d.ts +27 -0
  127. package/dist/git/merge.js +57 -0
  128. package/dist/git/merge.js.map +1 -0
  129. package/dist/git/refscan.d.ts +32 -0
  130. package/dist/git/refscan.js +82 -0
  131. package/dist/git/refscan.js.map +1 -0
  132. package/dist/git/repo.d.ts +45 -0
  133. package/dist/git/repo.js +95 -0
  134. package/dist/git/repo.js.map +1 -0
  135. package/package.json +51 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Per-invocation context: where the repository is, what time it is, where IDs
3
+ * come from, and how output is styled.
4
+ *
5
+ * This is the one place the conformance hooks `NAV_NOW` and `NAV_IDS` are
6
+ * honored (see `doc/spec/fixtures/README.md`). Keeping them here means `core`
7
+ * has no notion of testing and every command is deterministic under fixtures.
8
+ */
9
+ import { webcrypto } from "node:crypto";
10
+ import { mintId as coreMintId, isId } from "../core/id.js";
11
+ import { toIsoSeconds } from "../core/time.js";
12
+ import { findRepo, userIdentity } from "../git/repo.js";
13
+ import { NavError } from "./errors.js";
14
+ import { makeColors } from "./render/colors.js";
15
+ export const NAV_NOW_ENV = "NAV_NOW";
16
+ export const NAV_IDS_ENV = "NAV_IDS";
17
+ const defaultRandomBytes = (n) => webcrypto.getRandomValues(new Uint8Array(n));
18
+ export function makeContext(opts = {}) {
19
+ const cwd = opts.cwd ?? process.cwd();
20
+ const env = opts.env ?? process.env;
21
+ const stdout = opts.stdout ?? process.stdout;
22
+ const stderr = opts.stderr ?? process.stderr;
23
+ let paths;
24
+ try {
25
+ paths = findRepo(cwd);
26
+ }
27
+ catch (error) {
28
+ if (opts.requireRepo === false) {
29
+ paths = { repoRoot: cwd, navRoot: "", hasNavbook: false };
30
+ }
31
+ else {
32
+ throw new NavError(error instanceof Error ? error.message : String(error));
33
+ }
34
+ }
35
+ const fixedNow = parseFixedNow(env[NAV_NOW_ENV]);
36
+ const scriptedIds = parseScriptedIds(env[NAV_IDS_ENV]);
37
+ let idCursor = 0;
38
+ return {
39
+ cwd,
40
+ env,
41
+ repoRoot: paths.repoRoot,
42
+ navRoot: paths.navRoot,
43
+ hasNavbook: paths.hasNavbook,
44
+ colors: makeColors(stdout, env),
45
+ stdout,
46
+ stderr,
47
+ now: () => (fixedNow ? new Date(fixedNow) : new Date()),
48
+ mintId(taken) {
49
+ if (scriptedIds) {
50
+ const next = scriptedIds[idCursor++];
51
+ if (next === undefined) {
52
+ throw new NavError(`${NAV_IDS_ENV} is exhausted after ${scriptedIds.length} id(s)`);
53
+ }
54
+ return next;
55
+ }
56
+ for (let attempt = 0; attempt < 100; attempt++) {
57
+ const id = coreMintId(defaultRandomBytes);
58
+ if (!taken?.has(id))
59
+ return id;
60
+ }
61
+ /* c8 ignore next */
62
+ throw new NavError("could not mint a collision-free id");
63
+ },
64
+ identity: () => userIdentity(paths.repoRoot),
65
+ };
66
+ }
67
+ function parseFixedNow(value) {
68
+ if (!value)
69
+ return null;
70
+ const date = new Date(value);
71
+ if (Number.isNaN(date.getTime())) {
72
+ throw new NavError(`${NAV_NOW_ENV} is not a valid timestamp: ${value}`);
73
+ }
74
+ return date;
75
+ }
76
+ function parseScriptedIds(value) {
77
+ if (value === undefined)
78
+ return null;
79
+ const ids = value
80
+ .split(",")
81
+ .map((id) => id.trim())
82
+ .filter((id) => id !== "");
83
+ for (const id of ids) {
84
+ if (!isId(id))
85
+ throw new NavError(`${NAV_IDS_ENV} contains an invalid id: ${id}`);
86
+ }
87
+ return ids;
88
+ }
89
+ /** The current instant as an ISO 8601 string with second precision. */
90
+ export function nowIso(ctx) {
91
+ return toIsoSeconds(ctx.now());
92
+ }
93
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/cli/context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `$EDITOR` integration for the `--edit` flows.
3
+ *
4
+ * The editor is always opened on the real Navbook file, exactly as spec 04 §4.3
5
+ * describes, so nothing has to be parsed back out of a scratch buffer.
6
+ */
7
+ import type { Ctx } from "./context.ts";
8
+ /** Single-quote a path for a POSIX shell, escaping any embedded quotes. */
9
+ export declare function shellQuote(value: string): string;
10
+ /** The editor command to use, or null when none can be determined. */
11
+ export declare function editorCommand(env: NodeJS.ProcessEnv): string | null;
12
+ /** Open `absolutePath` in the user's editor, failing if it exits non-zero. */
13
+ export declare function openInEditor(ctx: Ctx, absolutePath: string): void;
14
+ /**
15
+ * Edit a Navbook file before it exists.
16
+ *
17
+ * The buffer is a real `.md` file (so editors highlight it) held next to
18
+ * `COMMIT_EDITMSG` in the git directory, and it is prefilled with the exact
19
+ * frontmatter the file will carry — the author can adjust the title, labels and
20
+ * body in one pass. Nothing is created in `.navbook/` until the text is valid,
21
+ * so an abort leaves no partial entity behind.
22
+ */
23
+ export declare function editBuffer(ctx: Ctx, name: string, initial: string): string;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `$EDITOR` integration for the `--edit` flows.
3
+ *
4
+ * The editor is always opened on the real Navbook file, exactly as spec 04 §4.3
5
+ * describes, so nothing has to be parsed back out of a scratch buffer.
6
+ */
7
+ import { spawnSync } from "node:child_process";
8
+ import { readFileSync, rmSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { gitDir } from "../git/repo.js";
11
+ import { fail } from "./errors.js";
12
+ /** Single-quote a path for a POSIX shell, escaping any embedded quotes. */
13
+ export function shellQuote(value) {
14
+ return `'${value.replaceAll("'", "'\\''")}'`;
15
+ }
16
+ /** The editor command to use, or null when none can be determined. */
17
+ export function editorCommand(env) {
18
+ for (const key of ["VISUAL", "EDITOR"]) {
19
+ const value = env[key];
20
+ if (value && value.trim() !== "")
21
+ return value.trim();
22
+ }
23
+ return null;
24
+ }
25
+ /** Open `absolutePath` in the user's editor, failing if it exits non-zero. */
26
+ export function openInEditor(ctx, absolutePath) {
27
+ const editor = editorCommand(ctx.env);
28
+ if (!editor) {
29
+ fail("no editor configured", [
30
+ "set $VISUAL or $EDITOR, or pass -m/--message to supply the text directly",
31
+ ]);
32
+ }
33
+ // $EDITOR may carry its own flags ("code -w"), so it is interpreted by a
34
+ // shell; the path is quoted rather than passed as an argument, because a
35
+ // shell command string has no argv to bind positional parameters from.
36
+ const result = spawnSync(`${editor} ${shellQuote(absolutePath)}`, {
37
+ stdio: "inherit",
38
+ shell: true,
39
+ cwd: ctx.repoRoot,
40
+ env: ctx.env,
41
+ });
42
+ if (result.error)
43
+ fail(`could not start editor '${editor}': ${result.error.message}`);
44
+ if ((result.status ?? 1) !== 0)
45
+ fail(`editor '${editor}' exited with status ${result.status}`);
46
+ }
47
+ /**
48
+ * Edit a Navbook file before it exists.
49
+ *
50
+ * The buffer is a real `.md` file (so editors highlight it) held next to
51
+ * `COMMIT_EDITMSG` in the git directory, and it is prefilled with the exact
52
+ * frontmatter the file will carry — the author can adjust the title, labels and
53
+ * body in one pass. Nothing is created in `.navbook/` until the text is valid,
54
+ * so an abort leaves no partial entity behind.
55
+ */
56
+ export function editBuffer(ctx, name, initial) {
57
+ const path = join(gitDir(ctx.repoRoot), name);
58
+ writeFileSync(path, initial, "utf8");
59
+ try {
60
+ openInEditor(ctx, path);
61
+ return readFileSync(path, "utf8");
62
+ }
63
+ finally {
64
+ rmSync(path, { force: true });
65
+ }
66
+ }
67
+ //# sourceMappingURL=editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.js","sourceRoot":"","sources":["../../src/cli/editor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAExC,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,2EAA2E;AAC3E,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC;AAC/C,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,aAAa,CAAC,GAAsB;IAClD,KAAK,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;IACxD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,YAAY,CAAC,GAAQ,EAAE,YAAoB;IACzD,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,IAAI,CAAC,sBAAsB,EAAE;YAC3B,0EAA0E;SAC3E,CAAC,CAAC;IACL,CAAC;IACD,yEAAyE;IACzE,yEAAyE;IACzE,uEAAuE;IACvE,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,IAAI,UAAU,CAAC,YAAY,CAAC,EAAE,EAAE;QAChE,KAAK,EAAE,SAAS;QAChB,KAAK,EAAE,IAAI;QACX,GAAG,EAAE,GAAG,CAAC,QAAQ;QACjB,GAAG,EAAE,GAAG,CAAC,GAAG;KACb,CAAC,CAAC;IACH,IAAI,MAAM,CAAC,KAAK;QAAE,IAAI,CAAC,2BAA2B,MAAM,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IACtF,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,CAAC;QAAE,IAAI,CAAC,WAAW,MAAM,wBAAwB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;AACjG,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,GAAQ,EAAE,IAAY,EAAE,OAAe;IAChE,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC;IAC9C,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IACrC,IAAI,CAAC;QACH,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACxB,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAChC,CAAC;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Exit codes — spec 04 §4.2: 0 success, 1 operational error, 2 format violation.
3
+ */
4
+ export type ExitCode = 0 | 1 | 2;
5
+ export declare class NavError extends Error {
6
+ readonly exitCode: ExitCode;
7
+ /** Extra lines printed under the message, e.g. candidate IDs. */
8
+ readonly details: string[];
9
+ constructor(message: string, opts?: {
10
+ exitCode?: ExitCode;
11
+ details?: string[];
12
+ });
13
+ }
14
+ /** An operational error: not found, ambiguous, malformed input (exit 1). */
15
+ export declare function fail(message: string, details?: string[]): never;
16
+ /** A format violation detected by `doctor` (exit 2). */
17
+ export declare function failFormat(message: string, details?: string[]): never;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Exit codes — spec 04 §4.2: 0 success, 1 operational error, 2 format violation.
3
+ */
4
+ export class NavError extends Error {
5
+ exitCode;
6
+ /** Extra lines printed under the message, e.g. candidate IDs. */
7
+ details;
8
+ constructor(message, opts = {}) {
9
+ super(message);
10
+ this.name = "NavError";
11
+ this.exitCode = opts.exitCode ?? 1;
12
+ this.details = opts.details ?? [];
13
+ }
14
+ }
15
+ /** An operational error: not found, ambiguous, malformed input (exit 1). */
16
+ export function fail(message, details = []) {
17
+ throw new NavError(message, { exitCode: 1, details });
18
+ }
19
+ /** A format violation detected by `doctor` (exit 2). */
20
+ export function failFormat(message, details = []) {
21
+ throw new NavError(message, { exitCode: 2, details });
22
+ }
23
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/cli/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,QAAQ,CAAW;IAC5B,iEAAiE;IACxD,OAAO,CAAW;IAE3B,YAAY,OAAe,EAAE,OAAoD,EAAE;QACjF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;IACpC,CAAC;CACF;AAED,4EAA4E;AAC5E,MAAM,UAAU,IAAI,CAAC,OAAe,EAAE,UAAoB,EAAE;IAC1D,MAAM,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AACxD,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,UAAoB,EAAE;IAChE,MAAM,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The doctor checks that need git archaeology — spec 04 §4.3, D7, D9 and D10.
3
+ *
4
+ * Their decision logic lives in `core/validate.ts`; this module supplies the
5
+ * history those pure functions judge. Every check degrades to silence when the
6
+ * history is unavailable (a shallow clone, an unfetched commit), because an
7
+ * absent object is not evidence of a fault.
8
+ */
9
+ import { type Repo } from "../core/tree.ts";
10
+ import { type Diagnostic } from "../core/validate.ts";
11
+ import type { Ctx } from "./context.ts";
12
+ /**
13
+ * How far a frontmatter timestamp may sit from the commit that introduced it
14
+ * before D10 calls it inconsistent.
15
+ *
16
+ * The specification says "wildly inconsistent" without a number. Two days
17
+ * absorbs offline work, a delayed push and any timezone confusion, while still
18
+ * catching a timestamp that was copied or typed from the wrong year.
19
+ */
20
+ export declare const TIMESTAMP_SKEW_HOURS = 48;
21
+ export declare function runHistoryChecks(ctx: Ctx, repo: Repo): Diagnostic[];
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The doctor checks that need git archaeology — spec 04 §4.3, D7, D9 and D10.
3
+ *
4
+ * Their decision logic lives in `core/validate.ts`; this module supplies the
5
+ * history those pure functions judge. Every check degrades to silence when the
6
+ * history is unavailable (a shallow clone, an unfetched commit), because an
7
+ * absent object is not evidence of a fault.
8
+ */
9
+ import { parseFile, readRevisions } from "../core/files.js";
10
+ import { parseIso } from "../core/time.js";
11
+ import { allEntities } from "../core/tree.js";
12
+ import { checkRevisionsAppendOnly, checkTimestampSkew, planD9Fix, } from "../core/validate.js";
13
+ import { addedAt, blobAt, fileVersions, isAncestor, objectExists } from "../git/history.js";
14
+ import { currentBranch, resolveSha } from "../git/repo.js";
15
+ import { repoPath } from "./workspace.js";
16
+ /**
17
+ * How far a frontmatter timestamp may sit from the commit that introduced it
18
+ * before D10 calls it inconsistent.
19
+ *
20
+ * The specification says "wildly inconsistent" without a number. Two days
21
+ * absorbs offline work, a delayed push and any timezone confusion, while still
22
+ * catching a timestamp that was copied or typed from the wrong year.
23
+ */
24
+ export const TIMESTAMP_SKEW_HOURS = 48;
25
+ export function runHistoryChecks(ctx, repo) {
26
+ return [
27
+ ...checkAppendOnly(ctx, repo),
28
+ ...checkMergedButOpen(ctx, repo),
29
+ ...checkTimestamps(ctx, repo),
30
+ ];
31
+ }
32
+ /* ------------------------------------------- D7: revisions are append-only */
33
+ function checkAppendOnly(ctx, repo) {
34
+ const out = [];
35
+ for (const pr of repo.prs) {
36
+ const versions = fileVersions(ctx.repoRoot, repoPath(pr.filePath));
37
+ if (versions.length < 2)
38
+ continue;
39
+ const lists = [];
40
+ for (const version of versions) {
41
+ const text = blobAt(ctx.repoRoot, version.sha, version.path);
42
+ if (text === null)
43
+ continue;
44
+ try {
45
+ lists.push(readRevisions(parseFile(text).fm));
46
+ }
47
+ catch {
48
+ // A malformed historical version is D2's business, not D7's.
49
+ }
50
+ }
51
+ const result = checkRevisionsAppendOnly(lists);
52
+ if (result.ok)
53
+ continue;
54
+ out.push({
55
+ check: "D7",
56
+ level: "error",
57
+ path: pr.filePath,
58
+ message: `revisions must only be appended to (§2.7): ${result.message}`,
59
+ });
60
+ }
61
+ return out;
62
+ }
63
+ /* ------------------------------------ D9: merged but not archived (03 §3.5) */
64
+ function checkMergedButOpen(ctx, repo) {
65
+ const head = resolveSha(ctx.repoRoot, "HEAD");
66
+ if (!head)
67
+ return [];
68
+ // "Merged" only means anything on the branch the PR asked to merge into. On
69
+ // its own source branch the head is trivially an ancestor of HEAD, and on an
70
+ // unrelated branch containing it the fact is not actionable.
71
+ const branch = currentBranch(ctx.repoRoot);
72
+ if (!branch)
73
+ return [];
74
+ const out = [];
75
+ for (const pr of repo.prs) {
76
+ if (pr.status !== "open")
77
+ continue;
78
+ if (pr.fm.target !== branch)
79
+ continue;
80
+ const revisions = readRevisions(pr.fm);
81
+ const latest = revisions[revisions.length - 1];
82
+ // An unfetched head is not evidence of anything.
83
+ if (!latest || !objectExists(ctx.repoRoot, latest.head))
84
+ continue;
85
+ if (!isAncestor(ctx.repoRoot, latest.head, head))
86
+ continue;
87
+ out.push({
88
+ check: "D9",
89
+ level: "warning",
90
+ path: pr.dirPath,
91
+ message: `#${pr.id} is merged into this branch but still filed under prs/open/; move it to prs/merged/`,
92
+ fix: planD9Fix(pr),
93
+ });
94
+ }
95
+ return out;
96
+ }
97
+ /* --------------------------------- D10: timestamps versus the commit record */
98
+ function checkTimestamps(ctx, repo) {
99
+ const out = [];
100
+ for (const entity of allEntities(repo)) {
101
+ // An entity whose timestamps legitimately predate the commit that carries
102
+ // them: bulk-imported from a forge (§2.4), or archived from an era the
103
+ // visible history may no longer reach (03 §3.6).
104
+ if (entity.archived || entity.fm["imported-from"] !== undefined)
105
+ continue;
106
+ const created = typeof entity.fm.created === "string" ? parseIso(entity.fm.created) : null;
107
+ if (created) {
108
+ const added = addedAt(ctx.repoRoot, repoPath(entity.filePath));
109
+ if (added && !checkTimestampSkew(created, added.authored, TIMESTAMP_SKEW_HOURS)) {
110
+ out.push({
111
+ check: "D10",
112
+ level: "warning",
113
+ path: entity.filePath,
114
+ message: `'created: ${entity.fm.created}' is more than ${TIMESTAMP_SKEW_HOURS}h from the commit that added it (${added.authored.toISOString().slice(0, 10)})`,
115
+ });
116
+ }
117
+ }
118
+ for (const comment of entity.comments) {
119
+ const added = addedAt(ctx.repoRoot, repoPath(comment.path));
120
+ if (!added)
121
+ continue;
122
+ if (checkTimestampSkew(comment.date, added.authored, TIMESTAMP_SKEW_HOURS))
123
+ continue;
124
+ out.push({
125
+ check: "D10",
126
+ level: "warning",
127
+ path: comment.path,
128
+ message: `the filename timestamp is more than ${TIMESTAMP_SKEW_HOURS}h from the commit that added it (${added.authored.toISOString().slice(0, 10)})`,
129
+ });
130
+ }
131
+ }
132
+ return out;
133
+ }
134
+ //# sourceMappingURL=history-checks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history-checks.js","sourceRoot":"","sources":["../../src/cli/history-checks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shell completions.
3
+ *
4
+ * The scripts stay deliberately thin: anything that needs to know about the
5
+ * repository calls back into `nav __complete`, so completion behavior lives in
6
+ * one implementation rather than three shell dialects.
7
+ */
8
+ export declare const SHELLS: readonly ["bash", "zsh", "fish"];
9
+ export type Shell = (typeof SHELLS)[number];
10
+ export declare function isShell(value: string): value is Shell;
11
+ /** Guess the user's shell from `$SHELL`, defaulting to bash. */
12
+ export declare function detectShell(env: NodeJS.ProcessEnv): Shell;
13
+ /** Where a shell looks for per-user completion scripts. */
14
+ export declare function completionPath(shell: Shell, env: NodeJS.ProcessEnv): string;
15
+ /** Extra advice printed after installing, where a shell needs configuration. */
16
+ export declare function completionNote(shell: Shell, path: string): string | null;
17
+ export declare function completionScript(shell: Shell): string;
18
+ export declare function installCompletions(shell: Shell, env: NodeJS.ProcessEnv): string;
19
+ export declare function uninstallCompletions(shell: Shell, env: NodeJS.ProcessEnv): string | null;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Shell completions.
3
+ *
4
+ * The scripts stay deliberately thin: anything that needs to know about the
5
+ * repository calls back into `nav __complete`, so completion behavior lives in
6
+ * one implementation rather than three shell dialects.
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { basename, dirname, join } from "node:path";
11
+ export const SHELLS = ["bash", "zsh", "fish"];
12
+ export function isShell(value) {
13
+ return SHELLS.includes(value);
14
+ }
15
+ /** Guess the user's shell from `$SHELL`, defaulting to bash. */
16
+ export function detectShell(env) {
17
+ const name = basename(env.SHELL ?? "");
18
+ return isShell(name) ? name : "bash";
19
+ }
20
+ /** Where a shell looks for per-user completion scripts. */
21
+ export function completionPath(shell, env) {
22
+ const home = env.HOME ?? homedir();
23
+ const dataHome = env.XDG_DATA_HOME ?? join(home, ".local", "share");
24
+ const configHome = env.XDG_CONFIG_HOME ?? join(home, ".config");
25
+ switch (shell) {
26
+ case "bash":
27
+ return join(dataHome, "bash-completion", "completions", "nav");
28
+ case "zsh":
29
+ return join(dataHome, "zsh", "site-functions", "_nav");
30
+ default:
31
+ return join(configHome, "fish", "completions", "nav.fish");
32
+ }
33
+ }
34
+ /** Extra advice printed after installing, where a shell needs configuration. */
35
+ export function completionNote(shell, path) {
36
+ if (shell !== "zsh")
37
+ return null;
38
+ return `zsh needs ${dirname(path)} on its fpath; add this to ~/.zshrc if it is not:\n fpath=(${dirname(path)} $fpath)`;
39
+ }
40
+ export function completionScript(shell) {
41
+ switch (shell) {
42
+ case "bash":
43
+ return BASH;
44
+ case "zsh":
45
+ return ZSH;
46
+ default:
47
+ return FISH;
48
+ }
49
+ }
50
+ export function installCompletions(shell, env) {
51
+ const path = completionPath(shell, env);
52
+ mkdirSync(dirname(path), { recursive: true });
53
+ writeFileSync(path, completionScript(shell), "utf8");
54
+ return path;
55
+ }
56
+ export function uninstallCompletions(shell, env) {
57
+ const path = completionPath(shell, env);
58
+ if (!existsSync(path))
59
+ return null;
60
+ // Only remove a file this tool wrote.
61
+ if (!readFileSync(path, "utf8").includes("nav __complete"))
62
+ return null;
63
+ rmSync(path, { force: true });
64
+ return path;
65
+ }
66
+ const BASH = `# bash completion for nav (navbook). Generated by 'nav install --completions'.
67
+ _nav_complete() {
68
+ local cur prev
69
+ cur="\${COMP_WORDS[COMP_CWORD]}"
70
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
71
+ local reply
72
+ reply="$(nav __complete "\${COMP_WORDS[@]:1:COMP_CWORD-1}" 2>/dev/null)"
73
+ COMPREPLY=($(compgen -W "\${reply}" -- "\${cur}"))
74
+ return 0
75
+ }
76
+ complete -F _nav_complete nav
77
+ `;
78
+ const ZSH = `#compdef nav
79
+ # zsh completion for nav (navbook). Generated by 'nav install --completions'.
80
+ _nav() {
81
+ local -a candidates
82
+ candidates=(\${(f)"$(nav __complete \${words[2,CURRENT-1]} 2>/dev/null)"})
83
+ _describe 'nav' candidates
84
+ }
85
+ compdef _nav nav
86
+ `;
87
+ const FISH = `# fish completion for nav (navbook). Generated by 'nav install --completions'.
88
+ function __nav_complete
89
+ set -l tokens (commandline -opc)
90
+ nav __complete $tokens[2..-1] 2>/dev/null
91
+ end
92
+ complete -c nav -f -a '(__nav_complete)'
93
+ `;
94
+ //# sourceMappingURL=completions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completions.js","sourceRoot":"","sources":["../../../src/cli/install/completions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The `pre-commit` hook — spec 04 §4.5.
3
+ *
4
+ * The hook is a thin shell script that calls the binary, so `--no-verify` and
5
+ * manual hook removal behave exactly as users expect. It is written as a marked
6
+ * block appended to any existing hook, and `nav uninstall --hooks` removes
7
+ * precisely that block and nothing else.
8
+ */
9
+ export declare const MARKER_BEGIN = "# >>> navbook >>>";
10
+ export declare const MARKER_END = "# <<< navbook <<<";
11
+ export declare const HOOK_NAME = "pre-commit";
12
+ export declare function hookPath(repoRoot: string): string;
13
+ export declare function isInstalled(repoRoot: string): boolean;
14
+ /** Append the marked block, creating the hook file if there is none. */
15
+ export declare function installHook(repoRoot: string): void;
16
+ /**
17
+ * Remove exactly the marked block. A hook that contained nothing else is
18
+ * deleted; a hook the user also wrote by hand keeps everything they wrote.
19
+ */
20
+ export declare function uninstallHook(repoRoot: string): void;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The `pre-commit` hook — spec 04 §4.5.
3
+ *
4
+ * The hook is a thin shell script that calls the binary, so `--no-verify` and
5
+ * manual hook removal behave exactly as users expect. It is written as a marked
6
+ * block appended to any existing hook, and `nav uninstall --hooks` removes
7
+ * precisely that block and nothing else.
8
+ */
9
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { hooksDir } from "../../git/repo.js";
12
+ export const MARKER_BEGIN = "# >>> navbook >>>";
13
+ export const MARKER_END = "# <<< navbook <<<";
14
+ export const HOOK_NAME = "pre-commit";
15
+ const SHEBANG = "#!/bin/sh";
16
+ const BODY = `${MARKER_BEGIN}
17
+ # Validate staged Navbook files. Only a format violation (exit 2) blocks the
18
+ # commit; warnings never do, and a clone without nav installed is unaffected.
19
+ # Commit with --no-verify, or delete this block, to skip the check.
20
+ if command -v nav >/dev/null 2>&1; then
21
+ nav doctor --staged
22
+ if [ $? -eq 2 ]; then
23
+ exit 1
24
+ fi
25
+ fi
26
+ ${MARKER_END}`;
27
+ export function hookPath(repoRoot) {
28
+ return join(hooksDir(repoRoot), HOOK_NAME);
29
+ }
30
+ export function isInstalled(repoRoot) {
31
+ const path = hookPath(repoRoot);
32
+ return existsSync(path) && readFileSync(path, "utf8").includes(MARKER_BEGIN);
33
+ }
34
+ /** Append the marked block, creating the hook file if there is none. */
35
+ export function installHook(repoRoot) {
36
+ const path = hookPath(repoRoot);
37
+ mkdirSync(dirname(path), { recursive: true });
38
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
39
+ if (existing.includes(MARKER_BEGIN))
40
+ return;
41
+ const content = existing.trim() === ""
42
+ ? `${SHEBANG}\n${BODY}\n`
43
+ : `${existing.replace(/\n*$/, "\n")}\n${BODY}\n`;
44
+ writeFileSync(path, content, "utf8");
45
+ chmodSync(path, 0o755);
46
+ }
47
+ /**
48
+ * Remove exactly the marked block. A hook that contained nothing else is
49
+ * deleted; a hook the user also wrote by hand keeps everything they wrote.
50
+ */
51
+ export function uninstallHook(repoRoot) {
52
+ const path = hookPath(repoRoot);
53
+ if (!existsSync(path))
54
+ return;
55
+ const existing = readFileSync(path, "utf8");
56
+ if (!existing.includes(MARKER_BEGIN))
57
+ return;
58
+ const stripped = existing
59
+ .split("\n")
60
+ .reduce((state, line) => {
61
+ if (line.trim() === MARKER_BEGIN)
62
+ return { lines: state.lines, inside: true };
63
+ if (line.trim() === MARKER_END)
64
+ return { lines: state.lines, inside: false };
65
+ if (!state.inside)
66
+ state.lines.push(line);
67
+ return state;
68
+ }, { lines: [], inside: false })
69
+ .lines.join("\n");
70
+ const remainder = stripped.replace(SHEBANG, "").trim();
71
+ if (remainder === "") {
72
+ rmSync(path, { force: true });
73
+ return;
74
+ }
75
+ writeFileSync(path, `${stripped.replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "\n")}`, "utf8");
76
+ chmodSync(path, statSync(path).mode | 0o111);
77
+ }
78
+ //# sourceMappingURL=hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook.js","sourceRoot":"","sources":["../../../src/cli/install/hook.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `nav` entry point.
4
+ */
5
+ import { type ExitCode } from "./errors.ts";
6
+ export interface RunOptions {
7
+ argv?: string[];
8
+ cwd?: string;
9
+ env?: NodeJS.ProcessEnv;
10
+ stdout?: NodeJS.WriteStream;
11
+ stderr?: NodeJS.WriteStream;
12
+ }
13
+ /** Run the CLI and return its exit code. Never throws for expected failures. */
14
+ export declare function run(opts?: RunOptions): ExitCode;