@ghl-ai/aw 0.1.39 → 0.1.40-beta.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 (2) hide show
  1. package/git.mjs +101 -19
  2. package/package.json +1 -1
package/git.mjs CHANGED
@@ -9,6 +9,49 @@ import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR, RULES_SOURCE_DIR }
9
9
 
10
10
  const exec = promisify(execCb);
11
11
 
12
+ /**
13
+ * Env vars applied to every git command that touches the network.
14
+ * GIT_TERMINAL_PROMPT=0 prevents git from hanging when it would otherwise
15
+ * prompt for credentials (e.g. HTTPS URL with SSH-only auth configured).
16
+ * Instead, git exits immediately with a non-zero code that we can catch.
17
+ */
18
+ const GIT_NET_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
19
+
20
+ /**
21
+ * Convert an HTTPS GitHub URL to its SSH equivalent.
22
+ * e.g. https://github.com/Org/Repo.git → git@github.com:Org/Repo.git
23
+ */
24
+ function toSshUrl(httpsUrl) {
25
+ const m = httpsUrl.match(/^https:\/\/github\.com\/(.+)$/);
26
+ return m ? `git@github.com:${m[1]}` : httpsUrl;
27
+ }
28
+
29
+ /**
30
+ * Detect whether the user's git is configured to prefer SSH for github.com.
31
+ * Checks: 1) git insteadOf config 2) gh CLI auth status
32
+ * Returns true if SSH is preferred.
33
+ */
34
+ function prefersSsh() {
35
+ // Check git url."git@github.com:".insteadOf
36
+ try {
37
+ const out = execSync(
38
+ 'git config --global --get-regexp "url\\.git@github\\.com.*\\.insteadOf"',
39
+ { stdio: 'pipe', encoding: 'utf8', env: GIT_NET_ENV },
40
+ ).trim();
41
+ if (out.includes('https://github.com')) return true;
42
+ } catch { /* not configured */ }
43
+
44
+ // Check gh auth — if protocol is ssh, prefer SSH
45
+ try {
46
+ const out = execSync('gh auth status 2>&1', {
47
+ stdio: 'pipe', encoding: 'utf8', env: GIT_NET_ENV, timeout: 5000,
48
+ });
49
+ if (/git protocol:\s*ssh/i.test(out)) return true;
50
+ } catch { /* gh not installed or not authed */ }
51
+
52
+ return false;
53
+ }
54
+
12
55
  // ── Backward-compat: temp-dir sparse checkout (used by search.mjs) ────────────
13
56
 
