@optique/git 0.9.0-dev.266 → 0.9.0-dev.270

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.
package/dist/index.cjs CHANGED
@@ -26,31 +26,40 @@ const __optique_core_nonempty = __toESM(require("@optique/core/nonempty"));
26
26
  const isomorphic_git = __toESM(require("isomorphic-git"));
27
27
  const node_fs_promises = __toESM(require("node:fs/promises"));
28
28
  const node_process = __toESM(require("node:process"));
29
+ const __logtape_logtape = __toESM(require("@logtape/logtape"));
29
30
 
30
31
  //#region src/index.ts
32
+ const logger = (0, __logtape_logtape.getLogger)(["optique", "git"]);
31
33
  /**
32
- * Filesystem interface passed to isomorphic-git.
34
+ * Read-only filesystem interface passed to isomorphic-git.
33
35
  *
34
- * Although this package only performs read operations (validation and listing),
35
- * isomorphic-git's FsClient type requires write methods to be present.
36
- * These methods are included for type compatibility but are never called
37
- * by our read-only operations.
36
+ * This package only performs read operations (validation and listing).
37
+ * Write methods are implemented as stubs that return rejected promises,
38
+ * enforcing the read-only contract and preventing accidental writes.
38
39
  */
39
40
  const gitFs = {
40
41
  readFile: node_fs_promises.readFile,
41
- writeFile: node_fs_promises.writeFile,
42
- mkdir: node_fs_promises.mkdir,
43
- rmdir: node_fs_promises.rmdir,
44
- unlink: node_fs_promises.unlink,
42
+ writeFile: () => Promise.reject(/* @__PURE__ */ new Error("gitFs is read-only: writeFile is disabled.")),
43
+ mkdir: () => Promise.reject(/* @__PURE__ */ new Error("gitFs is read-only: mkdir is disabled.")),
44
+ rmdir: () => Promise.reject(/* @__PURE__ */ new Error("gitFs is read-only: rmdir is disabled.")),
45
+ unlink: () => Promise.reject(/* @__PURE__ */ new Error("gitFs is read-only: unlink is disabled.")),
45
46
  readdir: node_fs_promises.readdir,
46
47
  readlink: node_fs_promises.readlink,
47
- symlink: node_fs_promises.symlink,
48
+ symlink: () => Promise.reject(/* @__PURE__ */ new Error("gitFs is read-only: symlink is disabled.")),
48
49
  stat: node_fs_promises.stat,
49
50
  lstat: node_fs_promises.lstat
50
51
  };
51
52
  const METAVAR_BRANCH = "BRANCH";
52
53
  const METAVAR_TAG = "TAG";
53
54
  const METAVAR_REMOTE = "REMOTE";
55
+ /**
56
+ * Resolves the repository directory from the provided option or process.cwd().
57
+ *
58
+ * Note: This function does not validate that the directory exists or is
59
+ * accessible. Directory validation is deferred to the Git operations
60
+ * themselves, which will produce appropriate error messages if the directory
61
+ * is invalid or not a Git repository.
62
+ */
54
63
  function getRepoDir(dirOption) {
55
64
  if (dirOption != null) return dirOption;
56
65
  if (typeof node_process.default !== "undefined" && typeof node_process.default.cwd === "function") return node_process.default.cwd();
@@ -64,6 +73,8 @@ function listFailureMessage(error, dir, errors, fallback) {
64
73
  if (hasErrorCode(error, "NotAGitRepositoryError") || hasErrorCode(error, "NotFoundError")) return __optique_core_message.message`${(0, __optique_core_message.value)(dir)} is not a git repository.`;
65
74
  return fallback;
66
75
  }
76
+ /** Default depth for commit suggestions. */
77
+ const DEFAULT_SUGGESTION_DEPTH = 15;
67
78
  function createAsyncValueParser(options, metavar, parseFn, suggestFn) {
68
79
  (0, __optique_core_nonempty.ensureNonEmptyString)(metavar);
69
80
  return {
@@ -78,7 +89,10 @@ function createAsyncValueParser(options, metavar, parseFn, suggestFn) {
78
89
  },
79
90
  async *suggest(prefix) {
80
91
  const dir = getRepoDir(options?.dir);
81
- if (suggestFn) yield* suggestFn(dir, prefix);
92
+ if (suggestFn) {
93
+ const depth = options?.suggestionDepth ?? DEFAULT_SUGGESTION_DEPTH;
94
+ yield* suggestFn(dir, prefix, depth);
95
+ }
82
96
  }
83
97
  };
84
98
  }
@@ -127,7 +141,7 @@ function gitBranch(options) {
127
141
  error: listFailureMessage(error, dir, errors, fallback)
128
142
  };
129
143
  }
