@opencodehub/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 (191) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +85 -0
  3. package/dist/agent-context.d.ts +54 -0
  4. package/dist/agent-context.d.ts.map +1 -0
  5. package/dist/agent-context.js +122 -0
  6. package/dist/agent-context.js.map +1 -0
  7. package/dist/cobol-proleap-setup.d.ts +77 -0
  8. package/dist/cobol-proleap-setup.d.ts.map +1 -0
  9. package/dist/cobol-proleap-setup.js +289 -0
  10. package/dist/cobol-proleap-setup.js.map +1 -0
  11. package/dist/commands/analyze.d.ts +234 -0
  12. package/dist/commands/analyze.d.ts.map +1 -0
  13. package/dist/commands/analyze.js +1096 -0
  14. package/dist/commands/analyze.js.map +1 -0
  15. package/dist/commands/augment.d.ts +48 -0
  16. package/dist/commands/augment.d.ts.map +1 -0
  17. package/dist/commands/augment.js +249 -0
  18. package/dist/commands/augment.js.map +1 -0
  19. package/dist/commands/baseline.d.ts +68 -0
  20. package/dist/commands/baseline.d.ts.map +1 -0
  21. package/dist/commands/baseline.js +110 -0
  22. package/dist/commands/baseline.js.map +1 -0
  23. package/dist/commands/bench.d.ts +54 -0
  24. package/dist/commands/bench.d.ts.map +1 -0
  25. package/dist/commands/bench.js +283 -0
  26. package/dist/commands/bench.js.map +1 -0
  27. package/dist/commands/ci-init.d.ts +37 -0
  28. package/dist/commands/ci-init.d.ts.map +1 -0
  29. package/dist/commands/ci-init.js +115 -0
  30. package/dist/commands/ci-init.js.map +1 -0
  31. package/dist/commands/clean.d.ts +13 -0
  32. package/dist/commands/clean.d.ts.map +1 -0
  33. package/dist/commands/clean.js +38 -0
  34. package/dist/commands/clean.js.map +1 -0
  35. package/dist/commands/code-pack.d.ts +105 -0
  36. package/dist/commands/code-pack.d.ts.map +1 -0
  37. package/dist/commands/code-pack.js +187 -0
  38. package/dist/commands/code-pack.js.map +1 -0
  39. package/dist/commands/context.d.ts +30 -0
  40. package/dist/commands/context.d.ts.map +1 -0
  41. package/dist/commands/context.js +237 -0
  42. package/dist/commands/context.js.map +1 -0
  43. package/dist/commands/detect-changes.d.ts +26 -0
  44. package/dist/commands/detect-changes.d.ts.map +1 -0
  45. package/dist/commands/detect-changes.js +73 -0
  46. package/dist/commands/detect-changes.js.map +1 -0
  47. package/dist/commands/doctor.d.ts +52 -0
  48. package/dist/commands/doctor.d.ts.map +1 -0
  49. package/dist/commands/doctor.js +472 -0
  50. package/dist/commands/doctor.js.map +1 -0
  51. package/dist/commands/find-enclosing-symbol.d.ts +67 -0
  52. package/dist/commands/find-enclosing-symbol.d.ts.map +1 -0
  53. package/dist/commands/find-enclosing-symbol.js +106 -0
  54. package/dist/commands/find-enclosing-symbol.js.map +1 -0
  55. package/dist/commands/group.d.ts +123 -0
  56. package/dist/commands/group.d.ts.map +1 -0
  57. package/dist/commands/group.js +448 -0
  58. package/dist/commands/group.js.map +1 -0
  59. package/dist/commands/impact.d.ts +23 -0
  60. package/dist/commands/impact.d.ts.map +1 -0
  61. package/dist/commands/impact.js +91 -0
  62. package/dist/commands/impact.js.map +1 -0
  63. package/dist/commands/index-repo.d.ts +39 -0
  64. package/dist/commands/index-repo.d.ts.map +1 -0
  65. package/dist/commands/index-repo.js +148 -0
  66. package/dist/commands/index-repo.js.map +1 -0
  67. package/dist/commands/ingest-sarif.d.ts +64 -0
  68. package/dist/commands/ingest-sarif.d.ts.map +1 -0
  69. package/dist/commands/ingest-sarif.js +381 -0
  70. package/dist/commands/ingest-sarif.js.map +1 -0
  71. package/dist/commands/init.d.ts +75 -0
  72. package/dist/commands/init.d.ts.map +1 -0
  73. package/dist/commands/init.js +315 -0
  74. package/dist/commands/init.js.map +1 -0
  75. package/dist/commands/list.d.ts +17 -0
  76. package/dist/commands/list.d.ts.map +1 -0
  77. package/dist/commands/list.js +79 -0
  78. package/dist/commands/list.js.map +1 -0
  79. package/dist/commands/mcp.d.ts +8 -0
  80. package/dist/commands/mcp.d.ts.map +1 -0
  81. package/dist/commands/mcp.js +28 -0
  82. package/dist/commands/mcp.js.map +1 -0
  83. package/dist/commands/open-store.d.ts +25 -0
  84. package/dist/commands/open-store.d.ts.map +1 -0
  85. package/dist/commands/open-store.js +47 -0
  86. package/dist/commands/open-store.js.map +1 -0
  87. package/dist/commands/pack.d.ts +35 -0
  88. package/dist/commands/pack.d.ts.map +1 -0
  89. package/dist/commands/pack.js +83 -0
  90. package/dist/commands/pack.js.map +1 -0
  91. package/dist/commands/query.d.ts +85 -0
  92. package/dist/commands/query.d.ts.map +1 -0
  93. package/dist/commands/query.js +309 -0
  94. package/dist/commands/query.js.map +1 -0
  95. package/dist/commands/scan.d.ts +81 -0
  96. package/dist/commands/scan.d.ts.map +1 -0
  97. package/dist/commands/scan.js +407 -0
  98. package/dist/commands/scan.js.map +1 -0
  99. package/dist/commands/setup.d.ts +178 -0
  100. package/dist/commands/setup.d.ts.map +1 -0
  101. package/dist/commands/setup.js +370 -0
  102. package/dist/commands/setup.js.map +1 -0
  103. package/dist/commands/sql.d.ts +19 -0
  104. package/dist/commands/sql.d.ts.map +1 -0
  105. package/dist/commands/sql.js +51 -0
  106. package/dist/commands/sql.js.map +1 -0
  107. package/dist/commands/status.d.ts +13 -0
  108. package/dist/commands/status.d.ts.map +1 -0
  109. package/dist/commands/status.js +66 -0
  110. package/dist/commands/status.js.map +1 -0
  111. package/dist/commands/verdict-render.d.ts +33 -0
  112. package/dist/commands/verdict-render.d.ts.map +1 -0
  113. package/dist/commands/verdict-render.js +123 -0
  114. package/dist/commands/verdict-render.js.map +1 -0
  115. package/dist/commands/verdict.d.ts +61 -0
  116. package/dist/commands/verdict.d.ts.map +1 -0
  117. package/dist/commands/verdict.js +146 -0
  118. package/dist/commands/verdict.js.map +1 -0
  119. package/dist/commands/wiki.d.ts +26 -0
  120. package/dist/commands/wiki.d.ts.map +1 -0
  121. package/dist/commands/wiki.js +74 -0
  122. package/dist/commands/wiki.js.map +1 -0
  123. package/dist/editors/claude-code.d.ts +23 -0
  124. package/dist/editors/claude-code.d.ts.map +1 -0
  125. package/dist/editors/claude-code.js +58 -0
  126. package/dist/editors/claude-code.js.map +1 -0
  127. package/dist/editors/codex.d.ts +22 -0
  128. package/dist/editors/codex.d.ts.map +1 -0
  129. package/dist/editors/codex.js +59 -0
  130. package/dist/editors/codex.js.map +1 -0
  131. package/dist/editors/cursor.d.ts +13 -0
  132. package/dist/editors/cursor.d.ts.map +1 -0
  133. package/dist/editors/cursor.js +21 -0
  134. package/dist/editors/cursor.js.map +1 -0
  135. package/dist/editors/index.d.ts +12 -0
  136. package/dist/editors/index.d.ts.map +1 -0
  137. package/dist/editors/index.js +11 -0
  138. package/dist/editors/index.js.map +1 -0
  139. package/dist/editors/opencode.d.ts +23 -0
  140. package/dist/editors/opencode.d.ts.map +1 -0
  141. package/dist/editors/opencode.js +61 -0
  142. package/dist/editors/opencode.js.map +1 -0
  143. package/dist/editors/types.d.ts +33 -0
  144. package/dist/editors/types.d.ts.map +1 -0
  145. package/dist/editors/types.js +19 -0
  146. package/dist/editors/types.js.map +1 -0
  147. package/dist/editors/windows-wrap.d.ts +19 -0
  148. package/dist/editors/windows-wrap.d.ts.map +1 -0
  149. package/dist/editors/windows-wrap.js +28 -0
  150. package/dist/editors/windows-wrap.js.map +1 -0
  151. package/dist/editors/windsurf.d.ts +12 -0
  152. package/dist/editors/windsurf.d.ts.map +1 -0
  153. package/dist/editors/windsurf.js +21 -0
  154. package/dist/editors/windsurf.js.map +1 -0
  155. package/dist/embedder-downloader.d.ts +87 -0
  156. package/dist/embedder-downloader.d.ts.map +1 -0
  157. package/dist/embedder-downloader.js +261 -0
  158. package/dist/embedder-downloader.js.map +1 -0
  159. package/dist/fs-atomic.d.ts +22 -0
  160. package/dist/fs-atomic.d.ts.map +1 -0
  161. package/dist/fs-atomic.js +28 -0
  162. package/dist/fs-atomic.js.map +1 -0
  163. package/dist/groups.d.ts +64 -0
  164. package/dist/groups.d.ts.map +1 -0
  165. package/dist/groups.js +172 -0
  166. package/dist/groups.js.map +1 -0
  167. package/dist/index.d.ts +11 -0
  168. package/dist/index.d.ts.map +1 -0
  169. package/dist/index.js +703 -0
  170. package/dist/index.js.map +1 -0
  171. package/dist/lib/is-indexed.d.ts +20 -0
  172. package/dist/lib/is-indexed.d.ts.map +1 -0
  173. package/dist/lib/is-indexed.js +35 -0
  174. package/dist/lib/is-indexed.js.map +1 -0
  175. package/dist/registry.d.ts +64 -0
  176. package/dist/registry.d.ts.map +1 -0
  177. package/dist/registry.js +145 -0
  178. package/dist/registry.js.map +1 -0
  179. package/dist/scip-downloader.d.ts +138 -0
  180. package/dist/scip-downloader.d.ts.map +1 -0
  181. package/dist/scip-downloader.js +372 -0
  182. package/dist/scip-downloader.js.map +1 -0
  183. package/dist/scip-pins.d.ts +99 -0
  184. package/dist/scip-pins.d.ts.map +1 -0
  185. package/dist/scip-pins.js +195 -0
  186. package/dist/scip-pins.js.map +1 -0
  187. package/dist/skills-gen.d.ts +47 -0
  188. package/dist/skills-gen.d.ts.map +1 -0
  189. package/dist/skills-gen.js +292 -0
  190. package/dist/skills-gen.js.map +1 -0
  191. package/package.json +81 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * SHA256-pinned downloader for gte-modernbert-base weights.
3
+ *
4
+ * Resolves the target directory via {@link resolveModelDir}, then for each
5
+ * pinned file in {@link GTE_MODERNBERT_BASE_PINS}:
6
+ * 1. Skip when the file already exists and its SHA256 matches the pin.
7
+ * 2. Otherwise stream-download to `<target>.tmp`, hash during write, verify
8
+ * hash, and atomically rename to the final path.
9
+ *
10
+ * Retries transient network errors (ECONNRESET / timeout / 5xx) up to 3 times
11
+ * with exponential backoff (100ms, 500ms, 2s). A SHA256 mismatch is a hard
12
+ * error — the `.tmp` file is deleted and the error thrown. We never ship
13
+ * weights that don't match the pin.
14
+ *
15
+ * All disk access is streaming; we never buffer a 596 MB file in memory.
16
+ */
17
+ import { createHash } from "node:crypto";
18
+ import { createReadStream, createWriteStream } from "node:fs";
19
+ import { mkdir, rename, stat, unlink } from "node:fs/promises";
20
+ import { dirname, join } from "node:path";
21
+ import { Readable, Writable } from "node:stream";
22
+ import { pipeline as streamPipeline } from "node:stream/promises";
23
+ import { setTimeout as delay } from "node:timers/promises";
24
+ import { GTE_MODERNBERT_BASE_PINS, resolveModelDir } from "@opencodehub/embedder";
25
+ const DEFAULT_BACKOFF_MS = [100, 500, 2000];
26
+ const DEFAULT_MAX_RETRIES = 3;
27
+ /**
28
+ * Thrown when a downloaded file's SHA256 doesn't match the pinned value.
29
+ *
30
+ * The temp file is deleted before this throws so partial corrupt payloads
31
+ * never linger on disk.
32
+ */
33
+ export class Sha256MismatchError extends Error {
34
+ code = "EMBEDDER_SHA256_MISMATCH";
35
+ fileName;
36
+ expected;
37
+ actual;
38
+ constructor(fileName, expected, actual) {
39
+ super(`SHA256 mismatch for ${fileName}: expected ${expected}, got ${actual}`);
40
+ this.name = "Sha256MismatchError";
41
+ this.fileName = fileName;
42
+ this.expected = expected;
43
+ this.actual = actual;
44
+ }
45
+ }
46
+ /**
47
+ * Thrown for all non-hash download failures (404, network, etc.). Carries the
48
+ * URL in the message so operators can reproduce with curl.
49
+ */
50
+ export class DownloadError extends Error {
51
+ code = "EMBEDDER_DOWNLOAD_FAILED";
52
+ url;
53
+ constructor(url, message, options) {
54
+ super(`Download failed for ${url}: ${message}`, options);
55
+ this.name = "DownloadError";
56
+ this.url = url;
57
+ }
58
+ }
59
+ /**
60
+ * Hash an existing file on disk in streaming fashion.
61
+ *
62
+ * Returns `undefined` if the file does not exist — callers use that as the
63
+ * "not yet downloaded" signal rather than a dedicated stat() probe.
64
+ */
65
+ async function hashFileIfExists(path) {
66
+ try {
67
+ await stat(path);
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ const hasher = createHash("sha256");
73
+ const rs = createReadStream(path);
74
+ await streamPipeline(rs, new Writable({
75
+ write(chunk, _enc, cb) {
76
+ // Convert Buffer → Uint8Array view so strict TS typings accept it.
77
+ hasher.update(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
78
+ cb();
79
+ },
80
+ }));
81
+ return hasher.digest("hex");
82
+ }
83
+ /**
84
+ * Decide whether a network error is retryable. We treat `ECONNRESET`,
85
+ * `ETIMEDOUT`, `EAI_AGAIN`, `ECONNREFUSED`, generic `AbortError` on timeout,
86
+ * and 5xx HTTP responses as transient. SHA mismatch and 4xx are permanent.
87
+ */
88
+ function isRetryableError(err) {
89
+ if (err instanceof Sha256MismatchError)
90
+ return false;
91
+ if (!(err instanceof Error))
92
+ return false;
93
+ const transientCodes = new Set([
94
+ "ECONNRESET",
95
+ "ETIMEDOUT",
96
+ "EAI_AGAIN",
97
+ "ECONNREFUSED",
98
+ "ENETUNREACH",
99
+ "UND_ERR_SOCKET",
100
+ ]);
101
+ // Walk the error + its .cause chain; the network-layer code lives on the
102
+ // underlying cause when we've wrapped the error as a DownloadError.
103
+ let cur = err;
104
+ let hops = 0;
105
+ while (cur instanceof Error && hops < 8) {
106
+ const codeCandidate = cur.code;
107
+ if (typeof codeCandidate === "string" && transientCodes.has(codeCandidate)) {
108
+ return true;
109
+ }
110
+ cur = cur.cause;
111
+ hops += 1;
112
+ }
113
+ // DownloadError with a 5xx status is retryable; the message encodes the code.
114
+ if (err instanceof DownloadError && /HTTP 5\d\d/.test(err.message)) {
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+ /**
120
+ * Stream one pinned file to disk. Hash-as-we-write, verify, and atomic rename.
121
+ * Does NOT retry — that's the caller's job via {@link withRetry}.
122
+ */
123
+ async function downloadOne(pin, targetPath, fetchImpl) {
124
+ const tmpPath = `${targetPath}.tmp`;
125
+ // Best-effort cleanup of any stale tmp from a previous failed run.
126
+ try {
127
+ await unlink(tmpPath);
128
+ }
129
+ catch {
130
+ // Doesn't exist — fine.
131
+ }
132
+ let res;
133
+ try {
134
+ res = await fetchImpl(pin.url, { redirect: "follow" });
135
+ }
136
+ catch (err) {
137
+ throw new DownloadError(pin.url, err instanceof Error ? err.message : String(err), err instanceof Error ? { cause: err } : undefined);
138
+ }
139
+ if (!res.ok) {
140
+ throw new DownloadError(pin.url, `HTTP ${res.status} ${res.statusText}`);
141
+ }
142
+ if (res.body === null) {
143
+ throw new DownloadError(pin.url, "response body is null");
144
+ }
145
+ const hasher = createHash("sha256");
146
+ let bytesWritten = 0;
147
+ const writeStream = createWriteStream(tmpPath);
148
+ // Web ReadableStream → Node Readable bridge. `fetch` returns a Web stream
149
+ // in all Node releases we support (>=20). We assert through the Node
150
+ // stream/web type because `fetch`'s global typings reference lib.dom which
151
+ // we deliberately exclude from this package.
152
+ const bodyAsNode = Readable.fromWeb(res.body);
153
+ try {
154
+ await streamPipeline(bodyAsNode, new Writable({
155
+ write(chunk, _enc, cb) {
156
+ const view = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
157
+ hasher.update(view);
158
+ bytesWritten += chunk.byteLength;
159
+ if (!writeStream.write(chunk)) {
160
+ writeStream.once("drain", () => cb());
161
+ }
162
+ else {
163
+ cb();
164
+ }
165
+ },
166
+ final(cb) {
167
+ writeStream.end(() => cb());
168
+ },
169
+ }));
170
+ }
171
+ catch (err) {
172
+ // Clean up partial tmp before bubbling — no corrupt files on disk.
173
+ try {
174
+ await unlink(tmpPath);
175
+ }
176
+ catch {
177
+ // Nothing to do.
178
+ }
179
+ throw new DownloadError(pin.url, err instanceof Error ? err.message : String(err), err instanceof Error ? { cause: err } : undefined);
180
+ }
181
+ const actual = hasher.digest("hex");
182
+ if (actual !== pin.sha256) {
183
+ try {
184
+ await unlink(tmpPath);
185
+ }
186
+ catch {
187
+ // Nothing to do.
188
+ }
189
+ throw new Sha256MismatchError(pin.name, pin.sha256, actual);
190
+ }
191
+ await rename(tmpPath, targetPath);
192
+ return bytesWritten;
193
+ }
194
+ /**
195
+ * Run `task` with exponential backoff. The error type determines whether a
196
+ * retry is attempted; non-transient errors bubble immediately.
197
+ */
198
+ async function withRetry(task, maxRetries, backoffMs) {
199
+ let lastErr;
200
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
201
+ try {
202
+ return await task();
203
+ }
204
+ catch (err) {
205
+ lastErr = err;
206
+ if (!isRetryableError(err) || attempt === maxRetries - 1) {
207
+ throw err;
208
+ }
209
+ const waitMs = backoffMs[attempt] ?? backoffMs[backoffMs.length - 1] ?? 0;
210
+ if (waitMs > 0) {
211
+ await delay(waitMs);
212
+ }
213
+ }
214
+ }
215
+ // Unreachable — the loop always either returns or throws.
216
+ throw lastErr;
217
+ }
218
+ /**
219
+ * Download every pinned file for the requested variant, skipping files whose
220
+ * on-disk SHA256 already matches the pin (unless `force` is set).
221
+ *
222
+ * Returns `{downloaded, skipped, totalBytes}` where `totalBytes` counts the
223
+ * newly-downloaded bytes only (skipped files are not re-hashed into the
224
+ * total).
225
+ */
226
+ export async function downloadEmbedderWeights(opts) {
227
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
228
+ if (typeof fetchImpl !== "function") {
229
+ throw new Error("Global fetch is not available. Node >= 18 required; supply opts.fetchImpl otherwise.");
230
+ }
231
+ const maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
232
+ const backoffMs = opts.backoffMs ?? DEFAULT_BACKOFF_MS;
233
+ const modelDir = resolveModelDir(opts.modelDir, opts.variant);
234
+ await mkdir(modelDir, { recursive: true });
235
+ const files = GTE_MODERNBERT_BASE_PINS[opts.variant].files;
236
+ let downloaded = 0;
237
+ let skipped = 0;
238
+ let totalBytes = 0;
239
+ for (let i = 0; i < files.length; i++) {
240
+ const pin = files[i];
241
+ const target = join(modelDir, pin.name);
242
+ const pct = Math.round((i / files.length) * 100);
243
+ opts.onProgress?.(pct, pin.name);
244
+ if (!opts.force) {
245
+ const existing = await hashFileIfExists(target);
246
+ if (existing === pin.sha256) {
247
+ skipped += 1;
248
+ continue;
249
+ }
250
+ }
251
+ // Ensure parent dir exists (target itself lives directly under modelDir
252
+ // so this is mostly belt-and-suspenders for unusual overrides).
253
+ await mkdir(dirname(target), { recursive: true });
254
+ const bytes = await withRetry(() => downloadOne(pin, target, fetchImpl), maxRetries, backoffMs);
255
+ downloaded += 1;
256
+ totalBytes += bytes;
257
+ }
258
+ opts.onProgress?.(100, "done");
259
+ return { downloaded, skipped, totalBytes, modelDir };
260
+ }
261
+ //# sourceMappingURL=embedder-downloader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedder-downloader.js","sourceRoot":"","sources":["../src/embedder-downloader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Thin async wrapper around `write-file-atomic` that encodes our invariants:
3
+ * - always UTF-8
4
+ * - always fsync the temp file before rename
5
+ * - always a trailing newline (makes diffs readable across editors)
6
+ *
7
+ * `write-file-atomic` already handles the pid/ts-suffixed temp file, fsync, and
8
+ * atomic rename. We centralize the import so the rest of the package depends on
9
+ * a single import shape — and so tests can swap in an in-memory implementation
10
+ * via dependency injection if they want.
11
+ */
12
+ export interface WriteAtomicOptions {
13
+ readonly mode?: number;
14
+ /** If true, do not add a trailing newline. Defaults to false. */
15
+ readonly raw?: boolean;
16
+ }
17
+ /**
18
+ * Write `contents` to `path` atomically. The parent directory must already
19
+ * exist — callers that cannot guarantee that should `mkdir -p` first.
20
+ */
21
+ export declare function writeFileAtomic(path: string, contents: string, opts?: WriteAtomicOptions): Promise<void>;
22
+ //# sourceMappingURL=fs-atomic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-atomic.d.ts","sourceRoot":"","sources":["../src/fs-atomic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAUf"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Thin async wrapper around `write-file-atomic` that encodes our invariants:
3
+ * - always UTF-8
4
+ * - always fsync the temp file before rename
5
+ * - always a trailing newline (makes diffs readable across editors)
6
+ *
7
+ * `write-file-atomic` already handles the pid/ts-suffixed temp file, fsync, and
8
+ * atomic rename. We centralize the import so the rest of the package depends on
9
+ * a single import shape — and so tests can swap in an in-memory implementation
10
+ * via dependency injection if they want.
11
+ */
12
+ import { default as wfa } from "write-file-atomic";
13
+ /**
14
+ * Write `contents` to `path` atomically. The parent directory must already
15
+ * exist — callers that cannot guarantee that should `mkdir -p` first.
16
+ */
17
+ export async function writeFileAtomic(path, contents, opts = {}) {
18
+ const payload = opts.raw === true || contents.endsWith("\n") ? contents : `${contents}\n`;
19
+ const writeOpts = {
20
+ encoding: "utf8",
21
+ fsync: true,
22
+ };
23
+ if (opts.mode !== undefined) {
24
+ writeOpts.mode = opts.mode;
25
+ }
26
+ await wfa(path, payload, writeOpts);
27
+ }
28
+ //# sourceMappingURL=fs-atomic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-atomic.js","sourceRoot":"","sources":["../src/fs-atomic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,IAAI,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAQnD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAY,EACZ,QAAgB,EAChB,OAA2B,EAAE;IAE7B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,KAAK,IAAI,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,IAAI,CAAC;IAC1F,MAAM,SAAS,GAAgE;QAC7E,QAAQ,EAAE,MAAM;QAChB,KAAK,EAAE,IAAI;KACZ,CAAC;IACF,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,SAAS,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IAC7B,CAAC;IACD,MAAM,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;AACtC,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Cross-repo group registry at `~/.codehub/groups/<name>.json`.
3
+ *
4
+ * A group is a named bundle of already-indexed repos. Each group lives in its
5
+ * own JSON file so that:
6
+ * - `write-file-atomic` rewrites of one group never serialize against another,
7
+ * - deletion is a single `unlink`, and
8
+ * - additive use (two users sharing `~/.codehub` via sync) sees minimal
9
+ * conflicts.
10
+ *
11
+ * The on-disk shape is intentionally minimal (name + createdAt + repos[] +
12
+ * optional description). Callers that need richer metadata should compose
13
+ * against `~/.codehub/registry.json` via `readRegistry` at call time — we do
14
+ * NOT duplicate per-repo stats into the group file.
15
+ *
16
+ * Determinism: `repos[]` is sorted by `name` on every write; on-disk JSON ends
17
+ * with a single trailing newline; writes go through `write-file-atomic`. Group
18
+ * names must be filesystem-safe (`[a-z0-9_-]+`) and are validated before any
19
+ * filesystem call.
20
+ */
21
+ /** Per-group subdirectory under `~/.codehub`. */
22
+ export declare const GROUPS_DIR_NAME = "groups";
23
+ /** Allowed group names. Matches a single path segment that is safe on macOS, Linux, and Windows. */
24
+ export declare const GROUP_NAME_PATTERN: RegExp;
25
+ export interface GroupRepo {
26
+ readonly name: string;
27
+ readonly path: string;
28
+ }
29
+ export interface GroupEntry {
30
+ readonly name: string;
31
+ readonly createdAt: string;
32
+ readonly repos: readonly GroupRepo[];
33
+ readonly description?: string;
34
+ }
35
+ export interface GroupsOptions {
36
+ /** Override `~` root used to locate `~/.codehub/groups` (tests pass a tmpdir). */
37
+ readonly home?: string;
38
+ }
39
+ /**
40
+ * Validate a group name up front so we never create filesystem paths from
41
+ * arbitrary user input. Throws on bad names.
42
+ */
43
+ export declare function assertValidGroupName(name: string): void;
44
+ /** Resolve the absolute path of the `groups/` directory. */
45
+ export declare function resolveGroupsDir(opts?: GroupsOptions): string;
46
+ /** Resolve the absolute path of a group's JSON file. */
47
+ export declare function resolveGroupFile(name: string, opts?: GroupsOptions): string;
48
+ /** Read a single group by name. Returns `null` when the file does not exist. */
49
+ export declare function readGroup(name: string, opts?: GroupsOptions): Promise<GroupEntry | null>;
50
+ /**
51
+ * Write a group to disk atomically. The `repos` array is sorted by `name` so
52
+ * byte-for-byte output is stable across invocations. Creates the parent
53
+ * directory when missing.
54
+ */
55
+ export declare function writeGroup(group: GroupEntry, opts?: GroupsOptions): Promise<void>;
56
+ /** Delete a group. Returns `true` if the file existed, `false` otherwise. */
57
+ export declare function deleteGroup(name: string, opts?: GroupsOptions): Promise<boolean>;
58
+ /**
59
+ * Enumerate every group under `~/.codehub/groups`. Returned entries are
60
+ * sorted by name. Unreadable/malformed files are skipped with an error logged
61
+ * to stderr rather than tearing down the caller.
62
+ */
63
+ export declare function listGroups(opts?: GroupsOptions): Promise<readonly GroupEntry[]>;
64
+ //# sourceMappingURL=groups.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../src/groups.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,iDAAiD;AACjD,eAAO,MAAM,eAAe,WAAW,CAAC;AAExC,oGAAoG;AACpG,eAAO,MAAM,kBAAkB,QAAkB,CAAC;AAElD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;IACrC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAOvD;AAED,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,aAAkB,GAAG,MAAM,CAGjE;AAED,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,CAG/E;AAED,gFAAgF;AAChF,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAY5B;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe3F;AAED,6EAA6E;AAC7E,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAU1F;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC,CA0BzF"}
package/dist/groups.js ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Cross-repo group registry at `~/.codehub/groups/<name>.json`.
3
+ *
4
+ * A group is a named bundle of already-indexed repos. Each group lives in its
5
+ * own JSON file so that:
6
+ * - `write-file-atomic` rewrites of one group never serialize against another,
7
+ * - deletion is a single `unlink`, and
8
+ * - additive use (two users sharing `~/.codehub` via sync) sees minimal
9
+ * conflicts.
10
+ *
11
+ * The on-disk shape is intentionally minimal (name + createdAt + repos[] +
12
+ * optional description). Callers that need richer metadata should compose
13
+ * against `~/.codehub/registry.json` via `readRegistry` at call time — we do
14
+ * NOT duplicate per-repo stats into the group file.
15
+ *
16
+ * Determinism: `repos[]` is sorted by `name` on every write; on-disk JSON ends
17
+ * with a single trailing newline; writes go through `write-file-atomic`. Group
18
+ * names must be filesystem-safe (`[a-z0-9_-]+`) and are validated before any
19
+ * filesystem call.
20
+ */
21
+ // biome-ignore-all lint/complexity/useLiteralKeys: dot-access disallowed on Record index signatures
22
+ import { mkdir, readdir, readFile, unlink } from "node:fs/promises";
23
+ import { homedir } from "node:os";
24
+ import { basename, dirname, extname, join, resolve } from "node:path";
25
+ import { writeFileAtomic } from "./fs-atomic.js";
26
+ import { CODEHUB_HOME_DIR } from "./registry.js";
27
+ /** Per-group subdirectory under `~/.codehub`. */
28
+ export const GROUPS_DIR_NAME = "groups";
29
+ /** Allowed group names. Matches a single path segment that is safe on macOS, Linux, and Windows. */
30
+ export const GROUP_NAME_PATTERN = /^[a-z0-9_-]+$/;
31
+ /**
32
+ * Validate a group name up front so we never create filesystem paths from
33
+ * arbitrary user input. Throws on bad names.
34
+ */
35
+ export function assertValidGroupName(name) {
36
+ if (!GROUP_NAME_PATTERN.test(name)) {
37
+ throw new Error(`Invalid group name "${name}". Names must match ${GROUP_NAME_PATTERN} ` +
38
+ "(lowercase letters, digits, underscore, hyphen).");
39
+ }
40
+ }
41
+ /** Resolve the absolute path of the `groups/` directory. */
42
+ export function resolveGroupsDir(opts = {}) {
43
+ const home = opts.home ?? homedir();
44
+ return resolve(home, CODEHUB_HOME_DIR, GROUPS_DIR_NAME);
45
+ }
46
+ /** Resolve the absolute path of a group's JSON file. */
47
+ export function resolveGroupFile(name, opts = {}) {
48
+ assertValidGroupName(name);
49
+ return join(resolveGroupsDir(opts), `${name}.json`);
50
+ }
51
+ /** Read a single group by name. Returns `null` when the file does not exist. */
52
+ export async function readGroup(name, opts = {}) {
53
+ assertValidGroupName(name);
54
+ const file = resolveGroupFile(name, opts);
55
+ let raw;
56
+ try {
57
+ raw = await readFile(file, "utf8");
58
+ }
59
+ catch (err) {
60
+ if (err.code === "ENOENT")
61
+ return null;
62
+ throw err;
63
+ }
64
+ const parsed = JSON.parse(raw);
65
+ return validateGroup(parsed, file);
66
+ }
67
+ /**
68
+ * Write a group to disk atomically. The `repos` array is sorted by `name` so
69
+ * byte-for-byte output is stable across invocations. Creates the parent
70
+ * directory when missing.
71
+ */
72
+ export async function writeGroup(group, opts = {}) {
73
+ assertValidGroupName(group.name);
74
+ const file = resolveGroupFile(group.name, opts);
75
+ await mkdir(dirname(file), { recursive: true });
76
+ const sortedRepos = [...group.repos]
77
+ .map((r) => ({ name: r.name, path: r.path }))
78
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
79
+ const payload = {
80
+ name: group.name,
81
+ createdAt: group.createdAt,
82
+ repos: sortedRepos,
83
+ ...(group.description !== undefined ? { description: group.description } : {}),
84
+ };
85
+ const serialized = `${JSON.stringify(payload, null, 2)}\n`;
86
+ await writeFileAtomic(file, serialized, { raw: true });
87
+ }
88
+ /** Delete a group. Returns `true` if the file existed, `false` otherwise. */
89
+ export async function deleteGroup(name, opts = {}) {
90
+ assertValidGroupName(name);
91
+ const file = resolveGroupFile(name, opts);
92
+ try {
93
+ await unlink(file);
94
+ return true;
95
+ }
96
+ catch (err) {
97
+ if (err.code === "ENOENT")
98
+ return false;
99
+ throw err;
100
+ }
101
+ }
102
+ /**
103
+ * Enumerate every group under `~/.codehub/groups`. Returned entries are
104
+ * sorted by name. Unreadable/malformed files are skipped with an error logged
105
+ * to stderr rather than tearing down the caller.
106
+ */
107
+ export async function listGroups(opts = {}) {
108
+ const dir = resolveGroupsDir(opts);
109
+ let dirents;
110
+ try {
111
+ dirents = await readdir(dir);
112
+ }
113
+ catch (err) {
114
+ if (err.code === "ENOENT")
115
+ return [];
116
+ throw err;
117
+ }
118
+ const names = dirents
119
+ .filter((f) => extname(f) === ".json")
120
+ .map((f) => basename(f, ".json"))
121
+ .filter((n) => GROUP_NAME_PATTERN.test(n))
122
+ .sort();
123
+ const out = [];
124
+ for (const name of names) {
125
+ try {
126
+ const group = await readGroup(name, opts);
127
+ if (group)
128
+ out.push(group);
129
+ }
130
+ catch (err) {
131
+ const msg = err instanceof Error ? err.message : String(err);
132
+ console.warn(`codehub groups: skipping malformed group "${name}": ${msg}`);
133
+ }
134
+ }
135
+ return out;
136
+ }
137
+ // ----------------------------------------------------------------------------
138
+ // Internal helpers
139
+ // ----------------------------------------------------------------------------
140
+ function validateGroup(value, file) {
141
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
142
+ throw new Error(`Invalid group at ${file}: expected top-level object`);
143
+ }
144
+ const raw = value;
145
+ if (typeof raw["name"] !== "string") {
146
+ throw new Error(`Invalid group at ${file}: missing "name"`);
147
+ }
148
+ if (typeof raw["createdAt"] !== "string") {
149
+ throw new Error(`Invalid group at ${file}: missing "createdAt"`);
150
+ }
151
+ if (!Array.isArray(raw["repos"])) {
152
+ throw new Error(`Invalid group at ${file}: missing "repos" array`);
153
+ }
154
+ const repos = [];
155
+ for (const entry of raw["repos"]) {
156
+ if (typeof entry !== "object" || entry === null)
157
+ continue;
158
+ const r = entry;
159
+ if (typeof r["name"] !== "string" || typeof r["path"] !== "string")
160
+ continue;
161
+ repos.push({ name: r["name"], path: r["path"] });
162
+ }
163
+ repos.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
164
+ const out = {
165
+ name: raw["name"],
166
+ createdAt: raw["createdAt"],
167
+ repos,
168
+ ...(typeof raw["description"] === "string" ? { description: raw["description"] } : {}),
169
+ };
170
+ return out;
171
+ }
172
+ //# sourceMappingURL=groups.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"groups.js","sourceRoot":"","sources":["../src/groups.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `codehub` CLI entrypoint.
4
+ *
5
+ * Every subcommand is loaded lazily via `await import(...)` so that
6
+ * `codehub --help` (and `codehub <command> --help`) stays fast: no DuckDB
7
+ * native binding, no pipeline, no MCP SDK unless we are actually going to
8
+ * run that subcommand.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG"}