14
57
  /**
@@ -18,12 +61,23 @@ const exec = promisify(execCb);
18
61
  export function sparseCheckout(repo, paths) {
19
62
  const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
20
63
 
21
- const repoUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
22
- try {
23
- execSync(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${tempDir}"`, {
24
- stdio: 'pipe',
25
- });
26
- } catch (e) {
64
+ const httpsUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
65
+ const urls = prefersSsh() ? [toSshUrl(httpsUrl), httpsUrl] : [httpsUrl, toSshUrl(httpsUrl)];
66
+
67
+ let cloned = false;
68
+ for (const url of urls) {
69
+ try {
70
+ execSync(`git clone --filter=blob:none --no-checkout "${url}" "${tempDir}"`, {
71
+ stdio: 'pipe', env: GIT_NET_ENV,
72
+ });
73
+ cloned = true;
74
+ break;
75
+ } catch {
76
+ // Clean up partial clone so next attempt can use the same tempDir
77
+ try { rmSync(join(tempDir, '.git'), { recursive: true, force: true }); } catch {}
78
+ }
79
+ }
80
+ if (!cloned) {
27
81
  throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
28
82
  }
29
83
 
@@ -47,10 +101,20 @@ export function sparseCheckout(repo, paths) {
47
101
  export async function sparseCheckoutAsync(repo, paths) {
48
102
  const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
49
103
 
50
- const repoUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
51
- try {
52
- await exec(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${tempDir}"`);
53
- } catch (e) {
104
+ const httpsUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
105
+ const urls = prefersSsh() ? [toSshUrl(httpsUrl), httpsUrl] : [httpsUrl, toSshUrl(httpsUrl)];
106
+
107
+ let cloned = false;
108
+ for (const url of urls) {
109
+ try {
110
+ await exec(`git clone --filter=blob:none --no-checkout "${url}" "${tempDir}"`, { env: GIT_NET_ENV });
111
+ cloned = true;
112
+ break;
113
+ } catch {
114
+ try { rmSync(join(tempDir, '.git'), { recursive: true, force: true }); } catch {}
115
+ }
116
+ }
117
+ if (!cloned) {
54
118
  throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
55
119
  }
56
120
 
@@ -106,7 +170,9 @@ export function isValidClone(awHome, repoUrl) {
106
170
  if (!existsSync(join(awHome, '.git'))) return false;
107
171
  try {
108
172
  const remote = execSync('git remote get-url origin', { cwd: awHome, stdio: 'pipe', encoding: 'utf8' }).trim();
109
- return remote === repoUrl || remote === repoUrl.replace(/\.git$/, '') + '.git' || remote.replace(/\.git$/, '') === repoUrl.replace(/\.git$/, '');
173
+ // Normalize both sides to bare repo path for comparison (handles HTTPS SSH)
174
+ const normalize = (u) => u.replace(/\.git$/, '').replace(/^git@github\.com:/, 'https://github.com/');
175
+ return normalize(remote) === normalize(repoUrl);
110
176
  } catch {
111
177
  return false;
112
178
  }
@@ -123,10 +189,26 @@ export async function initPersistentClone(repoUrl, awHome, sparsePaths) {
123
189
  try { execSync(`rm -rf "${awHome}"`, { stdio: 'pipe' }); } catch {}
124
190
  }
125
191
 
126
- try {
127
- await exec(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${awHome}"`);
128
- } catch (e) {
129
- throw new Error(`Failed to clone ${repoUrl}: ${e.message}`);
192
+ const urls = prefersSsh()
193
+ ? [toSshUrl(repoUrl), repoUrl]
194
+ : [repoUrl, toSshUrl(repoUrl)];
195
+
196
+ let cloned = false;
197
+ for (const url of urls) {
198
+ // Clean up any partial clone from a previous attempt
199
+ if (existsSync(awHome) && !isValidClone(awHome, url)) {
200
+ try { execSync(`rm -rf "${awHome}"`, { stdio: 'pipe' }); } catch {}
201
+ }
202
+ try {
203
+ await exec(`git clone --filter=blob:none --no-checkout "${url}" "${awHome}"`, { env: GIT_NET_ENV });
204
+ cloned = true;
205
+ break;
206
+ } catch {
207
+ // try next URL
208
+ }
209
+ }
210
+ if (!cloned) {
211
+ throw new Error(`Failed to clone ${repoUrl}. Check your git credentials and repo access (HTTPS and SSH both failed).`);
130
212
  }
131
213
 
132
214
  try {
@@ -286,7 +368,7 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
286
368
 
287
369
  // ── 2. Fetch ──────────────────────────────────────────────────────────────
288
370
  try {
289
- await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}`);
371
+ await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}`, { env: GIT_NET_ENV });
290
372
  } catch (e) {
291
373
  throw new Error(`Failed to fetch from origin: ${e.message}`);
292
374
  }
@@ -315,7 +397,7 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
315
397
  // someone else pushed to the remote tracking branch since our last fetch.
316
398
  if (isPushBranch) {
317
399
  try {
318
- await exec(`git -C "${awHome}" push --force-with-lease origin "${currentBranch}"`);
400
+ await exec(`git -C "${awHome}" push --force-with-lease origin "${currentBranch}"`, { env: GIT_NET_ENV });
319
401
  } catch { /* non-blocking — divergence will be resolved on next aw push */ }
320
402
  }
321
403
  } catch {
@@ -442,7 +524,7 @@ export function updatePushBranch(awHome, pushBranchName) {
442
524
  }
443
525
 
444
526
  try {
445
- execSync(`git -C "${awHome}" push origin "${pushBranchName}" --force`, { stdio: 'pipe' });
527
+ execSync(`git -C "${awHome}" push origin "${pushBranchName}" --force`, { stdio: 'pipe', env: GIT_NET_ENV });
446
528
  } catch (e) {
447
529
  throw new Error(`Failed to push branch: ${e.message}`);
448
530
  }
@@ -485,7 +567,7 @@ export async function createPushBranch(awHome, branchName, files, commitMsg, pre
485
567
  }
486
568
 
487
569
  try {
488
- await exec(`git -C "${awHome}" push -u origin "${branchName}"`);
570
+ await exec(`git -C "${awHome}" push -u origin "${branchName}"`, { env: GIT_NET_ENV });
489
571
  } catch (e) {
490
572
  throw new Error(`Failed to push branch: ${e.message}`);
491
573
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.39",
3
+ "version": "0.1.40-beta.0",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {