@optique/git 0.9.0-dev.1

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 ADDED
@@ -0,0 +1,489 @@
1
+ import { message, text } from "@optique/core/message";
2
+ import { ensureNonEmptyString } from "@optique/core/nonempty";
3
+ import * as git from "isomorphic-git";
4
+ import { expandOid, listBranches, listRemotes, listTags, readObject, resolveRef } from "isomorphic-git";
5
+ import process from "node:process";
6
+
7
+ //#region src/index.ts
8
+ const METAVAR_BRANCH = "BRANCH";
9
+ const METAVAR_TAG = "TAG";
10
+ 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;
78
+ }
79
+ function createAsyncValueParser(options, metavar, parseFn, suggestFn) {
80
+ return {
81
+ $mode: "async",
82
+ metavar,
83
+ async parse(input) {
84
+ const fs = options?.fs ?? await getDefaultFs();
85
+ const dir = options?.dir ?? (typeof process !== "undefined" ? process.cwd() : ".");
86
+ ensureNonEmptyString(metavar);
87
+ return parseFn(fs, dir, input);
88
+ },
89
+ format(value) {
90
+ return value;
91
+ },
92
+ 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);
96
+ }
97
+ };
98
+ }
99
+ /**
100
+ * Creates a value parser that validates local branch names.
101
+ *
102
+ * This parser uses isomorphic-git to verify that the provided input
103
+ * matches an existing branch in the repository.
104
+ *
105
+ * @param options Configuration options for the parser.
106
+ * @returns A value parser that accepts existing branch names.
107
+ * @since 0.9.0
108
+ *
109
+ * @example
110
+ * ~~~~ typescript
111
+ * import { gitBranch } from "@optique/git";
112
+ * import { argument } from "@optique/core/primitives";
113
+ *
114
+ * const parser = argument(gitBranch());
115
+ * ~~~~
116
+ */
117
+ function gitBranch(options) {
118
+ const metavar = options?.metavar ?? METAVAR_BRANCH;
119
+ return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
120
+ try {
121
+ const branches = await git.listBranches({
122
+ fs,
123
+ dir
124
+ });
125
+ if (branches.includes(input)) return {
126
+ success: true,
127
+ value: input
128
+ };
129
+ return {
130
+ success: false,
131
+ error: message`Branch ${text(input)} does not exist. Available branches: ${branches.join(", ")}`
132
+ };
133
+ } catch {
134
+ return {
135
+ success: false,
136
+ error: message`Failed to list branches. Ensure ${text(dir)} is a valid git repository.`
137
+ };
138
+ }
139
+ }, async function* suggestBranch(fs, dir, prefix) {
140
+ try {
141
+ const branches = await git.listBranches({
142
+ fs,
143
+ dir
144
+ });
145
+ for (const branch of branches) if (branch.startsWith(prefix)) yield {
146
+ kind: "literal",
147
+ text: branch
148
+ };
149
+ } catch {}
150
+ });
151
+ }
152
+ /**
153
+ * Creates a value parser that validates remote branch names.
154
+ *
155
+ * This parser uses isomorphic-git to verify that the provided input
156
+ * matches an existing branch on the specified remote.
157
+ *
158
+ * @param remote The remote name to validate against.
159
+ * @param options Configuration options for the parser.
160
+ * @returns A value parser that accepts existing remote branch names.
161
+ * @since 0.9.0
162
+ *
163
+ * @example
164
+ * ~~~~ typescript
165
+ * import { gitRemoteBranch } from "@optique/git";
166
+ * import { option } from "@optique/core/primitives";
167
+ *
168
+ * const parser = option("-b", "--branch", gitRemoteBranch("origin"));
169
+ * ~~~~
170
+ */
171
+ function gitRemoteBranch(remote, options) {
172
+ const metavar = options?.metavar ?? METAVAR_BRANCH;
173
+ return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
174
+ try {
175
+ const branches = await git.listBranches({
176
+ fs,
177
+ dir,
178
+ remote
179
+ });
180
+ if (branches.includes(input)) return {
181
+ success: true,
182
+ value: input
183
+ };
184
+ return {
185
+ success: false,
186
+ error: message`Remote branch ${text(input)} does not exist on remote ${text(remote)}. Available branches: ${branches.join(", ")}`
187
+ };
188
+ } catch {
189
+ return {
190
+ success: false,
191
+ error: message`Failed to list remote branches. Ensure remote ${text(remote)} exists.`
192
+ };
193
+ }
194
+ }, async function* suggestRemoteBranch(fs, dir, prefix) {
195
+ try {
196
+ const branches = await git.listBranches({
197
+ fs,
198
+ dir,
199
+ remote
200
+ });
201
+ for (const branch of branches) if (branch.startsWith(prefix)) yield {
202
+ kind: "literal",
203
+ text: branch
204
+ };
205
+ } catch {}
206
+ });
207
+ }
208
+ /**
209
+ * Creates a value parser that validates tag names.
210
+ *
211
+ * This parser uses isomorphic-git to verify that the provided input
212
+ * matches an existing tag in the repository.
213
+ *
214
+ * @param options Configuration options for the parser.
215
+ * @returns A value parser that accepts existing tag names.
216
+ * @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
+ */
226
+ function gitTag(options) {
227
+ const metavar = options?.metavar ?? METAVAR_TAG;
228
+ return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
229
+ try {
230
+ const tags = await git.listTags({
231
+ fs,
232
+ dir
233
+ });
234
+ if (tags.includes(input)) return {
235
+ success: true,
236
+ value: input
237
+ };
238
+ return {
239
+ success: false,
240
+ error: message`Tag ${text(input)} does not exist. Available tags: ${tags.join(", ")}`
241
+ };
242
+ } catch {
243
+ return {
244
+ success: false,
245
+ error: message`Failed to list tags. Ensure ${text(dir)} is a valid git repository.`
246
+ };
247
+ }
248
+ }, async function* suggestTag(fs, dir, prefix) {
249
+ try {
250
+ const tags = await git.listTags({
251
+ fs,
252
+ dir
253
+ });
254
+ for (const tag of tags) if (tag.startsWith(prefix)) yield {
255
+ kind: "literal",
256
+ text: tag
257
+ };
258
+ } catch {}
259
+ });
260
+ }
261
+ /**
262
+ * Creates a value parser that validates remote names.
263
+ *
264
+ * This parser uses isomorphic-git to verify that the provided input
265
+ * matches an existing remote in the repository.
266
+ *
267
+ * @param options Configuration options for the parser.
268
+ * @returns A value parser that accepts existing remote names.
269
+ * @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
+ */
279
+ function gitRemote(options) {
280
+ const metavar = options?.metavar ?? METAVAR_REMOTE;
281
+ return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
282
+ try {
283
+ const remotes = await git.listRemotes({
284
+ fs,
285
+ dir
286
+ });
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 {
290
+ success: true,
291
+ value: input
292
+ };
293
+ return {
294
+ success: false,
295
+ error: message`Remote ${text(input)} does not exist. Available remotes: ${remoteNames.join(", ")}`
296
+ };
297
+ } catch {
298
+ return {
299
+ success: false,
300
+ error: message`Failed to list remotes. Ensure ${text(dir)} is a valid git repository.`
301
+ };
302
+ }
303
+ }, async function* suggestRemote(fs, dir, prefix) {
304
+ try {
305
+ const remotes = await git.listRemotes({
306
+ fs,
307
+ dir
308
+ });
309
+ for (const r of remotes) if ("remote" in r && typeof r.remote === "string" && r.remote.startsWith(prefix)) yield {
310
+ kind: "literal",
311
+ text: r.remote
312
+ };
313
+ } catch {}
314
+ });
315
+ }
316
+ /**
317
+ * Creates a value parser that validates commit SHAs.
318
+ *
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.
321
+ *
322
+ * @param options Configuration options for the parser.
323
+ * @returns A value parser that accepts valid commit SHAs.
324
+ * @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
+ */
334
+ function gitCommit(options) {
335
+ const metavar = options?.metavar ?? "COMMIT";
336
+ return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
337
+ try {
338
+ const oid = await git.expandOid({
339
+ fs,
340
+ dir,
341
+ oid: input
342
+ });
343
+ await git.readObject({
344
+ fs,
345
+ dir,
346
+ oid
347
+ });
348
+ return {
349
+ success: true,
350
+ value: oid
351
+ };
352
+ } catch {
353
+ return {
354
+ success: false,
355
+ error: message`Commit ${text(input)} does not exist. Provide a valid commit SHA.`
356
+ };
357
+ }
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
+ });
379
+ }
380
+ /**
381
+ * Creates a value parser that validates any git reference
382
+ * (branches, tags, or commits).
383
+ *
384
+ * This parser uses isomorphic-git to verify that the provided input
385
+ * resolves to a valid git reference (branch, tag, or commit SHA).
386
+ *
387
+ * @param options Configuration options for the parser.
388
+ * @returns A value parser that accepts branches, tags, or commit SHAs.
389
+ * @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
+ */
399
+ function gitRef(options) {
400
+ const metavar = options?.metavar ?? "REF";
401
+ return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
402
+ try {
403
+ const oid = await git.resolveRef({
404
+ fs,
405
+ dir,
406
+ ref: input
407
+ });
408
+ return {
409
+ success: true,
410
+ value: oid
411
+ };
412
+ } catch {
413
+ return {
414
+ success: false,
415
+ error: message`Reference ${text(input)} does not exist. Provide a valid branch, tag, or commit SHA.`
416
+ };
417
+ }
418
+ }, async function* suggestRef(fs, dir, prefix) {
419
+ try {
420
+ const branches = await git.listBranches({
421
+ fs,
422
+ dir
423
+ });
424
+ for (const branch of branches) if (branch.startsWith(prefix)) yield {
425
+ kind: "literal",
426
+ text: branch
427
+ };
428
+ const tags = await git.listTags({
429
+ fs,
430
+ dir
431
+ });
432
+ for (const tag of tags) if (tag.startsWith(prefix)) yield {
433
+ kind: "literal",
434
+ text: tag
435
+ };
436
+ } catch {}
437
+ });
438
+ }
439
+ /**
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).
444
+ *
445
+ * @param options Shared configuration options for all parsers.
446
+ * @returns An object with methods for creating individual git parsers.
447
+ * @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
+ */
459
+ function createGitParsers(options) {
460
+ 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
+ })
485
+ };
486
+ }
487
+
488
+ //#endregion
489
+ export { createGitParsers, expandOid, gitBranch, gitCommit, gitRef, gitRemote, gitRemoteBranch, gitTag, listBranches, listRemotes, listTags, readObject, resolveRef };
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@optique/git",
3
+ "version": "0.9.0-dev.1",
4
+ "description": "Git value parsers for Optique",
5
+ "keywords": [
6
+ "CLI",
7
+ "command-line",
8
+ "commandline",
9
+ "parser",
10
+ "git",
11
+ "git-reference",
12
+ "branch",
13
+ "tag",
14
+ "commit"
15
+ ],
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Hong Minhee",
19
+ "email": "hong@minhee.org",
20
+ "url": "https://hongminhee.org/"
21
+ },
22
+ "homepage": "https://optique.dev/",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/dahlia/optique.git",
26
+ "directory": "packages/git/"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/dahlia/optique/issues"
30
+ },
31
+ "funding": [
32
+ "https://github.com/sponsors/dahlia"
33
+ ],
34
+ "engines": {
35
+ "node": ">=20.0.0",
36
+ "bun": ">=1.2.0",
37
+ "deno": ">=2.3.0"
38
+ },
39
+ "files": [
40
+ "dist/",
41
+ "package.json",
42
+ "README.md"
43
+ ],
44
+ "type": "module",
45
+ "module": "./dist/index.js",
46
+ "main": "./dist/index.cjs",
47
+ "types": "./dist/index.d.ts",
48
+ "exports": {
49
+ ".": {
50
+ "types": {
51
+ "import": "./dist/index.d.ts",
52
+ "require": "./dist/index.d.cts"
53
+ },
54
+ "import": "./dist/index.js",
55
+ "require": "./dist/index.cjs"
56
+ }
57
+ },
58
+ "sideEffects": false,
59
+ "dependencies": {
60
+ "isomorphic-git": "^1.36.1",
61
+ "@optique/core": ""
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^20.19.9",
65
+ "tsdown": "^0.13.0",
66
+ "typescript": "^5.8.3"
67
+ },
68
+ "scripts": {
69
+ "build": "tsdown",
70
+ "prepublish": "tsdown",
71
+ "test": "tsdown && node --experimental-transform-types --test",
72
+ "test:bun": "tsdown && bun test",
73
+ "test:deno": "deno test",
74
+ "test-all": "tsdown && node --experimental-transform-types --test && bun test && deno test"
75
+ }
76
+ }