kennel 0.2.32 → 0.2.34

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 (81) hide show
  1. package/dist/cli/dispatch.js +5 -0
  2. package/dist/cli/usage.js +5 -0
  3. package/dist/core/runtime.js +458 -166
  4. package/dist/llm/command.js +10 -3
  5. package/dist/llm/worker.js +297 -32
  6. package/dist/notes/command.d.ts +1 -0
  7. package/dist/notes/command.js +556 -0
  8. package/dist/package/preview.js +481 -188
  9. package/dist/site/server-plugins.js +51 -10
  10. package/package.json +6 -3
  11. package/schemas/config.schema.json +11 -0
  12. package/scripts/verify-config.mjs +59 -0
  13. package/web/assets/{abnfDiagram-VRR7QNED-DPZyna8O.js → abnfDiagram-VRR7QNED-CpP1VHKI.js} +1 -1
  14. package/web/assets/{arc-BAcU9y_V.js → arc-kL4AxfvR.js} +1 -1
  15. package/web/assets/{architectureDiagram-ZJ3FMSHR-D5yd7Cag.js → architectureDiagram-ZJ3FMSHR-BWaRktRJ.js} +1 -1
  16. package/web/assets/{ask-tray-PMT6VVXN-C4dJH21Q.js → ask-tray-PMT6VVXN-FqPDpwtw.js} +3 -3
  17. package/web/assets/{blockDiagram-677ZJIJ3-C7LSGyxV.js → blockDiagram-677ZJIJ3-B07uVoPU.js} +1 -1
  18. package/web/assets/{c4Diagram-LMCZKHZV-BW2KfoQr.js → c4Diagram-LMCZKHZV-uvoaQC09.js} +1 -1
  19. package/web/assets/channel-Djmf1Ik1.js +1 -0
  20. package/web/assets/{chunk-2Q5K7J3B-4Og6VAFG.js → chunk-2Q5K7J3B-BH3SaHqk.js} +1 -1
  21. package/web/assets/{chunk-32BRIVSS-BYOowQl5.js → chunk-32BRIVSS-SPD8Eraw.js} +1 -1
  22. package/web/assets/{chunk-5VM5RSS4-C1m-lq64.js → chunk-5VM5RSS4-DNeOgOuZ.js} +1 -1
  23. package/web/assets/{chunk-EX3LRPZG-B2hriRpJ.js → chunk-EX3LRPZG-SJQ-vKGz.js} +1 -1
  24. package/web/assets/{chunk-JWPE2WC7-C6RsnzVA.js → chunk-JWPE2WC7-Dy_9wZ2Y.js} +1 -1
  25. package/web/assets/{chunk-MOJQB5TN-yKbMmUWf.js → chunk-MOJQB5TN-CuIx80Vn.js} +1 -1
  26. package/web/assets/{chunk-RYQCIY6F-BOokm3Qc.js → chunk-RYQCIY6F-DDG61RKR.js} +1 -1
  27. package/web/assets/{chunk-V7JOEXUC-CKW-H5An.js → chunk-V7JOEXUC-7OMVhaoH.js} +1 -1
  28. package/web/assets/{chunk-VR4S4FIN-BU5pFrup.js → chunk-VR4S4FIN-Y-HNaSmV.js} +1 -1
  29. package/web/assets/{chunk-XXDRQBXY-DYtdSJHU.js → chunk-XXDRQBXY-CtG0MPgC.js} +1 -1
  30. package/web/assets/classDiagram-OUVF2IWQ-hlDS9RWf.js +1 -0
  31. package/web/assets/classDiagram-v2-EOCWNBFH-hlDS9RWf.js +1 -0
  32. package/web/assets/{cose-bilkent-JH36ORCC-BZeXtC6E.js → cose-bilkent-JH36ORCC-BZaPMYcl.js} +1 -1
  33. package/web/assets/{cynefin-VYW2F7L2-BSDVyzMI.js → cynefin-VYW2F7L2-DxAlRHA1.js} +1 -1
  34. package/web/assets/{cynefinDiagram-TSTJHNR4-CReoDBYo.js → cynefinDiagram-TSTJHNR4-gjgzmagS.js} +1 -1
  35. package/web/assets/{dagre-VKFMJZFB-CBLLzMxd.js → dagre-VKFMJZFB-DvLPeYuw.js} +1 -1
  36. package/web/assets/{diagram-FQU43EPY-BcGWiZXZ.js → diagram-FQU43EPY-443LFnzG.js} +1 -1
  37. package/web/assets/{diagram-G47NLZAW-BXUYZA9N.js → diagram-G47NLZAW-CqfUewot.js} +1 -1
  38. package/web/assets/{diagram-NH7WQ7WH-BrvrZs7N.js → diagram-NH7WQ7WH-D-FElUGW.js} +1 -1
  39. package/web/assets/{diagram-OA4YK3LP-7lnVcjXL.js → diagram-OA4YK3LP-DbidUAp5.js} +1 -1
  40. package/web/assets/{diagram-WEI45ONY-s58_bMFf.js → diagram-WEI45ONY-CPLeckCZ.js} +1 -1
  41. package/web/assets/{ebnfDiagram-CCIWWBDH-BemWecoD.js → ebnfDiagram-CCIWWBDH-DR0rqZzN.js} +1 -1
  42. package/web/assets/{entity-search-page-G6QEQNXT--sKP5dP3.js → entity-search-page-G6QEQNXT-CnG8d-at.js} +1 -1
  43. package/web/assets/{erDiagram-Q63AITRT-ClVJddch.js → erDiagram-Q63AITRT-DMXv_8Tj.js} +1 -1
  44. package/web/assets/{flowDiagram-23GEKE2U-xzUJu-i_.js → flowDiagram-23GEKE2U-DnbQjZs0.js} +1 -1
  45. package/web/assets/{ganttDiagram-NO4QXBWP-CXQmtF5g.js → ganttDiagram-NO4QXBWP-DBKIrjj8.js} +1 -1
  46. package/web/assets/{gitGraphDiagram-IHSO6WYX-DCdljcLl.js → gitGraphDiagram-IHSO6WYX-BqqxKWT_.js} +1 -1
  47. package/web/assets/{index-CtTx1o2d.js → index-Bd3ghcW-.js} +5 -5
  48. package/web/assets/{index-vjrhIAJn.css → index-Dt8OzQCe.css} +1 -1
  49. package/web/assets/{infoDiagram-FWYZ7A6U-B11SJYSK.js → infoDiagram-FWYZ7A6U-Ba6Fs17y.js} +1 -1
  50. package/web/assets/{ishikawaDiagram-FXEZZL3T-B2CCGlAm.js → ishikawaDiagram-FXEZZL3T-D3BOtFjy.js} +1 -1
  51. package/web/assets/{journeyDiagram-5HDEW3XC-CBPETzOk.js → journeyDiagram-5HDEW3XC-MbzFPQm8.js} +1 -1
  52. package/web/assets/{kanban-definition-HUTT4EX6-unhQ3RPE.js → kanban-definition-HUTT4EX6-CurBmwu1.js} +1 -1
  53. package/web/assets/{linear-DuP_pG51.js → linear-CQIr4JjI.js} +1 -1
  54. package/web/assets/{linked-session-header-M7WY6IGF-DwlNCJlj.js → linked-session-header-M7WY6IGF-BBgN8zTq.js} +1 -1
  55. package/web/assets/{mermaid.core-BlYNzkpn.js → mermaid.core-bE6Vl3bZ.js} +4 -4
  56. package/web/assets/{mindmap-definition-LN4V7U3C-CtKHN6O1.js → mindmap-definition-LN4V7U3C-C1a46OGU.js} +1 -1
  57. package/web/assets/{pegDiagram-2B236MQR-CRyz6XWY.js → pegDiagram-2B236MQR-DZxMOxek.js} +1 -1
  58. package/web/assets/{pieDiagram-ENE6RG2P-BXfkPm3Q.js → pieDiagram-ENE6RG2P-BN0XOKLi.js} +1 -1
  59. package/web/assets/{plugin-host-modules-5NQgs5Cq.js → plugin-host-modules-CaRD5_5q.js} +1 -1
  60. package/web/assets/{project-search-dialog-HJFGW4DG-DPly4OiB.js → project-search-dialog-HJFGW4DG-h_dHeh7g.js} +1 -1
  61. package/web/assets/{project-search-page-NKQEBGPF-Cc6HV7BN.js → project-search-page-NKQEBGPF-2AzjjLEC.js} +1 -1
  62. package/web/assets/{quadrantDiagram-ABIIQ3AL-Drb_qWex.js → quadrantDiagram-ABIIQ3AL-C6IegTP-.js} +1 -1
  63. package/web/assets/{railroadDiagram-RFXS5EU6-DCKk2Pix.js → railroadDiagram-RFXS5EU6-bsjyAl-q.js} +1 -1
  64. package/web/assets/{requirementDiagram-TGXJPOKE-6WwZwcpD.js → requirementDiagram-TGXJPOKE-Cl8EZWy7.js} +1 -1
  65. package/web/assets/{sankeyDiagram-HTMAVEWB-DGGk3WCE.js → sankeyDiagram-HTMAVEWB-DhK4rRux.js} +1 -1
  66. package/web/assets/{selection-popup-532EHNCZ-yPhTrk7L.js → selection-popup-532EHNCZ-E2iQ2gQQ.js} +1 -1
  67. package/web/assets/{sequenceDiagram-DBY2YBRQ-a4tGi3DH.js → sequenceDiagram-DBY2YBRQ-DQTaPSD6.js} +1 -1
  68. package/web/assets/{sizeCapture-X5ZJPWSS-CHehBmy9.js → sizeCapture-X5ZJPWSS-k77bZ1zg.js} +1 -1
  69. package/web/assets/{stateDiagram-2N3HPSRC-Bn9GSWgV.js → stateDiagram-2N3HPSRC-Cv_5WuZJ.js} +1 -1
  70. package/web/assets/{stateDiagram-v2-6OUMAXLB-DI7fdBd7.js → stateDiagram-v2-6OUMAXLB-DJ1Lrskh.js} +1 -1
  71. package/web/assets/{swimlanes-5IMT3BWC-BX0WRzfR.js → swimlanes-5IMT3BWC-gBtAjG3z.js} +2 -2
  72. package/web/assets/swimlanesDiagram-G3AALYLV-DF1K9voG.js +8 -0
  73. package/web/assets/{timeline-definition-FHXFAJF6-9Ni2kIzq.js → timeline-definition-FHXFAJF6-CFjnM1Qq.js} +1 -1
  74. package/web/assets/{vennDiagram-L72KCM5P-rfBSNh3U.js → vennDiagram-L72KCM5P-CeMWrThY.js} +1 -1
  75. package/web/assets/{wardleyDiagram-EHGQE667-CzziImCC.js → wardleyDiagram-EHGQE667-R-ZaAOAI.js} +1 -1
  76. package/web/assets/{xychartDiagram-FW5EYKEG-DTLI4h5R.js → xychartDiagram-FW5EYKEG-BFXoMXiN.js} +1 -1
  77. package/web/index.html +2 -2
  78. package/web/assets/channel-um92dqpm.js +0 -1
  79. package/web/assets/classDiagram-OUVF2IWQ-h7sFEJBg.js +0 -1
  80. package/web/assets/classDiagram-v2-EOCWNBFH-h7sFEJBg.js +0 -1
  81. package/web/assets/swimlanesDiagram-G3AALYLV-OGHC9nqi.js +0 -8
@@ -0,0 +1,556 @@
1
+ // packages/notes/src/store.ts
2
+ import { execFile } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import {
5
+ mkdir,
6
+ readdir,
7
+ readFile,
8
+ realpath,
9
+ rename,
10
+ rm,
11
+ stat,
12
+ writeFile
13
+ } from "node:fs/promises";
14
+ import { homedir } from "node:os";
15
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
16
+ import { promisify } from "node:util";
17
+ var run = promisify(execFile);
18
+ var REPOSITORY_ID_KEY = "kennel.repoId";
19
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
20
+ var markdownExtensions = new Set([".md", ".markdown"]);
21
+ var caseInsensitiveHosts = new Set(["github.com", "gitlab.com", "bitbucket.org"]);
22
+ var indexLockStaleMs = 30000;
23
+ var indexLockAttempts = 100;
24
+ var indexLockWaitMs = 50;
25
+
26
+ class RepositoryNotesError extends Error {
27
+ code;
28
+ constructor(code, message) {
29
+ super(message);
30
+ this.name = "RepositoryNotesError";
31
+ this.code = code;
32
+ }
33
+ }
34
+ function kennelConfigHome() {
35
+ const xdg = process.env.XDG_CONFIG_HOME?.trim();
36
+ return xdg ? xdg : join(homedir(), ".config");
37
+ }
38
+ function kennelConfigFile() {
39
+ return join(kennelConfigHome(), "kennel", "config.json");
40
+ }
41
+ function repositoryNotesRoot() {
42
+ return join(kennelConfigHome(), "kennel", "repos");
43
+ }
44
+ function repositoryIndexFile() {
45
+ return join(repositoryNotesRoot(), "index.json");
46
+ }
47
+ function isRepositoryId(value) {
48
+ return typeof value === "string" && UUID.test(value);
49
+ }
50
+ function managedNotesDirectory(id) {
51
+ if (!isRepositoryId(id)) {
52
+ throw new RepositoryNotesError("invalid-id", `Invalid repository id: ${id}`);
53
+ }
54
+ return join(repositoryNotesRoot(), id);
55
+ }
56
+ function enableHint() {
57
+ return `Enable repository notes by adding {"repositoryNotes":{"enabled":true}} to ${kennelConfigFile()} (keep the other keys of that file).`;
58
+ }
59
+ async function readRepositoryNotesSettings() {
60
+ const file = kennelConfigFile();
61
+ let text;
62
+ try {
63
+ text = await readFile(file, "utf8");
64
+ } catch (error) {
65
+ if (error.code === "ENOENT") {
66
+ return { enabled: false };
67
+ }
68
+ throw new RepositoryNotesError("config-invalid", `Cannot read ${file}: ${errorMessage(error)}`);
69
+ }
70
+ let value;
71
+ try {
72
+ value = JSON.parse(text);
73
+ } catch (error) {
74
+ throw new RepositoryNotesError("config-invalid", `Invalid JSON in ${file}: ${errorMessage(error)}`);
75
+ }
76
+ if (!isRecord(value)) {
77
+ throw new RepositoryNotesError("config-invalid", `${file} must hold an object`);
78
+ }
79
+ const section = value.repositoryNotes;
80
+ if (section === undefined)
81
+ return { enabled: false };
82
+ if (!isRecord(section) || section.enabled !== undefined && typeof section.enabled !== "boolean") {
83
+ throw new RepositoryNotesError("config-invalid", `${file}: repositoryNotes must be an object with a boolean "enabled" field`);
84
+ }
85
+ return { enabled: section.enabled === true };
86
+ }
87
+ async function git(args, cwd) {
88
+ const { stdout } = await run("git", args, {
89
+ cwd,
90
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" },
91
+ maxBuffer: 1024 * 1024
92
+ });
93
+ return stdout.replace(/\r?\n$/u, "");
94
+ }
95
+ function gitExitCode(error) {
96
+ const code = error.code;
97
+ return typeof code === "number" ? code : undefined;
98
+ }
99
+ async function resolveCheckout(folder) {
100
+ let start;
101
+ try {
102
+ start = await realpath(resolve(folder));
103
+ if (!(await stat(start)).isDirectory())
104
+ start = dirname(start);
105
+ } catch {
106
+ throw new RepositoryNotesError("not-git", `Folder does not exist: ${folder}`);
107
+ }
108
+ let output;
109
+ try {
110
+ output = await git(["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-common-dir"], start);
111
+ } catch (error) {
112
+ if (gitExitCode(error) === 128) {
113
+ throw new RepositoryNotesError("not-git", `Not a Git repository: ${start}. Repository notes need a Git checkout.`);
114
+ }
115
+ throw error;
116
+ }
117
+ const [top, common] = output.split(/\r?\n/u);
118
+ if (!top || !common) {
119
+ throw new RepositoryNotesError("not-git", `Not a Git work tree: ${start}`);
120
+ }
121
+ const root = await realpath(top);
122
+ const commonDir = await realpath(common);
123
+ return { root, commonDir, configFile: join(commonDir, "config") };
124
+ }
125
+ async function readBinding(checkout) {
126
+ let value;
127
+ try {
128
+ value = await git(["config", "--file", checkout.configFile, "--get", REPOSITORY_ID_KEY], checkout.root);
129
+ } catch (error) {
130
+ if (gitExitCode(error) === 1)
131
+ return;
132
+ throw error;
133
+ }
134
+ const id = value.trim();
135
+ if (!id)
136
+ return;
137
+ if (!isRepositoryId(id)) {
138
+ throw new RepositoryNotesError("invalid-binding", `${checkout.configFile} has an invalid ${REPOSITORY_ID_KEY} value. Remove it or run "kennel notes new-identity".`);
139
+ }
140
+ return id;
141
+ }
142
+ async function writeBinding(checkout, id) {
143
+ await git(["config", "--file", checkout.configFile, REPOSITORY_ID_KEY, id], checkout.root);
144
+ }
145
+ function redactRemoteUrl(url) {
146
+ return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@]*@/iu, "$1").replace(/^[^@/:\s]+@([^:/\s]+:)/u, "$1");
147
+ }
148
+ function isLocalPath(url) {
149
+ return url.startsWith("/") || url.startsWith("./") || url.startsWith("../") || url.startsWith("~") || url === "." || url === ".." || /^[A-Za-z]:[\\/]/u.test(url) || /^file:/iu.test(url);
150
+ }
151
+ function normalizeHost(host) {
152
+ const bare = host.replace(/^\[|\]$/gu, "");
153
+ return bare.includes(".") || bare.includes(":") || bare === "localhost" ? bare.toLowerCase() : bare;
154
+ }
155
+ function normalizePath(path, host) {
156
+ let value = path.replace(/\/{2,}/gu, "/").replace(/^\/+/u, "").replace(/\/+$/u, "");
157
+ if (value.endsWith(".git"))
158
+ value = value.slice(0, -4);
159
+ value = value.replace(/\/+$/u, "");
160
+ if (caseInsensitiveHosts.has(host))
161
+ value = value.toLowerCase();
162
+ return value;
163
+ }
164
+ function normalizeRemoteAlias(url) {
165
+ const trimmed = url.trim();
166
+ const reject = (reason) => {
167
+ throw new RepositoryNotesError("unsupported-remote", `Unsupported remote URL ${redactRemoteUrl(trimmed)}: ${reason}. Use "kennel notes new-identity" for a local-only collection.`);
168
+ };
169
+ if (!trimmed || /[\s\u0000-\u001f\u007f]/u.test(trimmed))
170
+ reject("it is empty or contains whitespace");
171
+ if (isLocalPath(trimmed))
172
+ reject("local paths and file URLs have no shared address");
173
+ const scheme = /^([a-z][a-z0-9+.-]*):\/\//iu.exec(trimmed);
174
+ if (scheme) {
175
+ const name = scheme[1].toLowerCase();
176
+ const defaultPorts = {
177
+ "git+ssh": "22",
178
+ "ssh+git": "22",
179
+ git: "9418",
180
+ http: "80",
181
+ https: "443",
182
+ ssh: "22"
183
+ };
184
+ if (!(name in defaultPorts))
185
+ reject(`the ${name} scheme is not supported`);
186
+ let parsed;
187
+ try {
188
+ parsed = new URL(trimmed);
189
+ } catch {
190
+ return reject("it is not a valid URL");
191
+ }
192
+ if (!parsed.hostname)
193
+ reject("it has no host");
194
+ const host = normalizeHost(parsed.hostname);
195
+ const port = parsed.port && parsed.port !== defaultPorts[name] ? `:${parsed.port}` : "";
196
+ const path = normalizePath(decodeURIComponent(parsed.pathname), host);
197
+ if (!path)
198
+ reject("it has no repository path");
199
+ return `${host}${port}/${path}`;
200
+ }
201
+ const scp = /^(?:[^@/:\s]+@)?([^:/\s]+):(.+)$/u.exec(trimmed);
202
+ if (scp) {
203
+ const host = normalizeHost(scp[1]);
204
+ const path = normalizePath(scp[2], host);
205
+ if (!path)
206
+ reject("it has no repository path");
207
+ return `${host}/${path}`;
208
+ }
209
+ return reject("only ssh://, https://, http://, git:// and user@host:path forms are supported");
210
+ }
211
+ async function selectRemote(checkout, preferred) {
212
+ const names = (await git(["remote"], checkout.root)).split(/\r?\n/u).filter(Boolean);
213
+ let name;
214
+ if (preferred !== undefined) {
215
+ if (!names.includes(preferred)) {
216
+ throw new RepositoryNotesError("ambiguous-remote", `Remote "${preferred}" does not exist. Remotes: ${names.join(", ") || "none"}.`);
217
+ }
218
+ name = preferred;
219
+ } else if (names.includes("origin")) {
220
+ name = "origin";
221
+ } else if (names.length === 1) {
222
+ name = names[0];
223
+ } else if (names.length > 1) {
224
+ throw new RepositoryNotesError("ambiguous-remote", `Several remotes and no origin (${names.join(", ")}). Choose one with --remote <name>.`);
225
+ }
226
+ if (!name)
227
+ return;
228
+ const url = await git(["remote", "get-url", name], checkout.root);
229
+ return { alias: normalizeRemoteAlias(url), name };
230
+ }
231
+ function isRecord(value) {
232
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
233
+ }
234
+ function errorMessage(error) {
235
+ return error instanceof Error ? error.message : String(error);
236
+ }
237
+ function malformedIndex(detail) {
238
+ throw new RepositoryNotesError("index-malformed", `Malformed repository notes index ${repositoryIndexFile()}: ${detail}. Repair the file; Kennel does not replace it.`);
239
+ }
240
+ async function readRepositoryIndex() {
241
+ let text;
242
+ try {
243
+ text = await readFile(repositoryIndexFile(), "utf8");
244
+ } catch (error) {
245
+ if (error.code === "ENOENT") {
246
+ return { version: 1, repositories: {} };
247
+ }
248
+ throw error;
249
+ }
250
+ let value;
251
+ try {
252
+ value = JSON.parse(text);
253
+ } catch (error) {
254
+ return malformedIndex(errorMessage(error));
255
+ }
256
+ if (!isRecord(value) || value.version !== 1 || !isRecord(value.repositories)) {
257
+ malformedIndex('expected {"version":1,"repositories":{...}}');
258
+ }
259
+ const repositories = {};
260
+ const owners = new Map;
261
+ for (const [id, entry] of Object.entries(value.repositories)) {
262
+ if (!isRepositoryId(id))
263
+ malformedIndex(`"${id}" is not a repository id`);
264
+ if (!isRecord(entry) || !Array.isArray(entry.aliases)) {
265
+ malformedIndex(`entry ${id} needs an aliases array`);
266
+ }
267
+ const aliases = [];
268
+ for (const alias of entry.aliases) {
269
+ if (typeof alias !== "string" || !alias.trim())
270
+ malformedIndex(`entry ${id} has an invalid alias`);
271
+ const owner = owners.get(alias);
272
+ if (owner && owner !== id)
273
+ malformedIndex(`alias ${alias} belongs to ${owner} and ${id}`);
274
+ owners.set(alias, id);
275
+ if (!aliases.includes(alias))
276
+ aliases.push(alias);
277
+ }
278
+ repositories[id] = {
279
+ aliases,
280
+ createdAt: typeof entry.createdAt === "string" ? entry.createdAt : new Date(0).toISOString(),
281
+ ...typeof entry.name === "string" ? { name: entry.name } : {}
282
+ };
283
+ }
284
+ return { version: 1, repositories };
285
+ }
286
+ async function writeRepositoryIndex(index) {
287
+ const file = repositoryIndexFile();
288
+ const temporary = join(dirname(file), `.index-${process.pid}-${randomUUID()}.tmp`);
289
+ try {
290
+ await writeFile(temporary, `${JSON.stringify(index, null, 2)}
291
+ `, { flag: "wx" });
292
+ await rename(temporary, file);
293
+ } finally {
294
+ await rm(temporary, { force: true });
295
+ }
296
+ }
297
+ async function withIndexLock(action) {
298
+ const root = repositoryNotesRoot();
299
+ await mkdir(root, { recursive: true });
300
+ const lock = join(root, "index.lock");
301
+ for (let attempt = 0;attempt < indexLockAttempts; attempt += 1) {
302
+ try {
303
+ await mkdir(lock);
304
+ break;
305
+ } catch (error) {
306
+ if (error.code !== "EEXIST")
307
+ throw error;
308
+ const details = await stat(lock).catch(() => {
309
+ return;
310
+ });
311
+ if (details && Date.now() - details.mtimeMs > indexLockStaleMs) {
312
+ await rm(lock, { force: true, recursive: true });
313
+ continue;
314
+ }
315
+ if (attempt === indexLockAttempts - 1) {
316
+ throw new RepositoryNotesError("index-locked", `Another registration holds ${lock}. Retry, or remove the lock if no Kennel process is running.`);
317
+ }
318
+ await new Promise((done) => setTimeout(done, indexLockWaitMs));
319
+ }
320
+ }
321
+ try {
322
+ return await action();
323
+ } finally {
324
+ await rm(lock, { force: true, recursive: true });
325
+ }
326
+ }
327
+ function aliasOwner(index, alias) {
328
+ return Object.entries(index.repositories).find(([, entry]) => entry.aliases.includes(alias))?.[0];
329
+ }
330
+ function displayName(alias, root) {
331
+ return basename(alias ?? root);
332
+ }
333
+ async function assertEnabled() {
334
+ const settings = await readRepositoryNotesSettings();
335
+ if (!settings.enabled) {
336
+ throw new RepositoryNotesError("disabled", `Repository notes are disabled. ${enableHint()}`);
337
+ }
338
+ }
339
+ async function bindAndCreate(checkout, id, alias) {
340
+ try {
341
+ await writeBinding(checkout, id);
342
+ } catch (error) {
343
+ throw new RepositoryNotesError("partial-registration", `The index records ${id}${alias ? ` for ${alias}` : ""}, but writing ${REPOSITORY_ID_KEY} to ${checkout.configFile} failed: ${errorMessage(error)}. The checkout is not bound; run the command again.`);
344
+ }
345
+ const notesDirectory = managedNotesDirectory(id);
346
+ await mkdir(notesDirectory, { recursive: true });
347
+ return { id, notesDirectory, root: checkout.root, ...alias ? { alias } : {} };
348
+ }
349
+ async function ensureRepositoryNotes(folder, options = {}) {
350
+ const checkout = await resolveCheckout(folder);
351
+ await assertEnabled();
352
+ const bound = await readBinding(checkout);
353
+ if (bound) {
354
+ const notesDirectory = managedNotesDirectory(bound);
355
+ await mkdir(notesDirectory, { recursive: true });
356
+ return { id: bound, notesDirectory, root: checkout.root };
357
+ }
358
+ const remote = await selectRemote(checkout, options.remote);
359
+ return withIndexLock(async () => {
360
+ const raced = await readBinding(checkout);
361
+ if (raced)
362
+ return bindAndCreate(checkout, raced, undefined);
363
+ const index = await readRepositoryIndex();
364
+ const known = remote ? aliasOwner(index, remote.alias) : undefined;
365
+ const id = known ?? randomUUID();
366
+ if (!known) {
367
+ index.repositories[id] = {
368
+ aliases: remote ? [remote.alias] : [],
369
+ createdAt: new Date().toISOString(),
370
+ name: displayName(remote?.alias, checkout.root)
371
+ };
372
+ await writeRepositoryIndex(index);
373
+ }
374
+ return bindAndCreate(checkout, id, remote?.alias);
375
+ });
376
+ }
377
+ async function bindRepositoryNotes(folder, id, options = {}) {
378
+ if (!isRepositoryId(id)) {
379
+ throw new RepositoryNotesError("invalid-id", `Invalid repository id: ${id}`);
380
+ }
381
+ const checkout = await resolveCheckout(folder);
382
+ await assertEnabled();
383
+ const bound = await readBinding(checkout);
384
+ if (bound && bound !== id && !options.replace) {
385
+ throw new RepositoryNotesError("binding-conflict", `${checkout.root} is already bound to ${bound}. Pass --replace to bind it to ${id} instead; notes are not moved or merged.`);
386
+ }
387
+ const remote = options.recordAlias === false ? undefined : await selectRemote(checkout, options.remote);
388
+ return withIndexLock(async () => {
389
+ const index = await readRepositoryIndex();
390
+ const exists = Boolean(index.repositories[id]) || await stat(managedNotesDirectory(id)).then((details) => details.isDirectory()).catch(() => false);
391
+ if (!exists) {
392
+ throw new RepositoryNotesError("unknown-id", `Unknown repository id ${id}: it is not in the index and has no notes directory.`);
393
+ }
394
+ if (remote) {
395
+ const owner = aliasOwner(index, remote.alias);
396
+ if (owner && owner !== id) {
397
+ throw new RepositoryNotesError("alias-conflict", `Remote ${remote.alias} already belongs to collection ${owner}. Pass --no-alias to bind without recording this address.`);
398
+ }
399
+ const entry = index.repositories[id] ??= {
400
+ aliases: [],
401
+ createdAt: new Date().toISOString(),
402
+ name: displayName(remote.alias, checkout.root)
403
+ };
404
+ if (!owner) {
405
+ entry.aliases.push(remote.alias);
406
+ await writeRepositoryIndex(index);
407
+ }
408
+ }
409
+ return bindAndCreate(checkout, id, remote?.alias);
410
+ });
411
+ }
412
+ async function newRepositoryIdentity(folder, options = {}) {
413
+ const checkout = await resolveCheckout(folder);
414
+ await assertEnabled();
415
+ const previousId = await readBinding(checkout);
416
+ const remote = await selectRemote(checkout, options.remote);
417
+ return withIndexLock(async () => {
418
+ const index = await readRepositoryIndex();
419
+ const id = randomUUID();
420
+ const owner = remote ? aliasOwner(index, remote.alias) : undefined;
421
+ const alias = remote && !owner ? remote.alias : undefined;
422
+ index.repositories[id] = {
423
+ aliases: alias ? [alias] : [],
424
+ createdAt: new Date().toISOString(),
425
+ name: displayName(remote?.alias, checkout.root)
426
+ };
427
+ await writeRepositoryIndex(index);
428
+ const destination = await bindAndCreate(checkout, id, alias);
429
+ return {
430
+ ...destination,
431
+ ...owner ? { aliasOwner: owner } : {},
432
+ ...previousId ? { previousId } : {}
433
+ };
434
+ });
435
+ }
436
+
437
+ // packages/cli/src/notes/command.ts
438
+ var USAGE = `Usage:
439
+ kennel notes path [folder] [--json] [--remote <name>]
440
+ kennel notes bind <uuid> [folder] [--json] [--remote <name>] [--replace] [--no-alias]
441
+ kennel notes new-identity [folder] [--json] [--remote <name>]
442
+ kennel notes --help
443
+
444
+ Repository notes live in \${XDG_CONFIG_HOME:-$HOME/.config}/kennel/repos/<uuid>/.
445
+ The feature is opt-in: {"repositoryNotes":{"enabled":true}} in kennel/config.json.
446
+
447
+ path Resolve and create the notes folder of the Git repository that
448
+ contains the folder (default: the current directory). Plain
449
+ output is the absolute folder; --json adds id and root. A new
450
+ checkout reuses the collection of its normalized origin (or sole)
451
+ remote, or gets a new id. The binding is kennel.repoId in the
452
+ shared local Git config. Nothing is moved, fetched, or published.
453
+ bind Bind a checkout to an existing collection, for renamed remotes or
454
+ fresh clones. --replace changes an existing different binding.
455
+ The current remote address is recorded as an alias unless
456
+ --no-alias is given; an address owned by another collection fails.
457
+ new-identity Start a new collection for this checkout. Existing notes stay.
458
+ --remote Choose the remote when there are several and no origin.
459
+
460
+ Errors go to stderr with a nonzero exit status. Non-Git folders are an error.
461
+ `;
462
+ function usageError(message) {
463
+ throw new Error(`${message}
464
+ Run kennel notes --help for usage.`);
465
+ }
466
+ function parseArgs(args) {
467
+ const options = { folder: ".", help: false, json: false, recordAlias: true, replace: false };
468
+ const positionals = [];
469
+ for (let index = 0;index < args.length; index += 1) {
470
+ const arg = args[index];
471
+ if (arg === "-h" || arg === "--help")
472
+ options.help = true;
473
+ else if (arg === "--json")
474
+ options.json = true;
475
+ else if (arg === "--replace")
476
+ options.replace = true;
477
+ else if (arg === "--no-alias")
478
+ options.recordAlias = false;
479
+ else if (arg === "--remote" || arg.startsWith("--remote=")) {
480
+ const value = arg === "--remote" ? args[++index] : arg.slice("--remote=".length);
481
+ if (!value || value.startsWith("-"))
482
+ usageError("--remote requires a remote name");
483
+ options.remote = value;
484
+ } else if (arg.startsWith("-"))
485
+ usageError(`Unknown option: ${arg}`);
486
+ else
487
+ positionals.push(arg);
488
+ }
489
+ if (options.help)
490
+ return options;
491
+ const [subcommand, ...rest] = positionals;
492
+ options.subcommand = subcommand;
493
+ if (subcommand === "bind") {
494
+ const [id, folder, extra] = rest;
495
+ if (!id)
496
+ usageError("bind requires a repository id");
497
+ if (extra !== undefined)
498
+ usageError(`Unexpected argument: ${extra}`);
499
+ options.id = id;
500
+ if (folder)
501
+ options.folder = folder;
502
+ } else if (subcommand === "path" || subcommand === "new-identity") {
503
+ const [folder, extra] = rest;
504
+ if (extra !== undefined)
505
+ usageError(`Unexpected argument: ${extra}`);
506
+ if (folder)
507
+ options.folder = folder;
508
+ } else if (subcommand === undefined) {
509
+ usageError("kennel notes needs a subcommand: path, bind, or new-identity");
510
+ } else {
511
+ usageError(`Unknown subcommand: ${subcommand}`);
512
+ }
513
+ if (options.replace && subcommand !== "bind")
514
+ usageError("--replace applies to bind only");
515
+ if (!options.recordAlias && subcommand !== "bind")
516
+ usageError("--no-alias applies to bind only");
517
+ return options;
518
+ }
519
+ async function notes(args) {
520
+ let options;
521
+ try {
522
+ options = parseArgs(args);
523
+ } catch (error) {
524
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
525
+ process.exitCode = 1;
526
+ return;
527
+ }
528
+ if (options.help) {
529
+ process.stdout.write(USAGE);
530
+ return;
531
+ }
532
+ try {
533
+ const result = options.subcommand === "bind" ? await bindRepositoryNotes(options.folder, options.id, {
534
+ recordAlias: options.recordAlias,
535
+ remote: options.remote,
536
+ replace: options.replace
537
+ }) : options.subcommand === "new-identity" ? await newRepositoryIdentity(options.folder, { remote: options.remote }) : await ensureRepositoryNotes(options.folder, { remote: options.remote });
538
+ if (options.json) {
539
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
540
+ `);
541
+ return;
542
+ }
543
+ process.stdout.write(`${result.notesDirectory}
544
+ `);
545
+ if ("aliasOwner" in result && result.aliasOwner) {
546
+ console.error(`Note: the remote address stays with collection ${result.aliasOwner}; this checkout now uses ${result.id} without an alias.`);
547
+ }
548
+ } catch (error) {
549
+ const message = error instanceof Error ? error.message : String(error);
550
+ console.error(error instanceof RepositoryNotesError ? `Error (${error.code}): ${message}` : `Error: ${message}`);
551
+ process.exitCode = 1;
552
+ }
553
+ }
554
+ export {
555
+ notes
556
+ };