130
- }, async function* suggestBranch(dir, prefix) {
144
+ }, async function* suggestBranch(dir, prefix, _depth) {
131
145
  try {
132
146
  const branches = await isomorphic_git.listBranches({
133
147
  fs: gitFs,
@@ -137,7 +151,13 @@ function gitBranch(options) {
137
151
  kind: "literal",
138
152
  text: branch
139
153
  };
140
- } catch {}
154
+ } catch (error) {
155
+ logger.debug("Failed to list branches for suggestions.", {
156
+ dir,
157
+ prefix,
158
+ error
159
+ });
160
+ }
141
161
  });
142
162
  }
143
163
  /**
@@ -187,7 +207,7 @@ function gitRemoteBranch(remote, options) {
187
207
  error: listFailureMessage(error, dir, errors, fallback)
188
208
  };
189
209
  }
190
- }, async function* suggestRemoteBranch(dir, prefix) {
210
+ }, async function* suggestRemoteBranch(dir, prefix, _depth) {
191
211
  try {
192
212
  const branches = await isomorphic_git.listBranches({
193
213
  fs: gitFs,
@@ -198,7 +218,14 @@ function gitRemoteBranch(remote, options) {
198
218
  kind: "literal",
199
219
  text: branch
200
220
  };
201
- } catch {}
221
+ } catch (error) {
222
+ logger.debug("Failed to list remote branches for suggestions.", {
223
+ dir,
224
+ remote,
225
+ prefix,
226
+ error
227
+ });
228
+ }
202
229
  });
203
230
  }
204
231
  /**
@@ -235,7 +262,7 @@ function gitTag(options) {
235
262
  error: listFailureMessage(error, dir, errors, fallback)
236
263
  };
237
264
  }
238
- }, async function* suggestTag(dir, prefix) {
265
+ }, async function* suggestTag(dir, prefix, _depth) {
239
266
  try {
240
267
  const tags = await isomorphic_git.listTags({
241
268
  fs: gitFs,
@@ -245,7 +272,13 @@ function gitTag(options) {
245
272
  kind: "literal",
246
273
  text: tag
247
274
  };
248
- } catch {}
275
+ } catch (error) {
276
+ logger.debug("Failed to list tags for suggestions.", {
277
+ dir,
278
+ prefix,
279
+ error
280
+ });
281
+ }
249
282
  });
250
283
  }
251
284
  /**
@@ -283,7 +316,7 @@ function gitRemote(options) {
283
316
  error: listFailureMessage(error, dir, errors, fallback)
284
317
  };
285
318
  }
286
- }, async function* suggestRemote(dir, prefix) {
319
+ }, async function* suggestRemote(dir, prefix, _depth) {
287
320
  try {
288
321
  const remotes = await isomorphic_git.listRemotes({
289
322
  fs: gitFs,
@@ -293,7 +326,13 @@ function gitRemote(options) {
293
326
  kind: "literal",
294
327
  text: r.remote
295
328
  };
296
- } catch {}
329
+ } catch (error) {
330
+ logger.debug("Failed to list remotes for suggestions.", {
331
+ dir,
332
+ prefix,
333
+ error
334
+ });
335
+ }
297
336
  });
298
337
  }
299
338
  /**
@@ -316,7 +355,7 @@ function gitCommit(options) {
316
355
  };
317
356
  return {
318
357
  success: false,
319
- error: __optique_core_message.message`Invalid commit SHA: ${(0, __optique_core_message.value)(input)}. Commits must be 440 hexadecimal characters.`
358
+ error: __optique_core_message.message`Invalid commit SHA: ${(0, __optique_core_message.value)(input)}. Provide an abbreviated (4+) or full (40) hexadecimal commit SHA.`
320
359
  };
321
360
  }
322
361
  try {
@@ -329,7 +368,11 @@ function gitCommit(options) {
329
368
  success: true,
330
369
  value: oid
331
370
  };
332
- } catch {
371
+ } catch (e) {
372
+ if (hasErrorCode(e, "AmbiguousShortOidError")) return {
373
+ success: false,
374
+ error: __optique_core_message.message`Commit SHA ${(0, __optique_core_message.value)(input)} is ambiguous. Provide more characters to disambiguate.`
375
+ };
333
376
  if (errors?.notFound) return {
334
377
  success: false,
335
378
  error: errors.notFound(input)
@@ -339,12 +382,12 @@ function gitCommit(options) {
339
382
  error: __optique_core_message.message`Commit ${(0, __optique_core_message.value)(input)} does not exist. Provide a valid commit SHA.`
340
383
  };
341
384
  }
342
- }, async function* suggestCommit(dir, prefix) {
385
+ }, async function* suggestCommit(dir, prefix, depth) {
343
386
  try {
344
387
  const commits = await isomorphic_git.log({
345
388
  fs: gitFs,
346
389
  dir,
347
- depth: 15
390
+ depth
348
391
  });
349
392
  for (const commit of commits) if (commit.oid.startsWith(prefix)) {
350
393
  const shortOid = commit.oid.slice(0, 7);
@@ -355,7 +398,13 @@ function gitCommit(options) {
355
398
  description: __optique_core_message.message`${firstLine}`
356
399
  };
357
400
  }
358
- } catch {}
401
+ } catch (error) {
402
+ logger.debug("Failed to list commits for suggestions.", {
403
+ dir,
404
+ prefix,
405
+ error
406
+ });
407
+ }
359
408
  });
360
409
  }
361
410
  /**
@@ -393,7 +442,11 @@ function gitRef(options) {
393
442
  success: true,
394
443
  value: oid
395
444
  };
396
- } catch {
445
+ } catch (e) {
446
+ if (hasErrorCode(e, "AmbiguousShortOidError")) return {
447
+ success: false,
448
+ error: __optique_core_message.message`Reference ${(0, __optique_core_message.value)(input)} is ambiguous. Provide more characters to disambiguate.`
449
+ };
397
450
  if (errors?.notFound) return {
398
451
  success: false,
399
452
  error: errors.notFound(input)
@@ -403,7 +456,7 @@ function gitRef(options) {
403
456
  error: __optique_core_message.message`Reference ${(0, __optique_core_message.value)(input)} does not exist. Provide a valid branch, tag, or commit SHA.`
404
457
  };
405
458
  }
406
- }, async function* suggestRef(dir, prefix) {
459
+ }, async function* suggestRef(dir, prefix, depth) {
407
460
  try {
408
461
  const [branches, tags, commits] = await Promise.all([
409
462
  isomorphic_git.listBranches({
@@ -417,7 +470,7 @@ function gitRef(options) {
417
470
  isomorphic_git.log({
418
471
  fs: gitFs,
419
472
  dir,
420
- depth: 10
473
+ depth
421
474
  })
422
475
  ]);
423
476
  for (const branch of branches) if (branch.startsWith(prefix)) yield {
@@ -437,7 +490,13 @@ function gitRef(options) {
437
490
  description: __optique_core_message.message`${firstLine}`
438
491
  };
439
492
  }
440
- } catch {}
493
+ } catch (error) {
494
+ logger.debug("Failed to list refs for suggestions.", {
495
+ dir,
496
+ prefix,
497
+ error
498
+ });
499
+ }
441
500
  });
442
501
  }
443
502
  /**
package/dist/index.d.cts CHANGED
@@ -27,6 +27,14 @@ interface GitParserOptions {
27
27
  * @since 0.9.0
28
28
  */
