@ghl-ai/aw 0.1.40-beta.0 → 0.1.41-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.
package/commands/init.mjs CHANGED
@@ -371,7 +371,7 @@ export async function initCommand(args) {
371
371
  }
372
372
 
373
373
  // Determine sparse paths
374
- const sparsePaths = [`.aw_registry/platform`, `content`, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
374
+ const sparsePaths = [`.aw_registry/platform`, `content`, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`, `.github/CODEOWNERS`];
375
375
  if (folderName) {
376
376
  sparsePaths.push(`.aw_registry/${folderName}`);
377
377
  }
package/commands/push.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // commands/push.mjs — Push local agents/skills to registry via PR using persistent git clone
2
2
 
3
3
  import { existsSync, statSync, readFileSync, appendFileSync } from 'node:fs';
4
- import { join, dirname } from 'node:path';
4
+ import { join, dirname, relative } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { exec as execCb, execFile as execFileCb } from 'node:child_process';
7
7
  import { promisify } from 'node:util';
@@ -361,7 +361,11 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
361
361
 
362
362
  // CODEOWNERS for new namespaces (runs inside spinner so no silent gap)
363
363
  const topNamespaces = [...new Set(files.map(f => f.namespace.split('/')[0]))];
364
- const codeownersPath = join(awHome, 'CODEOWNERS');
364
+ // GitHub looks for CODEOWNERS in .github/ first (takes precedence over root).
365
+ // platform-docs has .github/CODEOWNERS as the authoritative file.
366
+ const codeownersPath = existsSync(join(awHome, '.github', 'CODEOWNERS'))
367
+ ? join(awHome, '.github', 'CODEOWNERS')
368
+ : join(awHome, 'CODEOWNERS');
365
369
  const newNamespaces = [];
366
370
  const ghUser = await getGitHubUser();
367
371
  for (const ns of topNamespaces) {
@@ -374,7 +378,8 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
374
378
 
375
379
  const pathsToStage = files.map(f => f.registryTarget);
376
380
  if (newNamespaces.length > 0 && existsSync(codeownersPath)) {
377
- pathsToStage.push('CODEOWNERS');
381
+ // Stage the correct relative path (could be .github/CODEOWNERS or root CODEOWNERS)
382
+ pathsToStage.push(relative(awHome, codeownersPath));
378
383
  }
379
384
  // Also stage any extra paths (content/, CODEOWNERS manual edits) passed from the caller
380
385
  for (const p of extraPaths) {
@@ -473,17 +478,17 @@ export async function pushCommand(args) {
473
478
  if (!input) {
474
479
  const rulesChanged = hasRulesChanges(cwd);
475
480
 
476
- // Extra paths outside .aw_registry/ that aw also manages: content/ and CODEOWNERS.
481
+ // Extra paths outside .aw_registry/ that aw also manages: content/, CODEOWNERS, .github/CODEOWNERS.
477
482
  // Detect staged variants for staged-mode and unstaged variants for auto-mode.
478
483
  const getExtraStagedPaths = async () => {
479
484
  try {
480
- const { stdout } = await exec(`git -C "${awHome}" diff --cached --name-only -- content/ CODEOWNERS`);
485
+ const { stdout } = await exec(`git -C "${awHome}" diff --cached --name-only -- content/ CODEOWNERS .github/CODEOWNERS`);
481
486
  return stdout.trim().split('\n').filter(Boolean);
482
487
  } catch { return []; }
483
488
  };
484
489
  const getExtraChangedPaths = async () => {
485
490
  try {
486
- const { stdout } = await exec(`git -C "${awHome}" status --porcelain -- content/ CODEOWNERS`);
491
+ const { stdout } = await exec(`git -C "${awHome}" status --porcelain -- content/ CODEOWNERS .github/CODEOWNERS`);
487
492
  // git status --porcelain prefix is XY (2 chars) + optional space + path.
488
493
  // Staged-only files: `M path` (2-char prefix); unstaged files: ` M path` (3-char prefix).
489
494
  // slice(2).trimStart() handles both cases correctly.
package/git.mjs CHANGED
@@ -9,49 +9,6 @@ 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
-
55
12
  // ── Backward-compat: temp-dir sparse checkout (used by search.mjs) ────────────
56
13
 
57
14
  /**
@@ -61,23 +18,12 @@ function prefersSsh() {
61
18
  export function sparseCheckout(repo, paths) {
62
19
  const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
63
20
 
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) {
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) {
81
27
  throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
82
28
  }
83
29
 
@@ -101,20 +47,10 @@ export function sparseCheckout(repo, paths) {
101
47
  export async function sparseCheckoutAsync(repo, paths) {
102
48
  const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
103
49
 
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) {
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) {
118
54
  throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
119
55
  }
120
56
 
@@ -170,9 +106,7 @@ export function isValidClone(awHome, repoUrl) {
170
106
  if (!existsSync(join(awHome, '.git'))) return false;
171
107
  try {
172
108
  const remote = execSync('git remote get-url origin', { cwd: awHome, stdio: 'pipe', encoding: 'utf8' }).trim();
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);
109
+ return remote === repoUrl || remote === repoUrl.replace(/\.git$/, '') + '.git' || remote.replace(/\.git$/, '') === repoUrl.replace(/\.git$/, '');
176
110
  } catch {
177
111
  return false;
178
112
  }
@@ -189,26 +123,10 @@ export async function initPersistentClone(repoUrl, awHome, sparsePaths) {
189
123
  try { execSync(`rm -rf "${awHome}"`, { stdio: 'pipe' }); } catch {}
190
124
  }
191
125
 
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).`);
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}`);
212
130
  }
213
131
 
214
132
  try {
@@ -368,7 +286,7 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
368
286
 
369
287
  // ── 2. Fetch ──────────────────────────────────────────────────────────────
370
288
  try {
371
- await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}`, { env: GIT_NET_ENV });
289
+ await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}`);
372
290
  } catch (e) {
373
291
  throw new Error(`Failed to fetch from origin: ${e.message}`);
374
292
  }
@@ -397,7 +315,7 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
397
315
  // someone else pushed to the remote tracking branch since our last fetch.
398
316
  if (isPushBranch) {
399
317
  try {
400
- await exec(`git -C "${awHome}" push --force-with-lease origin "${currentBranch}"`, { env: GIT_NET_ENV });
318
+ await exec(`git -C "${awHome}" push --force-with-lease origin "${currentBranch}"`);
401
319
  } catch { /* non-blocking — divergence will be resolved on next aw push */ }
402
320
  }
403
321
  } catch {
@@ -524,7 +442,7 @@ export function updatePushBranch(awHome, pushBranchName) {
524
442
  }
525
443
 
526
444
  try {
527
- execSync(`git -C "${awHome}" push origin "${pushBranchName}" --force`, { stdio: 'pipe', env: GIT_NET_ENV });
445
+ execSync(`git -C "${awHome}" push origin "${pushBranchName}" --force`, { stdio: 'pipe' });
528
446
  } catch (e) {
529
447
  throw new Error(`Failed to push branch: ${e.message}`);
530
448
  }
@@ -567,7 +485,7 @@ export async function createPushBranch(awHome, branchName, files, commitMsg, pre
567
485
  }
568
486
 
569
487
  try {
570
- await exec(`git -C "${awHome}" push -u origin "${branchName}"`, { env: GIT_NET_ENV });
488
+ await exec(`git -C "${awHome}" push -u origin "${branchName}"`);
571
489
  } catch (e) {
572
490
  throw new Error(`Failed to push branch: ${e.message}`);
573
491
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.40-beta.0",
3
+ "version": "0.1.41-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": {