@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,58 @@
1
+ /**
2
+ * Column-aligned listings for `nav issue list` / `nav pr list`.
3
+ */
4
+ const GAP = " ";
5
+ /** Render a table, shrinking flexible columns to fit the terminal width. */
6
+ export function renderTable(columns, rows, opts) {
7
+ if (rows.length === 0)
8
+ return "";
9
+ const widths = columns.map((column, index) => Math.max(column.header.length, ...rows.map((row) => displayWidth(row[index] ?? ""))));
10
+ const available = opts.width;
11
+ if (available !== undefined)
12
+ shrinkToFit(columns, widths, available);
13
+ const lines = [];
14
+ lines.push(opts.colors.dim(joinCells(columns.map((column, index) => pad(column.header.toUpperCase(), widths[index])))));
15
+ for (const row of rows) {
16
+ lines.push(joinCells(columns.map((_, index) => pad(truncate(row[index] ?? "", widths[index]), widths[index]))));
17
+ }
18
+ return lines.join("\n");
19
+ }
20
+ function shrinkToFit(columns, widths, available) {
21
+ const gaps = GAP.length * (columns.length - 1);
22
+ const flexible = columns
23
+ .map((column, index) => ({ column, index }))
24
+ .filter(({ column }) => column.flexible);
25
+ if (flexible.length === 0)
26
+ return;
27
+ let total = widths.reduce((sum, w) => sum + w, 0) + gaps;
28
+ for (const { column, index } of flexible) {
29
+ if (total <= available)
30
+ break;
31
+ const minWidth = column.minWidth ?? 8;
32
+ const current = widths[index];
33
+ const reduced = Math.max(minWidth, current - (total - available));
34
+ total -= current - reduced;
35
+ widths[index] = reduced;
36
+ }
37
+ }
38
+ function joinCells(cells) {
39
+ return cells.join(GAP).replace(/\s+$/, "");
40
+ }
41
+ function pad(text, width) {
42
+ const padding = width - displayWidth(text);
43
+ return padding > 0 ? text + " ".repeat(padding) : text;
44
+ }
45
+ /** Truncate to `width`, marking the cut with a single ellipsis character. */
46
+ export function truncate(text, width) {
47
+ if (width <= 0)
48
+ return "";
49
+ if (displayWidth(text) <= width)
50
+ return text;
51
+ if (width === 1)
52
+ return "~";
53
+ return `${text.slice(0, width - 1)}~`;
54
+ }
55
+ function displayWidth(text) {
56
+ return [...text].length;
57
+ }
58
+ //# sourceMappingURL=table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table.js","sourceRoot":"","sources":["../../../src/cli/render/table.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Turning what the user typed into the entity they meant — spec 02 §2.2
3
+ * (prefixes of at least four characters) and spec 04 §4.3 (a verb given an ID
4
+ * of the other kind points at the right noun).
5
+ */
6
+ import { type EntityKind, type EntityRecord, type Repo } from "../core/tree.ts";
7
+ /**
8
+ * Accept a whole entity directory name wherever an ID is expected.
9
+ *
10
+ * Shell completion offers `<id>-<slug>` names, and they are what a user copies
11
+ * out of a path or a forge URL; the ID is simply its first eight characters.
12
+ */
13
+ export declare function asId(argument: string): string;
14
+ /** Resolve an ID or unambiguous prefix to an entity of the expected kind. */
15
+ export declare function resolveEntity(repo: Repo, prefix: string, kind: EntityKind): EntityRecord;
16
+ /** Resolve a comment ID or prefix within one entity (used by `--reply-to`). */
17
+ export declare function resolveComment(entity: EntityRecord, prefix: string): string;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Turning what the user typed into the entity they meant — spec 02 §2.2
3
+ * (prefixes of at least four characters) and spec 04 §4.3 (a verb given an ID
4
+ * of the other kind points at the right noun).
5
+ */
6
+ import { MIN_PREFIX_LENGTH, resolvePrefix } from "../core/id.js";
7
+ import { allEntities } from "../core/tree.js";
8
+ import { fail } from "./errors.js";
9
+ const NOUN = { issue: "issue", pr: "pull request" };
10
+ const COMMAND = { issue: "nav issue", pr: "nav pr" };
11
+ /**
12
+ * Accept a whole entity directory name wherever an ID is expected.
13
+ *
14
+ * Shell completion offers `<id>-<slug>` names, and they are what a user copies
15
+ * out of a path or a forge URL; the ID is simply its first eight characters.
16
+ */
17
+ export function asId(argument) {
18
+ const match = /^([a-z][a-z0-9]{7})-[a-z0-9-]+$/.exec(argument.toLowerCase());
19
+ return match ? match[1] : argument;
20
+ }
21
+ /** Resolve an ID or unambiguous prefix to an entity of the expected kind. */
22
+ export function resolveEntity(repo, prefix, kind) {
23
+ const entities = allEntities(repo);
24
+ const resolution = resolvePrefix(asId(prefix), entities.map((e) => e.id));
25
+ if (!resolution.ok) {
26
+ switch (resolution.reason) {
27
+ case "too-short":
28
+ fail(`'${prefix}' is too short; ID prefixes must be at least ${MIN_PREFIX_LENGTH} characters`);
29
+ break;
30
+ case "not-found":
31
+ fail(`no ${NOUN[kind]} matches '${prefix}'`);
32
+ break;
33
+ default:
34
+ fail(`'${prefix}' is ambiguous; ${resolution.matches.length} entities match`, resolution.matches.map((id) => describe(entities.find((e) => e.id === id))));
35
+ }
36
+ }
37
+ const matches = entities.filter((e) => e.id === resolution.id);
38
+ const wanted = matches.find((e) => e.kind === kind);
39
+ if (wanted)
40
+ return wanted;
41
+ const other = matches[0];
42
+ fail(`#${other.id} is ${other.kind === "pr" ? "a pull request" : "an issue"} — use '${COMMAND[other.kind]} ${verbHint()}'`);
43
+ }
44
+ function verbHint() {
45
+ return "show";
46
+ }
47
+ function describe(entity) {
48
+ if (!entity)
49
+ return "";
50
+ return `#${entity.id} ${entity.kind === "pr" ? "pr " : "issue"} ${entity.title}`;
51
+ }
52
+ /** Resolve a comment ID or prefix within one entity (used by `--reply-to`). */
53
+ export function resolveComment(entity, prefix) {
54
+ const resolution = resolvePrefix(prefix, entity.comments.map((c) => c.id));
55
+ if (resolution.ok)
56
+ return resolution.id;
57
+ switch (resolution.reason) {
58
+ case "too-short":
59
+ fail(`'${prefix}' is too short; ID prefixes must be at least ${MIN_PREFIX_LENGTH} characters`);
60
+ break;
61
+ case "not-found":
62
+ fail(`no comment of #${entity.id} matches '${prefix}'`);
63
+ break;
64
+ default:
65
+ fail(`'${prefix}' is ambiguous within #${entity.id}`, resolution.matches.map((id) => `#${id}`));
66
+ }
67
+ }
68
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/cli/resolve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Reading and mutating the `.navbook/` working tree.
3
+ *
4
+ * Core plans; this module executes. Every write goes through {@link applyOps}
5
+ * so the set of touched paths is known exactly — which is what makes the
6
+ * `--commit` guard of spec 04 §4.2 precise rather than approximate.
7
+ */
8
+ import type { FileOp } from "../core/ops.ts";
9
+ import { type Query } from "../core/query.ts";
10
+ import { type NavTree, type Repo } from "../core/tree.ts";
11
+ import type { Ctx } from "./context.ts";
12
+ export interface ReadTreeOptions {
13
+ /** Skip `comments/` directories when the command cannot need them. */
14
+ includeComments?: boolean;
15
+ }
16
+ /** Read `.navbook/` into a flat path→content map. */
17
+ export declare function readNavTree(navRoot: string, opts?: ReadTreeOptions): NavTree;
18
+ /** Load the repository model from the working tree. */
19
+ export declare function loadRepo(ctx: Ctx, opts?: ReadTreeOptions): Repo;
20
+ /** Load the repository, reading comment bodies only when the query needs them. */
21
+ export declare function loadRepoForQuery(ctx: Ctx, query: Query): Repo;
22
+ /** Fail with a helpful message when the repository has no `.navbook/` yet. */
23
+ export declare function requireNavbook(ctx: Ctx): void;
24
+ /** Convert a path relative to `.navbook/` into a repository-relative path. */
25
+ export declare function repoPath(navRelative: string): string;
26
+ /** Absolute filesystem path for a path relative to `.navbook/`. */
27
+ export declare function absPath(ctx: Ctx, navRelative: string): string;
28
+ export interface ApplyResult {
29
+ /** Repository-relative paths the operation created, rewrote or moved. */
30
+ touched: string[];
31
+ }
32
+ /**
33
+ * Execute file operations in order, then stage exactly the paths involved.
34
+ *
35
+ * Renames are performed as a filesystem move followed by `git add`, which
36
+ * produces the same index state as `git mv` while also working on entities that
37
+ * have not been committed yet.
38
+ */
39
+ export declare function applyOps(ctx: Ctx, ops: readonly FileOp[]): ApplyResult;
40
+ /** Stage paths, tolerating those that no longer exist and were never tracked. */
41
+ export declare function stage(ctx: Ctx, paths: readonly string[]): void;
42
+ /**
43
+ * Every ID in the repository, read from names alone.
44
+ *
45
+ * Entity IDs are in directory names and comment IDs are in filenames (§2.2,
46
+ * §2.6), so uniqueness can be checked without opening a single file — which
47
+ * keeps `open` fast while still honouring "unique across all entity and
48
+ * comment IDs".
49
+ */
50
+ export declare function scanAllIds(navRoot: string): Set<string>;
51
+ /** Repository-relative path of an absolute path, in POSIX form. */
52
+ export declare function toRepoRelative(ctx: Ctx, absolute: string): string;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Reading and mutating the `.navbook/` working tree.
3
+ *
4
+ * Core plans; this module executes. Every write goes through {@link applyOps}
5
+ * so the set of touched paths is known exactly — which is what makes the
6
+ * `--commit` guard of spec 04 §4.2 precise rather than approximate.
7
+ */
8
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, writeFileSync, } from "node:fs";
9
+ import { dirname, join, posix, relative, sep } from "node:path";
10
+ import { parseCommentFileName } from "../core/comments.js";
11
+ import { NAVBOOK_ROOT } from "../core/json.js";
12
+ import { needsComments } from "../core/query.js";
13
+ import { parseDirName } from "../core/slug.js";
14
+ import { parseTree } from "../core/tree.js";
15
+ import { gitMaybe } from "../git/exec.js";
16
+ import { add } from "../git/index-ops.js";
17
+ import { fail } from "./errors.js";
18
+ /** Read `.navbook/` into a flat path→content map. */
19
+ export function readNavTree(navRoot, opts = {}) {
20
+ const includeComments = opts.includeComments !== false;
21
+ const files = new Map();
22
+ if (!existsSync(navRoot))
23
+ return files;
24
+ walk(navRoot, "", files, includeComments);
25
+ return files;
26
+ }
27
+ function walk(absolute, rel, files, includeComments) {
28
+ let entries;
29
+ try {
30
+ entries = readdirSync(absolute, { withFileTypes: true, encoding: "utf8" });
31
+ }
32
+ catch {
33
+ return;
34
+ }
35
+ for (const entry of entries) {
36
+ const childRel = rel === "" ? entry.name : `${rel}/${entry.name}`;
37
+ const childAbs = join(absolute, entry.name);
38
+ if (entry.isDirectory()) {
39
+ if (!includeComments && entry.name === "comments")
40
+ continue;
41
+ walk(childAbs, childRel, files, includeComments);
42
+ }
43
+ else if (entry.isFile()) {
44
+ files.set(childRel, readFileSync(childAbs, "utf8"));
45
+ }
46
+ }
47
+ }
48
+ /** Load the repository model from the working tree. */
49
+ export function loadRepo(ctx, opts = {}) {
50
+ requireNavbook(ctx);
51
+ const includeComments = opts.includeComments !== false;
52
+ const files = readNavTree(ctx.navRoot, { includeComments });
53
+ return parseTree(files, { commentsLoaded: includeComments });
54
+ }
55
+ /** Load the repository, reading comment bodies only when the query needs them. */
56
+ export function loadRepoForQuery(ctx, query) {
57
+ return loadRepo(ctx, { includeComments: needsComments(query) });
58
+ }
59
+ /** Fail with a helpful message when the repository has no `.navbook/` yet. */
60
+ export function requireNavbook(ctx) {
61
+ if (!ctx.hasNavbook) {
62
+ fail(`not a Navbook repository: no ${NAVBOOK_ROOT}/ at the repository root`, [
63
+ "run 'nav init' to create it",
64
+ ]);
65
+ }
66
+ }
67
+ /** Convert a path relative to `.navbook/` into a repository-relative path. */
68
+ export function repoPath(navRelative) {
69
+ return posix.join(NAVBOOK_ROOT, navRelative);
70
+ }
71
+ /** Absolute filesystem path for a path relative to `.navbook/`. */
72
+ export function absPath(ctx, navRelative) {
73
+ return join(ctx.navRoot, ...navRelative.split("/"));
74
+ }
75
+ /**
76
+ * Execute file operations in order, then stage exactly the paths involved.
77
+ *
78
+ * Renames are performed as a filesystem move followed by `git add`, which
79
+ * produces the same index state as `git mv` while also working on entities that
80
+ * have not been committed yet.
81
+ */
82
+ export function applyOps(ctx, ops) {
83
+ const touched = new Set();
84
+ for (const op of ops) {
85
+ if (op.op === "write") {
86
+ const target = absPath(ctx, op.path);
87
+ mkdirSync(dirname(target), { recursive: true });
88
+ writeFileSync(target, op.content, "utf8");
89
+ touched.add(repoPath(op.path));
90
+ continue;
91
+ }
92
+ const from = absPath(ctx, op.from);
93
+ const to = absPath(ctx, op.to);
94
+ if (!existsSync(from))
95
+ fail(`cannot move ${repoPath(op.from)}: it does not exist`);
96
+ if (existsSync(to))
97
+ fail(`cannot move ${repoPath(op.from)}: ${repoPath(op.to)} already exists`);
98
+ mkdirSync(dirname(to), { recursive: true });
99
+ renameSync(from, to);
100
+ pruneEmptyParents(ctx, dirname(from));
101
+ touched.add(repoPath(op.from));
102
+ touched.add(repoPath(op.to));
103
+ }
104
+ stage(ctx, [...touched]);
105
+ return { touched: [...touched].sort() };
106
+ }
107
+ /**
108
+ * Remove directories left empty by a move, up to (but never including) the
109
+ * `.navbook/` root. Git does not track empty directories, so leaving them
110
+ * behind would make the working tree disagree with a fresh clone.
111
+ */
112
+ function pruneEmptyParents(ctx, startDir) {
113
+ let current = startDir;
114
+ while (current.startsWith(ctx.navRoot) && current !== ctx.navRoot) {
115
+ try {
116
+ if (readdirSync(current).length > 0)
117
+ return;
118
+ rmdirSync(current);
119
+ }
120
+ catch {
121
+ return;
122
+ }
123
+ current = dirname(current);
124
+ }
125
+ }
126
+ /** Stage paths, tolerating those that no longer exist and were never tracked. */
127
+ export function stage(ctx, paths) {
128
+ const stageable = paths.filter((path) => {
129
+ if (existsSync(join(ctx.repoRoot, ...path.split("/"))))
130
+ return true;
131
+ const tracked = gitMaybe(["ls-files", "--", path], { cwd: ctx.repoRoot });
132
+ return tracked !== null && tracked !== "";
133
+ });
134
+ add(ctx.repoRoot, stageable);
135
+ }
136
+ /**
137
+ * Every ID in the repository, read from names alone.
138
+ *
139
+ * Entity IDs are in directory names and comment IDs are in filenames (§2.2,
140
+ * §2.6), so uniqueness can be checked without opening a single file — which
141
+ * keeps `open` fast while still honouring "unique across all entity and
142
+ * comment IDs".
143
+ */
144
+ export function scanAllIds(navRoot) {
145
+ const ids = new Set();
146
+ if (!existsSync(navRoot))
147
+ return ids;
148
+ const walk = (dir, depth) => {
149
+ let entries;
150
+ try {
151
+ entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" });
152
+ }
153
+ catch {
154
+ return;
155
+ }
156
+ for (const entry of entries) {
157
+ if (entry.isDirectory()) {
158
+ const parsed = parseDirName(entry.name);
159
+ if (parsed)
160
+ ids.add(parsed.id);
161
+ if (depth < 6)
162
+ walk(join(dir, entry.name), depth + 1);
163
+ }
164
+ else if (entry.isFile()) {
165
+ const comment = parseCommentFileName(entry.name);
166
+ if (comment)
167
+ ids.add(comment.id);
168
+ }
169
+ }
170
+ };
171
+ walk(navRoot, 0);
172
+ return ids;
173
+ }
174
+ /** Repository-relative path of an absolute path, in POSIX form. */
175
+ export function toRepoRelative(ctx, absolute) {
176
+ return relative(ctx.repoRoot, absolute).split(sep).join("/");
177
+ }
178
+ //# sourceMappingURL=workspace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../src/cli/workspace.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Comment files — spec 02 §2.6. One file per comment, named with a colon-free
3
+ * UTC timestamp plus the comment's own ID, so listings sort chronologically and
4
+ * concurrent comments can never collide.
5
+ */
6
+ export declare const COMMENT_NAME_PATTERN: RegExp;
7
+ export interface CommentName {
8
+ stamp: string;
9
+ date: Date;
10
+ id: string;
11
+ }
12
+ /** Build the filename for a comment created at `date` with identifier `id`. */
13
+ export declare function commentFileName(date: Date, id: string): string;
14
+ /** Parse a comment filename, or return null when it violates the grammar. */
15
+ export declare function parseCommentFileName(name: string): CommentName | null;
16
+ export interface ThreadItem<T> {
17
+ comment: T;
18
+ depth: number;
19
+ }
20
+ /**
21
+ * Order comments for display: chronologically by filename, with replies placed
22
+ * directly under the comment they answer and indented by one level.
23
+ *
24
+ * Replies to unknown comments, and reply cycles, degrade to top level rather
25
+ * than disappearing.
26
+ */
27
+ export declare function threadOrder<T extends {
28
+ id: string;
29
+ replyTo?: string;
30
+ fileName: string;
31
+ }>(comments: readonly T[]): ThreadItem<T>[];
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Comment files — spec 02 §2.6. One file per comment, named with a colon-free
3
+ * UTC timestamp plus the comment's own ID, so listings sort chronologically and
4
+ * concurrent comments can never collide.
5
+ */
6
+ import { isId } from "./id.js";
7
+ import { fromCompactStamp, toCompactStamp } from "./time.js";
8
+ export const COMMENT_NAME_PATTERN = /^(\d{4}-\d{2}-\d{2}T\d{6}Z)-([a-z][a-z0-9]{7})\.md$/;
9
+ /** Build the filename for a comment created at `date` with identifier `id`. */
10
+ export function commentFileName(date, id) {
11
+ return `${toCompactStamp(date)}-${id}.md`;
12
+ }
13
+ /** Parse a comment filename, or return null when it violates the grammar. */
14
+ export function parseCommentFileName(name) {
15
+ const match = COMMENT_NAME_PATTERN.exec(name);
16
+ if (!match)
17
+ return null;
18
+ const stamp = match[1];
19
+ const id = match[2];
20
+ if (!isId(id))
21
+ return null;
22
+ const date = fromCompactStamp(stamp);
23
+ if (!date)
24
+ return null;
25
+ return { stamp, date, id };
26
+ }
27
+ /**
28
+ * Order comments for display: chronologically by filename, with replies placed
29
+ * directly under the comment they answer and indented by one level.
30
+ *
31
+ * Replies to unknown comments, and reply cycles, degrade to top level rather
32
+ * than disappearing.
33
+ */
34
+ export function threadOrder(comments) {
35
+ const sorted = [...comments].sort((a, b) => a.fileName < b.fileName ? -1 : a.fileName > b.fileName ? 1 : 0);
36
+ const byId = new Map(sorted.map((c) => [c.id, c]));
37
+ const children = new Map();
38
+ const roots = [];
39
+ for (const comment of sorted) {
40
+ const parent = comment.replyTo ? byId.get(comment.replyTo) : undefined;
41
+ if (!parent || parent.id === comment.id || createsCycle(comment, byId)) {
42
+ roots.push(comment);
43
+ continue;
44
+ }
45
+ const bucket = children.get(parent.id);
46
+ if (bucket)
47
+ bucket.push(comment);
48
+ else
49
+ children.set(parent.id, [comment]);
50
+ }
51
+ const out = [];
52
+ // Guard on identity, not id: duplicate ids are a doctor error (D3), and
53
+ // until they are fixed the thread must still show each file exactly once.
54
+ const emitted = new Set();
55
+ const walk = (comment, depth) => {
56
+ if (emitted.has(comment))
57
+ return;
58
+ emitted.add(comment);
59
+ out.push({ comment, depth });
60
+ for (const child of children.get(comment.id) ?? [])
61
+ walk(child, depth + 1);
62
+ };
63
+ for (const root of roots)
64
+ walk(root, 0);
65
+ for (const comment of sorted)
66
+ walk(comment, 0);
67
+ return out;
68
+ }
69
+ function createsCycle(start, byId) {
70
+ const seen = new Set([start.id]);
71
+ let current = start.replyTo ? byId.get(start.replyTo) : undefined;
72
+ while (current) {
73
+ if (seen.has(current.id))
74
+ return true;
75
+ seen.add(current.id);
76
+ current = current.replyTo ? byId.get(current.replyTo) : undefined;
77
+ }
78
+ return false;
79
+ }
80
+ //# sourceMappingURL=comments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments.js","sourceRoot":"","sources":["../../src/core/comments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The three Navbook file kinds — spec 02 §2.5 (`issue.md`), §2.6 (comments and
3
+ * reviews) and §2.7 (`pr.md`): parsing, validation, and construction.
4
+ *
5
+ * Parsing never throws on schema problems; it collects them so `nav doctor` can
6
+ * report every fault in a file at once.
7
+ *
8
+ * Frontmatter is normalized once, at parse time: keys whose spec type is a
9
+ * string (or a list/map of strings) are read from the scalar's source text, so
10
+ * a hand-written all-digit commit SHA — which YAML would resolve to a lossy
11
+ * number — still round-trips as the forty characters the author typed.
12
+ */
13
+ import { type NavDoc } from "./frontmatter.ts";
14
+ export declare const SHA_PATTERN: RegExp;
15
+ export declare const VERDICTS: readonly ["approve", "request-changes"];
16
+ export type Verdict = (typeof VERDICTS)[number];
17
+ export interface Problem {
18
+ key?: string;
19
+ message: string;
20
+ }
21
+ export interface ParsedFile {
22
+ nav: NavDoc;
23
+ /** Frontmatter with spec-typed keys normalized; unknown keys passed through. */
24
+ fm: Record<string, unknown>;
25
+ /** Frontmatter exactly as the YAML parser resolved it. */
26
+ raw: Record<string, unknown>;
27
+ body: string;
28
+ problems: Problem[];
29
+ }
30
+ export interface Revision {
31
+ head: string;
32
+ base: string;
33
+ date: string;
34
+ }
35
+ /** Parse any Navbook file; frontmatter faults surface as problems, not throws. */
36
+ export declare function parseFile(text: string): ParsedFile;
37
+ /** Coerce spec-typed frontmatter keys; leave unknown keys exactly as parsed. */
38
+ export declare function normalizeFrontmatter(nav: NavDoc, parsed?: Record<string, unknown>): Record<string, unknown>;
39
+ /** Read `labels` defensively, ignoring malformed entries. */
40
+ export declare function readLabels(fm: Record<string, unknown>): string[];
41
+ /** Read `assignee` (scalar or list) defensively. */
42
+ export declare function readAssignees(fm: Record<string, unknown>): string[];
43
+ /** Read `revisions` defensively, keeping only well-formed entries. */
44
+ export declare function readRevisions(fm: Record<string, unknown>): Revision[];
45
+ /** Read the `merged:` block defensively. */
46
+ export declare function readMerged(fm: Record<string, unknown>): Record<string, unknown> | null;
47
+ /** Validate an `issue.md` (§2.5). */
48
+ export declare function validateIssue(parsed: ParsedFile): Problem[];
49
+ /** Validate a `pr.md` (§2.7). */
50
+ export declare function validatePr(parsed: ParsedFile): Problem[];
51
+ /** Validate a comment file (§2.6), including the optional review fields. */
52
+ export declare function validateComment(parsed: ParsedFile, opts: {
53
+ onPr: boolean;
54
+ }): Problem[];
55
+ export interface NewIssueInput {
56
+ title: string;
57
+ author: string;
58
+ created: string;
59
+ body: string;
60
+ labels?: string[];
61
+ assignee?: string[];
62
+ milestone?: string;
63
+ }
64
+ /** Render a new `issue.md`. */
65
+ export declare function newIssueFile(input: NewIssueInput): string;
66
+ export interface NewPrInput extends NewIssueInput {
67
+ target: string;
68
+ source: string;
69
+ revisions: Revision[];
70
+ draft?: boolean;
71
+ }
72
+ /** Render a new `pr.md`. */
73
+ export declare function newPrFile(input: NewPrInput): string;
74
+ export interface NewCommentInput {
75
+ author: string;
76
+ body: string;
77
+ replyTo?: string;
78
+ verdict?: Verdict;
79
+ revision?: string;
80
+ file?: string;
81
+ line?: string;
82
+ }
83
+ /** Render a new comment or review file. */
84
+ export declare function newCommentFile(input: NewCommentInput): string;
85
+ /** Trim trailing whitespace and guarantee exactly one closing newline. */
86
+ export declare function normalizeBody(body: string): string;
87
+ /** Format a person for frontmatter, falling back to the bare address. */
88
+ export declare function formatAuthor(name: string | undefined, email: string): string;