@ghl-ai/aw 0.1.81 → 0.1.82

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/commands/push.mjs +89 -15
  2. package/package.json +1 -1
package/commands/push.mjs CHANGED
@@ -572,32 +572,91 @@ async function getProjectSourceRepo(projectRoot) {
572
572
  return `local/${basename(projectRoot)}`;
573
573
  }
574
574
 
575
- async function ensureAwDocsRepoClone(home, publishConfig) {
576
- const cloneDir = process.env.AW_DOCS_WORKTREE || join(home, '.aw-ghl-aw-docs');
577
-
578
- if (!existsSync(join(cloneDir, '.git'))) {
579
- await execFile('git', ['clone', publishConfig.repoUrl, cloneDir], {
575
+ // Network git ops against the docs repo always disable interactive credential
576
+ // prompts (so a missing/incorrect credential fails fast instead of hanging
577
+ // forever) and are time-bounded (so a stalled connection aborts instead of
578
+ // blocking the whole publish). Override the caps with AW_DOCS_GIT_TIMEOUT_MS.
579
+ const AW_DOCS_GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' };
580
+ const AW_DOCS_CLONE_TIMEOUT_MS = Number(process.env.AW_DOCS_GIT_TIMEOUT_MS) || 180000;
581
+ const AW_DOCS_NET_TIMEOUT_MS = Number(process.env.AW_DOCS_GIT_TIMEOUT_MS) || 120000;
582
+
583
+ async function awDocsSparseEnabled(cloneDir) {
584
+ try {
585
+ const { stdout } = await execFile('git', ['config', '--get', 'core.sparseCheckout'], {
586
+ cwd: cloneDir,
580
587
  encoding: 'utf8',
581
588
  });
589
+ return stdout.trim() === 'true';
590
+ } catch {
591
+ return false;
582
592
  }
593
+ }
583
594
 
584
- let status = await getGitStatus(cloneDir);
585
- if (status.trim() && await selfHealManagedAwCacheDirtiness(cloneDir, status)) {
586
- status = await getGitStatus(cloneDir);
595
+ async function ensureAwDocsRepoClone(home, publishConfig, sparseIncludePaths = []) {
596
+ const cloneDir = process.env.AW_DOCS_WORKTREE || join(home, '.aw-ghl-aw-docs');
597
+ const wantSparse = sparseIncludePaths.length > 0;
598
+ const freshClone = !existsSync(join(cloneDir, '.git'));
599
+
600
+ if (freshClone) {
601
+ // A full clone of ghl-aw-docs pulls the entire org's published docs
602
+ // (hundreds of MB / ~170k objects) and is the dominant cause of
603
+ // first-publish timeouts. Clone blobless (no file contents up front) and,
604
+ // when we know which subtree this publish touches, defer checkout and
605
+ // sparse-checkout only that path — turning hundreds of MB into a few MB.
606
+ // Degrades gracefully: if the remote does not support partial clone git
607
+ // ignores the filter, and if sparse-checkout is unavailable the branch
608
+ // checkout below still materialises the full tree.
609
+ const cloneArgs = ['clone', '--filter=blob:none'];
610
+ if (wantSparse) cloneArgs.push('--no-checkout');
611
+ cloneArgs.push(publishConfig.repoUrl, cloneDir);
612
+ await execFile('git', cloneArgs, {
613
+ encoding: 'utf8',
614
+ env: AW_DOCS_GIT_ENV,
615
+ timeout: AW_DOCS_CLONE_TIMEOUT_MS,
616
+ });
617
+ if (wantSparse) {
618
+ await execFile('git', ['sparse-checkout', 'set', '--cone', ...sparseIncludePaths], {
619
+ cwd: cloneDir,
620
+ encoding: 'utf8',
621
+ }).catch(() => { /* sparse unsupported → full checkout below */ });
622
+ }
587
623
  }
588
- if (status.trim()) {
589
- throw new Error([
590
- `AW docs repo worktree is dirty: ${cloneDir}`,
591
- status.trim(),
592
- 'Clean the cached docs repo or set AW_DOCS_WORKTREE to a clean clone before publishing.',
593
- ].join('\n'));
624
+
625
+ // Only validate cleanliness for a REUSED clone. A fresh blobless
626
+ // --no-checkout clone intentionally has no working tree yet (it is
627
+ // materialised by the branch checkout below), which would otherwise
628
+ // register as "dirty".
629
+ if (!freshClone) {
630
+ let status = await getGitStatus(cloneDir);
631
+ if (status.trim() && await selfHealManagedAwCacheDirtiness(cloneDir, status)) {
632
+ status = await getGitStatus(cloneDir);
633
+ }
634
+ if (status.trim()) {
635
+ throw new Error([
636
+ `AW docs repo worktree is dirty: ${cloneDir}`,
637
+ status.trim(),
638
+ 'Clean the cached docs repo or set AW_DOCS_WORKTREE to a clean clone before publishing.',
639
+ ].join('\n'));
640
+ }
594
641
  }
595
642
 
596
643
  await execFile('git', ['fetch', 'origin'], {
597
644
  cwd: cloneDir,
598
645
  encoding: 'utf8',
646
+ env: AW_DOCS_GIT_ENV,
647
+ timeout: AW_DOCS_NET_TIMEOUT_MS,
599
648
  });
600
649
 
650
+ // Keep the current publish's subtree inside the sparse cone. Only applies to
651
+ // clones that already use sparse-checkout (i.e. the blobless clones we
652
+ // created) — legacy full clones are left untouched so there is no regression.
653
+ if (wantSparse && await awDocsSparseEnabled(cloneDir)) {
654
+ await execFile('git', ['sparse-checkout', 'add', ...sparseIncludePaths], {
655
+ cwd: cloneDir,
656
+ encoding: 'utf8',
657
+ }).catch(() => { /* best effort — checkout below still works */ });
658
+ }
659
+
601
660
  const hasPublishBranch = await remoteBranchExists(cloneDir, publishConfig.branch);
602
661
  const checkoutStart = hasPublishBranch
603
662
  ? `origin/${publishConfig.branch}`
@@ -608,9 +667,13 @@ async function ensureAwDocsRepoClone(home, publishConfig) {
608
667
  }
609
668
 
610
669
  try {
670
+ // With a blobless clone, checkout lazily fetches the missing blobs for the
671
+ // sparse cone — a network op, so bound it like the other remote calls.
611
672
  await execFile('git', ['checkout', '-B', publishConfig.branch, checkoutStart], {
612
673
  cwd: cloneDir,
613
674
  encoding: 'utf8',
675
+ env: AW_DOCS_GIT_ENV,
676
+ timeout: AW_DOCS_NET_TIMEOUT_MS,
614
677
  });
615
678
  } catch (e) {
616
679
  const healedPath = isCheckoutOverwrittenByAwPathError(e)
@@ -622,6 +685,8 @@ async function ensureAwDocsRepoClone(home, publishConfig) {
622
685
  await execFile('git', ['checkout', '-B', publishConfig.branch, checkoutStart], {
623
686
  cwd: cloneDir,
624
687
  encoding: 'utf8',
688
+ env: AW_DOCS_GIT_ENV,
689
+ timeout: AW_DOCS_NET_TIMEOUT_MS,
625
690
  });
626
691
  }
627
692
 
@@ -629,6 +694,8 @@ async function ensureAwDocsRepoClone(home, publishConfig) {
629
694
  await execFile('git', ['pull', '--ff-only', 'origin', publishConfig.branch], {
630
695
  cwd: cloneDir,
631
696
  encoding: 'utf8',
697
+ env: AW_DOCS_GIT_ENV,
698
+ timeout: AW_DOCS_NET_TIMEOUT_MS,
632
699
  });
633
700
  }
634
701
  await removeTrackedAwSymlink(cloneDir);
@@ -729,6 +796,8 @@ async function commitAndPushAwDocsRepo(docsRepoDir, { message, branch }) {
729
796
  await execFile('git', ['push', '-u', 'origin', `HEAD:${branch}`], {
730
797
  cwd: docsRepoDir,
731
798
  encoding: 'utf8',
799
+ env: AW_DOCS_GIT_ENV,
800
+ timeout: AW_DOCS_NET_TIMEOUT_MS,
732
801
  });
733
802
  }
734
803
 
@@ -823,7 +892,12 @@ async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
823
892
  const s = fmt.spinner();
824
893
  s.start(`Publishing ${files.length} AW doc${files.length > 1 ? 's' : ''} to ${publishConfig.repo}...`);
825
894
  try {
826
- const docsRepoDir = await ensureAwDocsRepoClone(home, publishConfig);
895
+ // Scope the (blobless) docs clone to only the subtree this publish writes,
896
+ // so a fresh clone fetches a few MB instead of the whole org's docs.
897
+ const sparseIncludePaths = isRootScope
898
+ ? [`${publishConfig.dest}/${scope.relPrefix}`]
899
+ : [`${publishConfig.dest}/${repoSlug}/${githubUsername}`];
900
+ const docsRepoDir = await ensureAwDocsRepoClone(home, publishConfig, sparseIncludePaths);
827
901
  const deleteTarget = isRootScope
828
902
  ? join(docsRepoDir, publishConfig.dest, scope.relPrefix)
829
903
  : scope?.relPrefix
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.81",
3
+ "version": "0.1.82",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {