@ghl-ai/aw 0.1.36-beta.31 → 0.1.36-beta.33

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/pull.mjs CHANGED
@@ -7,7 +7,7 @@ import { execSync } from 'node:child_process';
7
7
  import * as config from '../config.mjs';
8
8
  import * as fmt from '../fmt.mjs';
9
9
  import { chalk } from '../fmt.mjs';
10
- import { fetchAndMerge, addToSparseCheckout, isValidClone, isWorktree, rebaseOntoOriginMain } from '../git.mjs';
10
+ import { fetchAndMerge, addToSparseCheckout, isValidClone, findNearestWorktree, rebaseOntoOriginMain } from '../git.mjs';
11
11
  import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL, DOCS_SOURCE_DIR } from '../constants.mjs';
12
12
  import { linkWorkspace } from '../link.mjs';
13
13
  import { generateCommands, copyInstructions } from '../integrate.mjs';
@@ -76,8 +76,8 @@ export async function pullCommand(args) {
76
76
  }
77
77
 
78
78
  // Always rebase project worktree's current branch onto origin/main
79
- const localAw = join(cwd, '.aw');
80
- if (cwd !== HOME && isWorktree(localAw)) {
79
+ const localAw = cwd !== HOME ? findNearestWorktree(cwd, HOME) : null;
80
+ if (localAw) {
81
81
  const rebaseSpinner = log.spinner();
82
82
  rebaseSpinner.start('Rebasing local branch onto latest...');
83
83
  try {
package/commands/push.mjs CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  createPushBranch,
16
16
  checkoutMain,
17
17
  isValidClone,
18
- isWorktree,
18
+ findNearestWorktree,
19
19
  getLocalRegistryDir,
20
20
  commitsAheadOfMain,
21
21
  logAheadOfMain,
@@ -320,24 +320,32 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
320
320
  const deletedParts = Object.entries(groupBy(deleted, 'type')).map(([t, items]) => `${items.length} ${singular(t, items.length)} removed`);
321
321
  const countParts = [...addedParts, ...deletedParts];
322
322
 
323
- if (files.length === 0) {
324
- fmt.logInfo('Branching current state (no new changes)');
325
- } else {
326
- fmt.logInfo(`${chalk.bold(files.length)} file${files.length > 1 ? 's' : ''} to push (${countParts.join(', ')})`);
327
- }
328
-
323
+ // ── Dry-run: just list files and exit ──────────────────────────────
329
324
  if (dryRun) {
330
- for (const f of files) {
331
- const ns = chalk.dim(` [${f.namespace}]`);
332
- const label = f.deleted ? chalk.red('DELETE') : chalk.yellow(f.type);
333
- fmt.logMessage(` ${label}/${f.slug}${ns}`);
325
+ if (files.length === 0) {
326
+ fmt.logInfo('No new changes would branch current state');
327
+ } else {
328
+ fmt.logInfo(`${chalk.bold(files.length)} file${files.length > 1 ? 's' : ''} to push (${countParts.join(', ')})`);
329
+ for (const f of files) {
330
+ const ns = chalk.dim(` [${f.namespace}]`);
331
+ const label = f.deleted ? chalk.red('DELETE') : chalk.yellow(f.type);
332
+ fmt.logMessage(` ${label}/${f.slug}${ns}`);
333
+ }
334
334
  }
335
335
  fmt.logWarn('No changes made (--dry-run)');
336
336
  fmt.outro(chalk.dim('Remove --dry-run to push'));
337
337
  return;
338
338
  }
339
339
 
340
- // Handle CODEOWNERS for new namespaces
340
+ // ── Phase 1: Prepare commit ────────────────────────────────────────
341
+ const prepLabel = files.length === 0
342
+ ? 'Creating upload branch from HEAD...'
343
+ : `Preparing ${countParts.join(', ')}...`;
344
+
345
+ const s = fmt.spinner();
346
+ s.start(prepLabel);
347
+
348
+ // CODEOWNERS for new namespaces (runs inside spinner so no silent gap)
341
349
  const topNamespaces = [...new Set(files.map(f => f.namespace.split('/')[0]))];
342
350
  const codeownersPath = join(awHome, 'CODEOWNERS');
343
351
  const newNamespaces = [];
@@ -359,19 +367,14 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
359
367
  const prTitle = generatePrTitle(files, awHome);
360
368
  const prBody = generatePrBody(files, newNamespaces, awHome);
361
369
 
362
- const s = fmt.spinner();
363
- s.start('Committing and pushing...');
370
+ // ── Phase 2: Commit + push branch ──────────────────────────────────
371
+ s.message('Creating branch and pushing...');
364
372
 
365
373
  let finalBranch;
366
374
  try {
367
375
  if (worktreeFlow) {
368
- // ── Worktree flow ─────────────────────────────────────────────────
369
- // Always create a new branch from current state, commit, push, stay there.
370
- // Every push = one new branch + one new PR. No force-push, no branch reuse.
371
376
  finalBranch = createPushBranch(awHome, generateBranchName(files), pathsToStage, commitMsg, preStaged);
372
377
  } else {
373
- // ── Global clone flow ─────────────────────────────────────────────
374
- // Checkout main → create branch → commit → push → return to main.
375
378
  if (!preStaged) {
376
379
  try { checkoutMain(awHome); } catch (e) {
377
380
  s.stop(chalk.red('Push failed'));
@@ -382,17 +385,21 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
382
385
  finalBranch = createPushBranch(awHome, generateBranchName(files), pathsToStage, commitMsg, preStaged);
383
386
  try { checkoutMain(awHome); } catch { /* best effort */ }
384
387
  }
385
- s.stop('Branch pushed');
388
+ const branchLabel = files.length === 0
389
+ ? 'Branch created'
390
+ : `Pushed ${countParts.join(', ')}`;
391
+ s.stop(branchLabel);
386
392
  } catch (e) {
387
393
  s.stop(chalk.red('Push failed'));
388
394
  fmt.cancel(`Push failed: ${e.message}`);
389
395
  return;
390
396
  }
391
397
 
398
+ // ── Phase 3: Open PR ───────────────────────────────────────────────
392
399
  const s2 = fmt.spinner();
393
- s2.start('Creating PR...');
400
+ s2.start('Opening pull request...');
394
401
  const { url: prUrl, updated: prUpdated } = createOrUpdatePR(awHome, finalBranch, prTitle, prBody);
395
- s2.stop(prUpdated ? 'PR updated' : 'PR created');
402
+ s2.stop(prUpdated ? `PR updated — ${chalk.cyan(prUrl)}` : `PR opened — ${chalk.cyan(prUrl)}`);
396
403
 
397
404
  if (newNamespaces.length > 0) {
398
405
  fmt.logInfo(`New namespace${newNamespaces.length > 1 ? 's' : ''} ${chalk.cyan(newNamespaces.join(', '))} — CODEOWNERS entr${newNamespaces.length > 1 ? 'ies' : 'y'} added`);
@@ -400,7 +407,6 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
400
407
  if (worktreeFlow) {
401
408
  fmt.logInfo(chalk.dim(`On branch ${finalBranch} — run aw push again to open a new PR`));
402
409
  }
403
- fmt.logSuccess(`PR: ${chalk.cyan(prUrl)}`);
404
410
  fmt.outro('⟁ Push complete');
405
411
  }
406
412
 
@@ -416,8 +422,8 @@ export function pushCommand(args) {
416
422
 
417
423
  // If the project has a local worktree (.aw/ with a .git file), use it so
418
424
  // changes made inside the project are correctly detected and committed.
419
- const localAw = join(cwd, '.aw');
420
- const hasWorktree = isWorktree(localAw);
425
+ const localAw = findNearestWorktree(cwd, HOME);
426
+ const hasWorktree = localAw !== null;
421
427
  const awHome = hasWorktree ? localAw : globalAw;
422
428
  const registrySubDir = join(awHome, REGISTRY_DIR);
423
429
  const workspaceDir = getLocalRegistryDir(cwd, join(HOME, '.aw_registry'));
@@ -5,7 +5,7 @@ import { homedir } from 'node:os';
5
5
  import * as config from '../config.mjs';
6
6
  import * as fmt from '../fmt.mjs';
7
7
  import { chalk } from '../fmt.mjs';
8
- import { detectChanges, getCurrentBranch, commitsAheadOfMain, isValidClone, isWorktree } from '../git.mjs';
8
+ import { detectChanges, getCurrentBranch, commitsAheadOfMain, isValidClone, findNearestWorktree } from '../git.mjs';
9
9
  import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL } from '../constants.mjs';
10
10
 
11
11
  export function statusCommand(args) {
@@ -15,12 +15,12 @@ export function statusCommand(args) {
15
15
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
16
16
 
17
17
  // Use local project worktree if present so changes made there are visible
18
- const localAw = join(cwd, '.aw');
19
- const AW_HOME = isWorktree(localAw) ? localAw : globalAw;
18
+ const localAw = cwd !== HOME ? findNearestWorktree(cwd, HOME) : null;
19
+ const AW_HOME = localAw ?? globalAw;
20
20
 
21
21
  fmt.intro('aw status');
22
22
 
23
- if (!isWorktree(localAw) && !isValidClone(AW_HOME, REGISTRY_URL)) {
23
+ if (!localAw && !isValidClone(AW_HOME, REGISTRY_URL)) {
24
24
  fmt.cancel('Registry not initialized. Run: aw init');
25
25
  return;
26
26
  }
package/git.mjs CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { execSync, exec as execCb } from 'node:child_process';
4
4
  import { mkdtempSync, existsSync, lstatSync, rmSync } from 'node:fs';
5
- import { join, basename } from 'node:path';
6
- import { tmpdir } from 'node:os';
5
+ import { join, basename, dirname } from 'node:path';
6
+ import { homedir, tmpdir } from 'node:os';
7
7
  import { promisify } from 'node:util';
8
8
  import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR } from './constants.mjs';
9
9
 
@@ -468,10 +468,28 @@ export function rebaseOntoOriginMain(awHome) {
468
468
  * worktree), so isWorktree returns false and the global fallback is used.
469
469
  */
470
470
  export function getLocalRegistryDir(cwd, fallback) {
471
- if (isWorktree(join(cwd, '.aw'))) return join(cwd, '.aw', REGISTRY_DIR);
471
+ const wt = findNearestWorktree(cwd, homedir());
472
+ if (wt) return join(wt, REGISTRY_DIR);
472
473
  return fallback;
473
474
  }
474
475
 
476
+ /**
477
+ * Walk up from `startDir` toward `stopDir` looking for the nearest `.aw`
478
+ * linked worktree. Returns the `.aw` path if found, null otherwise.
479
+ * Stops at `stopDir` (usually homedir) so it never escapes the user's tree.
480
+ */
481
+ export function findNearestWorktree(startDir, stopDir) {
482
+ let dir = startDir;
483
+ while (dir !== stopDir) {
484
+ const candidate = join(dir, '.aw');
485
+ if (isWorktree(candidate)) return candidate;
486
+ const parent = dirname(dir);
487
+ if (parent === dir) break; // filesystem root
488
+ dir = parent;
489
+ }
490
+ return null;
491
+ }
492
+
475
493
  /**
476
494
  * Check if dir is a git linked worktree (has .git as a FILE, not a directory).
477
495
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.36-beta.31",
3
+ "version": "0.1.36-beta.33",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": "bin.js",