29
29
  errors?: GitParserErrors;
30
+ /**
31
+ * Maximum number of recent commits to include in shell completion suggestions.
32
+ * Only applies to `gitCommit()` and `gitRef()` parsers.
33
+ * Defaults to 15.
34
+ *
35
+ * @since 0.9.0
36
+ */
37
+ suggestionDepth?: number;
30
38
  }
31
39
  /**
32
40
  * Custom error messages for git value parsers.
package/dist/index.d.ts CHANGED
@@ -27,6 +27,14 @@ interface GitParserOptions {
27
27
  * @since 0.9.0
28
28
  */
29
29
  errors?: GitParserErrors;
30
+ /**
31
+ * Maximum number of recent commits to include in shell completion suggestions.
32
+ * Only applies to `gitCommit()` and `gitRef()` parsers.
33
+ * Defaults to 15.
34
+ *
35
+ * @since 0.9.0
36
+ */
37
+ suggestionDepth?: number;
30
38
  }
31
39
  /**
32
40
  * Custom error messages for git value parsers.
package/dist/index.js CHANGED
@@ -4,31 +4,40 @@ import * as git from "isomorphic-git";
4
4
  import { expandOid, listBranches, listRemotes, listTags, readObject, resolveRef } from "isomorphic-git";
5
5
  import * as fs from "node:fs/promises";
6
6
  import process from "node:process";
7
+ import { getLogger } from "@logtape/logtape";
7
8
 
8
9
  //#region src/index.ts
10
+ const logger = getLogger(["optique", "git"]);
9
11
  /**
10
- * Filesystem interface passed to isomorphic-git.
12
+ * Read-only filesystem interface passed to isomorphic-git.
11
13
  *
12
- * Although this package only performs read operations (validation and listing),
13
- * isomorphic-git's FsClient type requires write methods to be present.
14
- * These methods are included for type compatibility but are never called
15
- * by our read-only operations.
14
+ * This package only performs read operations (validation and listing).
15
+ * Write methods are implemented as stubs that return rejected promises,
16
+ * enforcing the read-only contract and preventing accidental writes.
16
17
  */
17
18
  const gitFs = {
18
19
  readFile: fs.readFile,
19
- writeFile: fs.writeFile,
20
- mkdir: fs.mkdir,
21
- rmdir: fs.rmdir,
22
- unlink: fs.unlink,
20
+ writeFile: () => Promise.reject(/* @__PURE__ */ new Error("gitFs is read-only: writeFile is disabled.")),
21
+ mkdir: () => Promise.reject(/* @__PURE__ */ new Error("gitFs is read-only: mkdir is disabled.")),
22
+ rmdir: () => Promise.reject(/* @__PURE__ */ new Error("gitFs is read-only: rmdir is disabled.")),
23
+ unlink: () => Promise.reject(/* @__PURE__ */ new Error("gitFs is read-only: unlink is disabled.")),
23
24
  readdir: fs.readdir,
24
25
  readlink: fs.readlink,
25
- symlink: fs.symlink,
26
+ symlink: () => Promise.reject(/* @__PURE__ */ new Error("gitFs is read-only: symlink is disabled.")),
26
27
  stat: fs.stat,
27
28
  lstat: fs.lstat
28
29
  };
29
30
  const METAVAR_BRANCH = "BRANCH";
30
31
  const METAVAR_TAG = "TAG";
31
32
  const METAVAR_REMOTE = "REMOTE";
33
+ /**
34
+ * Resolves the repository directory from the provided option or process.cwd().
35
+ *
36
+ * Note: This function does not validate that the directory exists or is
37
+ * accessible. Directory validation is deferred to the Git operations
38
+ * themselves, which will produce appropriate error messages if the directory
39
+ * is invalid or not a Git repository.
40
+ */
32
41
  function getRepoDir(dirOption) {
33
42
  if (dirOption != null) return dirOption;
34
43
  if (typeof process !== "undefined" && typeof process.cwd === "function") return process.cwd();
@@ -42,6 +51,8 @@ function listFailureMessage(error, dir, errors, fallback) {
42
51
  if (hasErrorCode(error, "NotAGitRepositoryError") || hasErrorCode(error, "NotFoundError")) return message`${value(dir)} is not a git repository.`;
43
52
  return fallback;
44
53
  }
54
+ /** Default depth for commit suggestions. */
55
+ const DEFAULT_SUGGESTION_DEPTH = 15;
45
56
  function createAsyncValueParser(options, metavar, parseFn, suggestFn) {
46
57
  ensureNonEmptyString(metavar);
47
58
  return {
@@ -56,7 +67,10 @@ function createAsyncValueParser(options, metavar, parseFn, suggestFn) {
56
67
  },
57
68
  async *suggest(prefix) {
58
69
  const dir = getRepoDir(options?.dir);
59
- if (suggestFn) yield* suggestFn(dir, prefix);
70
+ if (suggestFn) {
71
+ const depth = options?.suggestionDepth ?? DEFAULT_SUGGESTION_DEPTH;
72
+ yield* suggestFn(dir, prefix, depth);
73
+ }
60
74
  }
61
75
  };
62
76
  }
@@ -105,7 +119,7 @@ function gitBranch(options) {
105
119
  error: listFailureMessage(error, dir, errors, fallback)
106
120
  };
107
121
  }
108
- }, async function* suggestBranch(dir, prefix) {
122
+ }, async function* suggestBranch(dir, prefix, _depth) {
109
123
  try {
110
124
  const branches = await git.listBranches({
111
125
  fs: gitFs,
@@ -115,7 +129,13 @@ function gitBranch(options) {
115
129
  kind: "literal",
116
130
  text: branch
117
131
  };
118
- } catch {}
132
+ } catch (error) {
133
+ logger.debug("Failed to list branches for suggestions.", {
134
+ dir,
135
+ prefix,
136
+ error
137
+ });
138
+ }
119
139
  });
120
140
  }
121
141
  /**
@@ -165,7 +185,7 @@ function gitRemoteBranch(remote, options) {
165
185
  error: listFailureMessage(error, dir, errors, fallback)
166
186
  };
167
187
  }
168
- }, async function* suggestRemoteBranch(dir, prefix) {
188
+ }, async function* suggestRemoteBranch(dir, prefix, _depth) {
169
189
  try {
170
190
  const branches = await git.listBranches({
171
191
  fs: gitFs,
@@ -176,7 +196,14 @@ function gitRemoteBranch(remote, options) {
176
196
  kind: "literal",
177
197
  text: branch
178
198
  };
179
- } catch {}
199
+ } catch (error) {
200
+ logger.debug("Failed to list remote branches for suggestions.", {
201
+ dir,
202
+ remote,
203
+ prefix,
204
+ error
205
+ });
206
+ }
180
207
  });
181
208
  }
182
209
  /**
@@ -213,7 +240,7 @@ function gitTag(options) {
213
240
  error: listFailureMessage(error, dir, errors, fallback)
214
241
  };
215
242
  }
216
- }, async function* suggestTag(dir, prefix) {
243
+ }, async function* suggestTag(dir, prefix, _depth) {
217
244
  try {
218
245
  const tags = await git.listTags({
219
246
  fs: gitFs,
@@ -223,7 +250,13 @@ function gitTag(options) {
223
250
  kind: "literal",
224
251
  text: tag
225
252
  };
226
- } catch {}
253
+ } catch (error) {
254
+ logger.debug("Failed to list tags for suggestions.", {
255
+ dir,
256
+ prefix,
257
+ error
258
+ });
259
+ }
227
260
  });
228
261
  }
229
262
  /**
@@ -261,7 +294,7 @@ function gitRemote(options) {
261
294
  error: listFailureMessage(error, dir, errors, fallback)
262
295
  };
263
296
  }
264
- }, async function* suggestRemote(dir, prefix) {
297
+ }, async function* suggestRemote(dir, prefix, _depth) {
265
298
  try {
266
299
  const remotes = await git.listRemotes({
267
300
  fs: gitFs,
@@ -271,7 +304,13 @@ function gitRemote(options) {
271
304
  kind: "literal",
272
305
  text: r.remote
273
306
  };
274
- } catch {}
307
+ } catch (error) {
308
+ logger.debug("Failed to list remotes for suggestions.", {
309
+ dir,
310
+ prefix,
311
+ error
312
+ });
313
+ }
275
314
  });
276
315
  }
277
316
  /**
@@ -294,7 +333,7 @@ function gitCommit(options) {
294
333
  };
295
334
  return {
296
335
  success: false,
297
- error: message`Invalid commit SHA: ${value(input)}. Commits must be 440 hexadecimal characters.`
336
+ error: message`Invalid commit SHA: ${value(input)}. Provide an abbreviated (4+) or full (40) hexadecimal commit SHA.`
298
337
  };
299
338
  }
300
339
  try {
@@ -307,7 +346,11 @@ function gitCommit(options) {
307
346
  success: true,
308
347
  value: oid
309
348
  };
310
- } catch {
349
+ } catch (e) {
350
+ if (hasErrorCode(e, "AmbiguousShortOidError")) return {
351
+ success: false,
352
+ error: message`Commit SHA ${value(input)} is ambiguous. Provide more characters to disambiguate.`
353
+ };
311
354
  if (errors?.notFound) return {
312
355
  success: false,
313
356
  error: errors.notFound(input)
@@ -317,12 +360,12 @@ function gitCommit(options) {
317
360
  error: message`Commit ${value(input)} does not exist. Provide a valid commit SHA.`
318
361
  };
319
362
  }
320
- }, async function* suggestCommit(dir, prefix) {
363
+ }, async function* suggestCommit(dir, prefix, depth) {
321
364
  try {
322
365
  const commits = await git.log({
323
366
  fs: gitFs,
324
367
  dir,
325
- depth: 15
368
+ depth
326
369
  });
327
370
  for (const commit of commits) if (commit.oid.startsWith(prefix)) {
328
371
  const shortOid = commit.oid.slice(0, 7);
@@ -333,7 +376,13 @@ function gitCommit(options) {
333
376
  description: message`${firstLine}`
334
377
  };
335
378
  }
336
- } catch {}
379
+ } catch (error) {
380
+ logger.debug("Failed to list commits for suggestions.", {
381
+ dir,
382
+ prefix,
383
+ error
384
+ });
385
+ }
337
386
  });
338
387
  }
339
388
  /**
@@ -371,7 +420,11 @@ function gitRef(options) {
371
420
  success: true,
372
421
  value: oid
373
422
  };
374
- } catch {
423
+ } catch (e) {
424
+ if (hasErrorCode(e, "AmbiguousShortOidError")) return {
425
+ success: false,
426
+ error: message`Reference ${value(input)} is ambiguous. Provide more characters to disambiguate.`
427
+ };
375
428
  if (errors?.notFound) return {
376
429
  success: false,
377
430
  error: errors.notFound(input)
@@ -381,7 +434,7 @@ function gitRef(options) {
381
434
  error: message`Reference ${value(input)} does not exist. Provide a valid branch, tag, or commit SHA.`
382
435
  };
383
436
  }
384
- }, async function* suggestRef(dir, prefix) {
437
+ }, async function* suggestRef(dir, prefix, depth) {
385
438
  try {
386
439
  const [branches, tags, commits] = await Promise.all([
387
440
  git.listBranches({
@@ -395,7 +448,7 @@ function gitRef(options) {
395
448
  git.log({
396
449
  fs: gitFs,
397
450
  dir,
398
- depth: 10
451
+ depth
399
452
  })
400
453
  ]);
401
454
  for (const branch of branches) if (branch.startsWith(prefix)) yield {
@@ -415,7 +468,13 @@ function gitRef(options) {
415
468
  description: message`${firstLine}`
416
469
  };
417
470
  }
418
- } catch {}
471
+ } catch (error) {
472
+ logger.debug("Failed to list refs for suggestions.", {
473
+ dir,
474
+ prefix,
475
+ error
476
+ });
477
+ }
419
478
  });
420
479
  }
421
480
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/git",
3
- "version": "0.9.0-dev.266+49a4cc82",
3
+ "version": "0.9.0-dev.270+b5894625",
4
4
  "description": "Git value parsers for Optique",
5
5
  "keywords": [
6
6
  "CLI",
@@ -57,8 +57,9 @@
57
57
  },
58
58
  "sideEffects": false,
59
59
  "dependencies": {
60
- "isomorphic-git": "^1.36.1",
61
- "@optique/core": ""
60
+ "@logtape/logtape": "^1.2.2",
61
+ "@optique/core": "",
62
+ "isomorphic-git": "^1.36.1"
62
63
  },
63
64
  "devDependencies": {
64
65
  "@types/node": "^20.19.9",