@oomfware/cgr 0.1.7 → 0.1.8

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/README.md CHANGED
@@ -42,10 +42,16 @@ cgr clean --all
42
42
  ## commands
43
43
 
44
44
  ```
45
- cgr ask [-m opus|sonnet|haiku] [-w repo[#branch] ...] <repo>[#branch] <question>
45
+ cgr ask [-m opus|sonnet|haiku] [-s] [-w repo[#branch] ...] <repo>[#branch] <question>
46
46
  cgr clean [--all | <repo>]
47
47
  ```
48
48
 
49
+ | option | description |
50
+ | --------------- | ------------------------------------------------- |
51
+ | `-m, --model` | model to use: opus, sonnet, haiku (default haiku) |
52
+ | `-s, --shallow` | use shallow clone (depth 1) for faster cloning |
53
+ | `-w, --with` | additional repository to include (repeatable) |
54
+
49
55
  | command | description |
50
56
  | ------- | ----------------------------------------------------------------- |
51
57
  | `ask` | clone/update a repository and ask Claude Code a question about it |
@@ -82,6 +88,7 @@ You can use `@oomfware/cgr` to ask questions about external repositories.
82
88
 
83
89
  options:
84
90
  -m, --model <model> model to use: opus, sonnet, haiku (default: haiku)
91
+ -s, --shallow use shallow clone (depth 1) for faster cloning
85
92
  -w, --with <repo> additional repository to include, supports #branch (repeatable)
86
93
 
87
94
  Useful repositories:
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { argument, choice, command, constant, message, object, option, or, string } from "@optique/core";
2
+ import { argument, choice, command, constant, flag, message, object, option, or, string } from "@optique/core";
3
3
  import { run } from "@optique/run";
4
4
  import { spawn } from "node:child_process";
5
5
  import { basename, dirname, join, relative } from "node:path";
@@ -102,14 +102,24 @@ const gitOutput = (args, cwd) => new Promise((resolve, reject) => {
102
102
  proc.on("error", reject);
103
103
  });
104
104
  /**
105
+ * checks if a repository is a shallow clone.
106
+ * @param cachePath the local repository path
107
+ * @returns true if the repository is shallow
108
+ */
109
+ const isShallowRepo = async (cachePath) => {
110
+ return await gitOutput(["rev-parse", "--is-shallow-repository"], cachePath) === "true";
111
+ };
112
+ /**
105
113
  * clones a repository to the cache path.
106
114
  * @param remote the remote URL
107
115
  * @param cachePath the local cache path
108
116
  * @param branch optional branch to checkout
117
+ * @param shallow if true, performs a shallow clone with depth 1
109
118
  */
110
- const cloneRepo = async (remote, cachePath, branch) => {
119
+ const cloneRepo = async (remote, cachePath, branch, shallow) => {
111
120
  await mkdir(dirname(cachePath), { recursive: true });
112
121
  const args = ["clone"];
122
+ if (shallow) args.push("--depth", "1");
113
123
  if (branch) args.push("--branch", branch);
114
124
  args.push(remote, cachePath);
115
125
  await git(args);
@@ -119,9 +129,23 @@ const cloneRepo = async (remote, cachePath, branch) => {
119
129
  * discards any local modifications, staged changes, and untracked files.
120
130
  * @param cachePath the local cache path
121
131
  * @param branch optional branch to checkout (uses default branch if not specified)
132
+ * @param shallow if true, keeps shallow clone; if false, unshallows if needed
122
133
  */
123
- const updateRepo = async (cachePath, branch) => {
124
- await git(["fetch", "origin"], cachePath);
134
+ const updateRepo = async (cachePath, branch, shallow) => {
135
+ const currentlyShallow = await isShallowRepo(cachePath);
136
+ if (!shallow && currentlyShallow) await git([
137
+ "fetch",
138
+ "--unshallow",
139
+ "origin"
140
+ ], cachePath);
141
+ else if (shallow && !currentlyShallow) console.error(" note: repository is already a full clone, use `cgr clean` to re-clone as shallow");
142
+ if (shallow && currentlyShallow) await git([
143
+ "fetch",
144
+ "--depth",
145
+ "1",
146
+ "origin"
147
+ ], cachePath);
148
+ else if (!currentlyShallow || !shallow) await git(["fetch", "origin"], cachePath);
125
149
  let targetBranch = branch;
126
150
  if (!targetBranch) targetBranch = (await gitOutput(["symbolic-ref", "refs/remotes/origin/HEAD"], cachePath)).replace("refs/remotes/origin/", "");
127
151
  await git([
@@ -143,13 +167,12 @@ const updateRepo = async (cachePath, branch) => {
143
167
  };
144
168
  /**
145
169
  * ensures a repository is cloned and up-to-date.
146
- * @param remote the remote URL
147
- * @param cachePath the local cache path
148
- * @param branch optional branch to checkout
170
+ * @param options repository options
149
171
  */
150
- const ensureRepo = async (remote, cachePath, branch) => {
151
- if (existsSync(cachePath)) await updateRepo(cachePath, branch);
152
- else await cloneRepo(remote, cachePath, branch);
172
+ const ensureRepo = async (options) => {
173
+ const { remote, cachePath, branch, shallow } = options;
174
+ if (existsSync(cachePath)) await updateRepo(cachePath, branch, shallow);
175
+ else await cloneRepo(remote, cachePath, branch, shallow);
153
176
  };
154
177
 
155
178
  //#endregion
@@ -315,6 +338,7 @@ const schema$1 = object({
315
338
  "sonnet",
316
339
  "haiku"
317
340
  ]), { description: message`model to use for analysis` }), "haiku"),
341
+ shallow: flag("-s", "--shallow", { description: message`use shallow clone (depth 1) to save time and disk space` }),
318
342
  branch: optional(option("-b", "--branch", string(), { description: message`branch to checkout (deprecated: use repo#branch instead)` })),
319
343
  with: withDefault(multiple(option("-w", "--with", string(), { description: message`additional repository to include` })), []),
320
344
  remote: argument(string({ metavar: "REPO" }), { description: message`git remote URL (HTTP/HTTPS/SSH), optionally with #branch` }),
@@ -420,7 +444,12 @@ const handler$1 = async (args) => {
420
444
  const remoteUrl = normalizeRemote(mainRepo.remote);
421
445
  console.error(`preparing repository: ${mainRepo.parsed.host}/${mainRepo.parsed.path}`);
422
446
  try {
423
- await ensureRepo(remoteUrl, mainRepo.cachePath, mainRepo.branch);
447
+ await ensureRepo({
448
+ remote: remoteUrl,
449
+ cachePath: mainRepo.cachePath,
450
+ branch: mainRepo.branch,
451
+ shallow: args.shallow
452
+ });
424
453
  } catch (err) {
425
454
  console.error(`error: failed to prepare repository: ${err}`);
426
455
  process.exit(1);
@@ -435,7 +464,12 @@ const handler$1 = async (args) => {
435
464
  const remoteUrl = normalizeRemote(repo.remote);
436
465
  const display = `${repo.parsed.host}/${repo.parsed.path}`;
437
466
  console.error(` preparing: ${display}`);
438
- await ensureRepo(remoteUrl, repo.cachePath, repo.branch);
467
+ await ensureRepo({
468
+ remote: remoteUrl,
469
+ cachePath: repo.cachePath,
470
+ branch: repo.branch,
471
+ shallow: args.shallow
472
+ });
439
473
  return repo;
440
474
  }));
441
475
  const failures = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oomfware/cgr",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "ask questions about git repositories using Claude Code",
5
5
  "license": "0BSD",
6
6
  "repository": {
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { join } from 'node:path';
3
3
 
4
- import { argument, choice, constant, type InferValue, message, object, option, string } from '@optique/core';
4
+ import { argument, choice, constant, flag, type InferValue, message, object, option, string } from '@optique/core';
5
5
  import { multiple, optional, withDefault } from '@optique/core/modifiers';
6
6
 
7
7
  import { ensureRepo } from '../lib/git.ts';
@@ -28,6 +28,9 @@ export const schema = object({
28
28
  }),
29
29
  'haiku',
30
30
  ),
31
+ shallow: flag('-s', '--shallow', {
32
+ description: message`use shallow clone (depth 1) to save time and disk space`,
33
+ }),
31
34
  // TODO: deprecated in favor of #branch syntax, remove in future version
32
35
  branch: optional(
33
36
  option('-b', '--branch', string(), {
@@ -168,7 +171,12 @@ export const handler = async (args: Args): Promise<void> => {
168
171
  const remoteUrl = normalizeRemote(mainRepo.remote);
169
172
  console.error(`preparing repository: ${mainRepo.parsed.host}/${mainRepo.parsed.path}`);
170
173
  try {
171
- await ensureRepo(remoteUrl, mainRepo.cachePath, mainRepo.branch);
174
+ await ensureRepo({
175
+ remote: remoteUrl,
176
+ cachePath: mainRepo.cachePath,
177
+ branch: mainRepo.branch,
178
+ shallow: args.shallow,
179
+ });
172
180
  } catch (err) {
173
181
  console.error(`error: failed to prepare repository: ${err}`);
174
182
  process.exit(1);
@@ -192,7 +200,12 @@ export const handler = async (args: Args): Promise<void> => {
192
200
  const remoteUrl = normalizeRemote(repo.remote);
193
201
  const display = `${repo.parsed.host}/${repo.parsed.path}`;
194
202
  console.error(` preparing: ${display}`);
195
- await ensureRepo(remoteUrl, repo.cachePath, repo.branch);
203
+ await ensureRepo({
204
+ remote: remoteUrl,
205
+ cachePath: repo.cachePath,
206
+ branch: repo.branch,
207
+ shallow: args.shallow,
208
+ });
196
209
  return repo;
197
210
  }),
198
211
  );
package/src/lib/git.ts CHANGED
@@ -85,15 +85,34 @@ const gitOutput = (args: string[], cwd?: string): Promise<string> =>
85
85
  proc.on('error', reject);
86
86
  });
87
87
 
88
+ /**
89
+ * checks if a repository is a shallow clone.
90
+ * @param cachePath the local repository path
91
+ * @returns true if the repository is shallow
92
+ */
93
+ export const isShallowRepo = async (cachePath: string): Promise<boolean> => {
94
+ const result = await gitOutput(['rev-parse', '--is-shallow-repository'], cachePath);
95
+ return result === 'true';
96
+ };
97
+
88
98
  /**
89
99
  * clones a repository to the cache path.
90
100
  * @param remote the remote URL
91
101
  * @param cachePath the local cache path
92
102
  * @param branch optional branch to checkout
103
+ * @param shallow if true, performs a shallow clone with depth 1
93
104
  */
94
- export const cloneRepo = async (remote: string, cachePath: string, branch?: string): Promise<void> => {
105
+ export const cloneRepo = async (
106
+ remote: string,
107
+ cachePath: string,
108
+ branch?: string,
109
+ shallow?: boolean,
110
+ ): Promise<void> => {
95
111
  await mkdir(dirname(cachePath), { recursive: true });
96
112
  const args = ['clone'];
113
+ if (shallow) {
114
+ args.push('--depth', '1');
115
+ }
97
116
  if (branch) {
98
117
  args.push('--branch', branch);
99
118
  }
@@ -106,9 +125,27 @@ export const cloneRepo = async (remote: string, cachePath: string, branch?: stri
106
125
  * discards any local modifications, staged changes, and untracked files.
107
126
  * @param cachePath the local cache path
108
127
  * @param branch optional branch to checkout (uses default branch if not specified)
128
+ * @param shallow if true, keeps shallow clone; if false, unshallows if needed
109
129
  */
110
- export const updateRepo = async (cachePath: string, branch?: string): Promise<void> => {
111
- await git(['fetch', 'origin'], cachePath);
130
+ export const updateRepo = async (cachePath: string, branch?: string, shallow?: boolean): Promise<void> => {
131
+ const currentlyShallow = await isShallowRepo(cachePath);
132
+
133
+ // handle shallow state transitions
134
+ if (!shallow && currentlyShallow) {
135
+ // want full history but have shallow - unshallow first
136
+ await git(['fetch', '--unshallow', 'origin'], cachePath);
137
+ } else if (shallow && !currentlyShallow) {
138
+ // want shallow but have full - just use the full clone as-is
139
+ console.error(' note: repository is already a full clone, use `cgr clean` to re-clone as shallow');
140
+ }
141
+
142
+ // fetch updates (use depth for shallow repos that stay shallow)
143
+ if (shallow && currentlyShallow) {
144
+ await git(['fetch', '--depth', '1', 'origin'], cachePath);
145
+ } else if (!currentlyShallow || !shallow) {
146
+ // either was unshallowed above, or is/was full
147
+ await git(['fetch', 'origin'], cachePath);
148
+ }
112
149
 
113
150
  // determine the branch to use
114
151
  let targetBranch = branch;
@@ -125,16 +162,26 @@ export const updateRepo = async (cachePath: string, branch?: string): Promise<vo
125
162
  await git(['reset', '--hard', `origin/${targetBranch}`], cachePath);
126
163
  };
127
164
 
165
+ export interface EnsureRepoOptions {
166
+ /** the remote URL */
167
+ remote: string;
168
+ /** the local cache path */
169
+ cachePath: string;
170
+ /** branch to checkout */
171
+ branch: string | undefined;
172
+ /** if true, uses shallow clone with depth 1 */
173
+ shallow: boolean;
174
+ }
175
+
128
176
  /**
129
177
  * ensures a repository is cloned and up-to-date.
130
- * @param remote the remote URL
131
- * @param cachePath the local cache path
132
- * @param branch optional branch to checkout
178
+ * @param options repository options
133
179
  */
134
- export const ensureRepo = async (remote: string, cachePath: string, branch?: string): Promise<void> => {
180
+ export const ensureRepo = async (options: EnsureRepoOptions): Promise<void> => {
181
+ const { remote, cachePath, branch, shallow } = options;
135
182
  if (existsSync(cachePath)) {
136
- await updateRepo(cachePath, branch);
183
+ await updateRepo(cachePath, branch, shallow);
137
184
  } else {
138
- await cloneRepo(remote, cachePath, branch);
185
+ await cloneRepo(remote, cachePath, branch, shallow);
139
186
  }
140
187
  };