@optique/git 0.9.0-dev.215 → 0.9.0-dev.227

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.js CHANGED
@@ -2,97 +2,51 @@ import { message, text } from "@optique/core/message";
2
2
  import { ensureNonEmptyString } from "@optique/core/nonempty";
3
3
  import * as git from "isomorphic-git";
4
4
  import { expandOid, listBranches, listRemotes, listTags, readObject, resolveRef } from "isomorphic-git";
5
+ import * as fs from "node:fs/promises";
5
6
  import process from "node:process";
6
7
 
7
8
  //#region src/index.ts
9
+ const gitFs = {
10
+ readFile: fs.readFile,
11
+ writeFile: fs.writeFile,
12
+ mkdir: fs.mkdir,
13
+ rmdir: fs.rmdir,
14
+ unlink: fs.unlink,
15
+ readdir: fs.readdir,
16
+ readlink: fs.readlink,
17
+ symlink: fs.symlink,
18
+ stat: fs.stat,
19
+ lstat: fs.lstat
20
+ };
8
21
  const METAVAR_BRANCH = "BRANCH";
9
22
  const METAVAR_TAG = "TAG";
10
23
  const METAVAR_REMOTE = "REMOTE";
11
- let defaultFs = null;
12
- let fsLoading = null;
13
- async function getDefaultFs() {
14
- if (defaultFs) return await defaultFs;
15
- if (fsLoading) return await fsLoading;
16
- fsLoading = (async () => {
17
- const nodeFs = await import("node:fs/promises");
18
- const { TextDecoder } = await import("node:util");
19
- const decoder = new TextDecoder();
20
- defaultFs = {
21
- async readFile(path) {
22
- const data = await nodeFs.readFile(path);
23
- if (path.endsWith("/index") || path.endsWith(".idx")) return data;
24
- return decoder.decode(data);
25
- },
26
- async writeFile(path, data) {
27
- await nodeFs.writeFile(path, data);
28
- },
29
- async mkdir(path, options) {
30
- await nodeFs.mkdir(path, options);
31
- },
32
- async rmdir(path, options) {
33
- await nodeFs.rmdir(path, options);
34
- },
35
- async unlink(path) {
36
- await nodeFs.unlink(path);
37
- },
38
- async readdir(path) {
39
- const entries = await nodeFs.readdir(path, { withFileTypes: false });
40
- return entries.filter((e) => typeof e === "string");
41
- },
42
- async lstat(path) {
43
- return await nodeFs.lstat(path);
44
- },
45
- async stat(path) {
46
- return await nodeFs.stat(path);
47
- },
48
- async readlink(path) {
49
- return await nodeFs.readlink(path);
50
- },
51
- async symlink(target, path) {
52
- await nodeFs.symlink(target, path, "file");
53
- },
54
- async chmod(path, mode) {
55
- await nodeFs.chmod(path, mode);
56
- },
57
- async chown(path, uid, gid) {
58
- await nodeFs.chown(path, uid, gid);
59
- },
60
- async rename(oldPath, newPath) {
61
- await nodeFs.rename(oldPath, newPath);
62
- },
63
- async copyFile(srcPath, destPath) {
64
- await nodeFs.copyFile(srcPath, destPath);
65
- },
66
- async exists(path) {
67
- try {
68
- await nodeFs.stat(path);
69
- return true;
70
- } catch {
71
- return false;
72
- }
73
- }
74
- };
75
- return defaultFs;
76
- })();
77
- return fsLoading;
24
+ function getRepoDir(dirOption) {
25
+ return dirOption ?? (typeof process !== "undefined" ? process.cwd() : ".");
26
+ }
27
+ function formatChoiceList(choices) {
28
+ let result = [];
29
+ for (let i = 0; i < choices.length; i++) {
30
+ if (i > 0) result = [...result, ...message`, `];
31
+ result = [...result, ...message`${choices[i]}`];
32
+ }
33
+ return result;
78
34
  }
79
35
  function createAsyncValueParser(options, metavar, parseFn, suggestFn) {
80
36
  return {
81
37
  $mode: "async",
82
38
  metavar,
83
- async parse(input) {
84
- const fs = options?.fs ?? await getDefaultFs();
85
- const dir = options?.dir ?? (typeof process !== "undefined" ? process.cwd() : ".");
39
+ parse(input) {
40
+ const dir = getRepoDir(options?.dir);
86
41
  ensureNonEmptyString(metavar);
87
- return parseFn(fs, dir, input);
42
+ return parseFn(dir, input, options?.errors);
88
43
  },
89
44
  format(value) {
90
45
  return value;
91
46
  },
92
47
  async *suggest(prefix) {
93
- const fs = options?.fs ?? await getDefaultFs();
94
- const dir = options?.dir ?? (typeof process !== "undefined" ? process.cwd() : ".");
95
- if (suggestFn) yield* suggestFn(fs, dir, prefix);
48
+ const dir = getRepoDir(options?.dir);
49
+ if (suggestFn) yield* suggestFn(dir, prefix);
96
50
  }
97
51
  };
98
52
  }
@@ -116,30 +70,38 @@ function createAsyncValueParser(options, metavar, parseFn, suggestFn) {
116
70
  */
117
71
  function gitBranch(options) {
118
72
  const metavar = options?.metavar ?? METAVAR_BRANCH;
119
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
73
+ return createAsyncValueParser(options, metavar, async (dir, input, errors) => {
120
74
  try {
121
75
  const branches = await git.listBranches({
122
- fs,
76
+ fs: gitFs,
123
77
  dir
124
78
  });
125
79
  if (branches.includes(input)) return {
126
80
  success: true,
127
81
  value: input
128
82
  };
83
+ if (errors?.notFound) return {
84
+ success: false,
85
+ error: errors.notFound(input, branches)
86
+ };
129
87
  return {
130
88
  success: false,
131
- error: message`Branch ${text(input)} does not exist. Available branches: ${branches.join(", ")}`
89
+ error: message`Branch ${text(input)} does not exist. Available branches: ${formatChoiceList(branches)}`
132
90
  };
133
91
  } catch {
92
+ if (errors?.listFailed) return {
93
+ success: false,
94
+ error: errors.listFailed(dir)
95
+ };
134
96
  return {
135
97
  success: false,
136
98
  error: message`Failed to list branches. Ensure ${text(dir)} is a valid git repository.`
137
99
  };
138
100
  }
139
- }, async function* suggestBranch(fs, dir, prefix) {
101
+ }, async function* suggestBranch(dir, prefix) {
140
102
  try {
141
103
  const branches = await git.listBranches({
142
- fs,
104
+ fs: gitFs,
143
105
  dir
144
106
  });
145
107
  for (const branch of branches) if (branch.startsWith(prefix)) yield {
@@ -170,10 +132,10 @@ function gitBranch(options) {
170
132
  */
171
133
  function gitRemoteBranch(remote, options) {
172
134
  const metavar = options?.metavar ?? METAVAR_BRANCH;
173
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
135
+ return createAsyncValueParser(options, metavar, async (dir, input, errors) => {
174
136
  try {
175
137
  const branches = await git.listBranches({
176
- fs,
138
+ fs: gitFs,
177
139
  dir,
178
140
  remote
179
141
  });
@@ -181,20 +143,28 @@ function gitRemoteBranch(remote, options) {
181
143
  success: true,
182
144
  value: input
183
145
  };
146
+ if (errors?.notFound) return {
147
+ success: false,
148
+ error: errors.notFound(input, branches)
149
+ };
184
150
  return {
185
151
  success: false,
186
- error: message`Remote branch ${text(input)} does not exist on remote ${text(remote)}. Available branches: ${branches.join(", ")}`
152
+ error: message`Remote branch ${text(input)} does not exist on remote ${text(remote)}. Available branches: ${formatChoiceList(branches)}`
187
153
  };
188
154
  } catch {
155
+ if (errors?.listFailed) return {
156
+ success: false,
157
+ error: errors.listFailed(dir)
158
+ };
189
159
  return {
190
160
  success: false,
191
161
  error: message`Failed to list remote branches. Ensure remote ${text(remote)} exists.`
192
162
  };
193
163
  }
194
- }, async function* suggestRemoteBranch(fs, dir, prefix) {
164
+ }, async function* suggestRemoteBranch(dir, prefix) {
195
165
  try {
196
166
  const branches = await git.listBranches({
197
- fs,
167
+ fs: gitFs,
198
168
  dir,
199
169
  remote
200
170
  });
@@ -208,47 +178,44 @@ function gitRemoteBranch(remote, options) {
208
178
  /**
209
179
  * Creates a value parser that validates tag names.
210
180
  *
211
- * This parser uses isomorphic-git to verify that the provided input
212
- * matches an existing tag in the repository.
213
- *
214
181
  * @param options Configuration options for the parser.
215
182
  * @returns A value parser that accepts existing tag names.
216
183
  * @since 0.9.0
217
- *
218
- * @example
219
- * ~~~~ typescript
220
- * import { gitTag } from "@optique/git";
221
- * import { option } from "@optique/core/primitives";
222
- *
223
- * const parser = option("-t", "--tag", gitTag());
224
- * ~~~~
225
184
  */
226
185
  function gitTag(options) {
227
186
  const metavar = options?.metavar ?? METAVAR_TAG;
228
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
187
+ return createAsyncValueParser(options, metavar, async (dir, input, errors) => {
229
188
  try {
230
189
  const tags = await git.listTags({
231
- fs,
190
+ fs: gitFs,
232
191
  dir
233
192
  });
234
193
  if (tags.includes(input)) return {
235
194
  success: true,
236
195
  value: input
237
196
  };
197
+ if (errors?.notFound) return {
198
+ success: false,
199
+ error: errors.notFound(input, tags)
200
+ };
238
201
  return {
239
202
  success: false,
240
- error: message`Tag ${text(input)} does not exist. Available tags: ${tags.join(", ")}`
203
+ error: message`Tag ${text(input)} does not exist. Available tags: ${formatChoiceList(tags)}`
241
204
  };
242
205
  } catch {
206
+ if (errors?.listFailed) return {
207
+ success: false,
208
+ error: errors.listFailed(dir)
209
+ };
243
210
  return {
244
211
  success: false,
245
212
  error: message`Failed to list tags. Ensure ${text(dir)} is a valid git repository.`
246
213
  };
247
214
  }
248
- }, async function* suggestTag(fs, dir, prefix) {
215
+ }, async function* suggestTag(dir, prefix) {
249
216
  try {
250
217
  const tags = await git.listTags({
251
- fs,
218
+ fs: gitFs,
252
219
  dir
253
220
  });
254
221
  for (const tag of tags) if (tag.startsWith(prefix)) yield {
@@ -261,52 +228,48 @@ function gitTag(options) {
261
228
  /**
262
229
  * Creates a value parser that validates remote names.
263
230
  *
264
- * This parser uses isomorphic-git to verify that the provided input
265
- * matches an existing remote in the repository.
266
- *
267
231
  * @param options Configuration options for the parser.
268
232
  * @returns A value parser that accepts existing remote names.
269
233
  * @since 0.9.0
270
- *
271
- * @example
272
- * ~~~~ typescript
273
- * import { gitRemote } from "@optique/git";
274
- * import { option } from "@optique/core/primitives";
275
- *
276
- * const parser = option("-r", "--remote", gitRemote());
277
- * ~~~~
278
234
  */
279
235
  function gitRemote(options) {
280
236
  const metavar = options?.metavar ?? METAVAR_REMOTE;
281
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
237
+ return createAsyncValueParser(options, metavar, async (dir, input, errors) => {
282
238
  try {
283
239
  const remotes = await git.listRemotes({
284
- fs,
240
+ fs: gitFs,
285
241
  dir
286
242
  });
287
- const remoteNames = [];
288
- for (const r of remotes) if ("remote" in r && typeof r.remote === "string") remoteNames.push(r.remote);
289
- if (remoteNames.includes(input)) return {
243
+ const names = remotes.map((r) => r.remote);
244
+ if (names.includes(input)) return {
290
245
  success: true,
291
246
  value: input
292
247
  };
248
+ if (errors?.notFound) return {
249
+ success: false,
250
+ error: errors.notFound(input, names)
251
+ };
293
252
  return {
294
253
  success: false,
295
- error: message`Remote ${text(input)} does not exist. Available remotes: ${remoteNames.join(", ")}`
254
+ error: message`Remote ${text(input)} does not exist. Available remotes: ${formatChoiceList(names)}`
296
255
  };
297
256
  } catch {
257
+ if (errors?.listFailed) return {
258
+ success: false,
259
+ error: errors.listFailed(dir)
260
+ };
298
261
  return {
299
262
  success: false,
300
263
  error: message`Failed to list remotes. Ensure ${text(dir)} is a valid git repository.`
301
264
  };
302
265
  }
303
- }, async function* suggestRemote(fs, dir, prefix) {
266
+ }, async function* suggestRemote(dir, prefix) {
304
267
  try {
305
268
  const remotes = await git.listRemotes({
306
- fs,
269
+ fs: gitFs,
307
270
  dir
308
271
  });
309
- for (const r of remotes) if ("remote" in r && typeof r.remote === "string" && r.remote.startsWith(prefix)) yield {
272
+ for (const r of remotes) if (r.remote.startsWith(prefix)) yield {
310
273
  kind: "literal",
311
274
  text: r.remote
312
275
  };
@@ -316,119 +279,119 @@ function gitRemote(options) {
316
279
  /**
317
280
  * Creates a value parser that validates commit SHAs.
318
281
  *
319
- * This parser uses isomorphic-git to verify that the provided input
320
- * is a valid commit SHA (full or shortened) that exists in the repository.
282
+ * This parser resolves the provided commit reference to its full 40-character
283
+ * OID.
321
284
  *
322
285
  * @param options Configuration options for the parser.
323
- * @returns A value parser that accepts valid commit SHAs.
286
+ * @returns A value parser that accepts existing commit SHAs.
324
287
  * @since 0.9.0
325
- *
326
- * @example
327
- * ~~~~ typescript
328
- * import { gitCommit } from "@optique/git";
329
- * import { option } from "@optique/core/primitives";
330
- *
331
- * const parser = option("-c", "--commit", gitCommit());
332
- * ~~~~
333
288
  */
334
289
  function gitCommit(options) {
335
290
  const metavar = options?.metavar ?? "COMMIT";
336
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
291
+ return createAsyncValueParser(options, metavar, async (dir, input, errors) => {
292
+ try {
293
+ ensureNonEmptyString(input);
294
+ } catch {
295
+ if (errors?.invalidFormat) return {
296
+ success: false,
297
+ error: errors.invalidFormat(input)
298
+ };
299
+ return {
300
+ success: false,
301
+ error: message`Invalid commit SHA: ${text(input)}`
302
+ };
303
+ }
304
+ if (input.length < 4 || input.length > 40) {
305
+ if (errors?.invalidFormat) return {
306
+ success: false,
307
+ error: errors.invalidFormat(input)
308
+ };
309
+ return {
310
+ success: false,
311
+ error: message`Commit ${text(input)} must be between 4 and 40 characters.`
312
+ };
313
+ }
337
314
  try {
338
315
  const oid = await git.expandOid({
339
- fs,
316
+ fs: gitFs,
340
317
  dir,
341
318
  oid: input
342
319
  });
343
- await git.readObject({
344
- fs,
345
- dir,
346
- oid
347
- });
348
320
  return {
349
321
  success: true,
350
322
  value: oid
351
323
  };
352
324
  } catch {
325
+ if (errors?.notFound) return {
326
+ success: false,
327
+ error: errors.notFound(input)
328
+ };
353
329
  return {
354
330
  success: false,
355
331
  error: message`Commit ${text(input)} does not exist. Provide a valid commit SHA.`
356
332
  };
357
333
  }
358
- }, async function* suggestCommit(fs, dir, prefix) {
359
- try {
360
- const branches = await git.listBranches({
361
- fs,
362
- dir
363
- });
364
- const commits = [];
365
- for (const branch of branches.slice(0, 10)) try {
366
- const oid = await git.resolveRef({
367
- fs,
368
- dir,
369
- ref: branch
370
- });
371
- if (oid.startsWith(prefix)) commits.push(oid);
372
- } catch {}
373
- for (const commit of [...new Set(commits)].slice(0, 10)) yield {
374
- kind: "literal",
375
- text: commit
376
- };
377
- } catch {}
378
334
  });
379
335
  }
380
336
  /**
381
- * Creates a value parser that validates any git reference
382
- * (branches, tags, or commits).
337
+ * Creates a value parser that validates any git reference.
383
338
  *
384
- * This parser uses isomorphic-git to verify that the provided input
385
- * resolves to a valid git reference (branch, tag, or commit SHA).
339
+ * Accepts branch names, tag names, or commit SHAs and resolves them to the
340
+ * corresponding commit OID.
386
341
  *
387
342
  * @param options Configuration options for the parser.
388
- * @returns A value parser that accepts branches, tags, or commit SHAs.
343
+ * @returns A value parser that accepts any git reference.
389
344
  * @since 0.9.0
390
- *
391
- * @example
392
- * ~~~~ typescript
393
- * import { gitRef } from "@optique/git";
394
- * import { option } from "@optique/core/primitives";
395
- *
396
- * const parser = option("--ref", gitRef());
397
- * ~~~~
398
345
  */
399
346
  function gitRef(options) {
400
347
  const metavar = options?.metavar ?? "REF";
401
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
348
+ return createAsyncValueParser(options, metavar, async (dir, input, errors) => {
402
349
  try {
403
- const oid = await git.resolveRef({
404
- fs,
350
+ const resolved = await git.resolveRef({
351
+ fs: gitFs,
405
352
  dir,
406
353
  ref: input
407
354
  });
408
355
  return {
409
356
  success: true,
410
- value: oid
357
+ value: resolved
411
358
  };
412
359
  } catch {
413
- return {
414
- success: false,
415
- error: message`Reference ${text(input)} does not exist. Provide a valid branch, tag, or commit SHA.`
416
- };
360
+ try {
361
+ const oid = await git.expandOid({
362
+ fs: gitFs,
363
+ dir,
364
+ oid: input
365
+ });
366
+ return {
367
+ success: true,
368
+ value: oid
369
+ };
370
+ } catch {
371
+ if (errors?.notFound) return {
372
+ success: false,
373
+ error: errors.notFound(input)
374
+ };
375
+ return {
376
+ success: false,
377
+ error: message`Reference ${text(input)} does not exist. Provide a valid branch, tag, or commit SHA.`
378
+ };
379
+ }
417
380
  }
418
- }, async function* suggestRef(fs, dir, prefix) {
381
+ }, async function* suggestRef(dir, prefix) {
419
382
  try {
420
383
  const branches = await git.listBranches({
421
- fs,
384
+ fs: gitFs,
385
+ dir
386
+ });
387
+ const tags = await git.listTags({
388
+ fs: gitFs,
422
389
  dir
423
390
  });
424
391
  for (const branch of branches) if (branch.startsWith(prefix)) yield {
425
392
  kind: "literal",
426
393
  text: branch
427
394
  };
428
- const tags = await git.listTags({
429
- fs,
430
- dir
431
- });
432
395
  for (const tag of tags) if (tag.startsWith(prefix)) yield {
433
396
  kind: "literal",
434
397
  text: tag
@@ -437,51 +400,20 @@ function gitRef(options) {
437
400
  });
438
401
  }
439
402
  /**
440
- * Creates a factory for git parsers with shared configuration.
441
- *
442
- * This function returns an object with methods for creating individual git
443
- * parsers that share the same configuration (filesystem and directory).
403
+ * Creates a set of git parsers with shared configuration.
444
404
  *
445
- * @param options Shared configuration options for all parsers.
446
- * @returns An object with methods for creating individual git parsers.
405
+ * @param options Shared configuration for the parsers.
406
+ * @returns An object containing git parsers.
447
407
  * @since 0.9.0
448
- *
449
- * @example
450
- * ~~~~ typescript
451
- * import { createGitParsers } from "@optique/git";
452
- *
453
- * const git = createGitParsers({ dir: "/path/to/repo" });
454
- *
455
- * const branchParser = git.branch();
456
- * const tagParser = git.tag();
457
- * ~~~~
458
408
  */
459
409
  function createGitParsers(options) {
460
410
  return {
461
- branch: (parserOptions) => gitBranch({
462
- ...options,
463
- ...parserOptions
464
- }),
465
- remoteBranch: (remote, parserOptions) => gitRemoteBranch(remote, {
466
- ...options,
467
- ...parserOptions
468
- }),
469
- tag: (parserOptions) => gitTag({
470
- ...options,
471
- ...parserOptions
472
- }),
473
- remote: (parserOptions) => gitRemote({
474
- ...options,
475
- ...parserOptions
476
- }),
477
- commit: (parserOptions) => gitCommit({
478
- ...options,
479
- ...parserOptions
480
- }),
481
- ref: (parserOptions) => gitRef({
482
- ...options,
483
- ...parserOptions
484
- })
411
+ branch: (branchOptions) => gitBranch(branchOptions ?? options),
412
+ remoteBranch: (remote, branchOptions) => gitRemoteBranch(remote, branchOptions ?? options),
413
+ tag: (tagOptions) => gitTag(tagOptions ?? options),
414
+ remote: (remoteOptions) => gitRemote(remoteOptions ?? options),
415
+ commit: (commitOptions) => gitCommit(commitOptions ?? options),
416
+ ref: (refOptions) => gitRef(refOptions ?? options)
485
417
  };
486
418
  }
487
419
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/git",
3
- "version": "0.9.0-dev.215+13b302c0",
3
+ "version": "0.9.0-dev.227+a31da310",
4
4
  "description": "Git value parsers for Optique",
5
5
  "keywords": [
6
6
  "CLI",