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

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/push.mjs CHANGED
@@ -2,8 +2,12 @@
2
2
 
3
3
  import { existsSync, statSync, readFileSync, appendFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
- import { execSync, execFileSync } from 'node:child_process';
5
+ import { exec as execCb, execFile as execFileCb } from 'node:child_process';
6
+ import { promisify } from 'node:util';
6
7
  import { homedir } from 'node:os';
8
+
9
+ const exec = promisify(execCb);
10
+ const execFile = promisify(execFileCb);
7
11
  import * as fmt from '../fmt.mjs';
8
12
  import { chalk } from '../fmt.mjs';
9
13
  import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR } from '../constants.mjs';
@@ -258,12 +262,14 @@ function parseRegistryPath(relPath) {
258
262
 
259
263
  // ── CODEOWNERS helpers ────────────────────────────────────────────────
260
264
 
261
- function getGitHubUser() {
265
+ async function getGitHubUser() {
262
266
  try {
263
- return execSync('gh api user --jq .login', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
267
+ const { stdout } = await exec('gh api user --jq .login');
268
+ return stdout.trim();
264
269
  } catch {
265
270
  try {
266
- return execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
271
+ const { stdout } = await exec('git config user.name');
272
+ return stdout.trim();
267
273
  } catch {
268
274
  return null;
269
275
  }
@@ -279,25 +285,26 @@ function isNewNamespaceInCodeowners(codeownersPath, namespace) {
279
285
  // ── Create or update PR via gh ────────────────────────────────────────
280
286
 
281
287
  // Returns { url, updated } — updated=true if an existing PR was refreshed.
282
- function createOrUpdatePR(awHome, branch, prTitle, prBody) {
288
+ async function createOrUpdatePR(awHome, branch, prTitle, prBody) {
283
289
  // Check for existing open PR on this branch
284
290
  try {
285
- const url = execFileSync('gh', [
291
+ const { stdout } = await execFile('gh', [
286
292
  'pr', 'view', branch, '--json', 'url', '--jq', '.url',
287
- ], { cwd: awHome, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
293
+ ], { cwd: awHome, encoding: 'utf8' });
294
+ const url = stdout.trim();
288
295
  if (url) return { url, updated: true };
289
296
  } catch { /* no existing PR */ }
290
297
 
291
298
  // Create new PR
292
299
  try {
293
- const url = execFileSync('gh', [
300
+ const { stdout } = await execFile('gh', [
294
301
  'pr', 'create',
295
302
  '--base', REGISTRY_BASE_BRANCH,
296
303
  '--head', branch,
297
304
  '--title', prTitle,
298
305
  '--body', prBody,
299
- ], { cwd: awHome, encoding: 'utf8' }).trim();
300
- return { url, updated: false };
306
+ ], { cwd: awHome, encoding: 'utf8' });
307
+ return { url: stdout.trim(), updated: false };
301
308
  } catch {
302
309
  return {
303
310
  url: `https://github.com/${REGISTRY_REPO}/compare/${REGISTRY_BASE_BRANCH}...${branch}?expand=1`,
@@ -312,7 +319,7 @@ function createOrUpdatePR(awHome, branch, prTitle, prBody) {
312
319
  // - Always creates a new branch from current state, commits, pushes, stays there.
313
320
  // - Every aw push = one new branch + one new PR. No force-push, no reuse.
314
321
  // Global flow (worktreeFlow=false): same but returns to main after push.
315
- function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false) {
322
+ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false) {
316
323
  const added = files.filter(f => !f.deleted);
317
324
  const deleted = files.filter(f => f.deleted);
318
325
 
@@ -349,7 +356,7 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
349
356
  const topNamespaces = [...new Set(files.map(f => f.namespace.split('/')[0]))];
350
357
  const codeownersPath = join(awHome, 'CODEOWNERS');
351
358
  const newNamespaces = [];
352
- const ghUser = getGitHubUser();
359
+ const ghUser = await getGitHubUser();
353
360
  for (const ns of topNamespaces) {
354
361
  if (ghUser && isNewNamespaceInCodeowners(codeownersPath, ns)) {
355
362
  newNamespaces.push(ns);
@@ -373,7 +380,7 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
373
380
  let finalBranch;
374
381
  try {
375
382
  if (worktreeFlow) {
376
- finalBranch = createPushBranch(awHome, generateBranchName(files), pathsToStage, commitMsg, preStaged);
383
+ finalBranch = await createPushBranch(awHome, generateBranchName(files), pathsToStage, commitMsg, preStaged);
377
384
  } else {
378
385
  if (!preStaged) {
379
386
  try { checkoutMain(awHome); } catch (e) {
@@ -382,7 +389,7 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
382
389
  return;
383
390
  }
384
391
  }
385
- finalBranch = createPushBranch(awHome, generateBranchName(files), pathsToStage, commitMsg, preStaged);
392
+ finalBranch = await createPushBranch(awHome, generateBranchName(files), pathsToStage, commitMsg, preStaged);
386
393
  try { checkoutMain(awHome); } catch { /* best effort */ }
387
394
  }
388
395
  const branchLabel = files.length === 0
@@ -398,7 +405,7 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
398
405
  // ── Phase 3: Open PR ───────────────────────────────────────────────
399
406
  const s2 = fmt.spinner();
400
407
  s2.start('Opening pull request...');
401
- const { url: prUrl, updated: prUpdated } = createOrUpdatePR(awHome, finalBranch, prTitle, prBody);
408
+ const { url: prUrl, updated: prUpdated } = await createOrUpdatePR(awHome, finalBranch, prTitle, prBody);
402
409
  s2.stop(prUpdated ? `PR updated — ${chalk.cyan(prUrl)}` : `PR opened — ${chalk.cyan(prUrl)}`);
403
410
 
404
411
  if (newNamespaces.length > 0) {
@@ -412,7 +419,7 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
412
419
 
413
420
  // ── Main command ─────────────────────────────────────────────────────
414
421
 
415
- export function pushCommand(args) {
422
+ export async function pushCommand(args) {
416
423
  const input = args._positional?.[0];
417
424
  const dryRun = args['--dry-run'] === true;
418
425
  const cwd = process.cwd();
@@ -457,7 +464,7 @@ export function pushCommand(args) {
457
464
  };
458
465
  });
459
466
  fmt.logInfo(`${chalk.dim('mode:')} staged (${files.length} file${files.length > 1 ? 's' : ''})`);
460
- doPush(files, awHome, dryRun, worktreeFlow, true);
467
+ await doPush(files, awHome, dryRun, worktreeFlow, true);
461
468
  return;
462
469
  }
463
470
 
@@ -471,7 +478,7 @@ export function pushCommand(args) {
471
478
 
472
479
  if (allEntries.length === 0 && worktreeFlow && commitsAheadOfMain(awHome) > 0) {
473
480
  fmt.logInfo(`${chalk.dim('mode:')} auto (no new changes — branching current state)`);
474
- doPush([], awHome, dryRun, worktreeFlow, false);
481
+ await doPush([], awHome, dryRun, worktreeFlow, false);
475
482
  return;
476
483
  }
477
484
 
@@ -498,7 +505,7 @@ export function pushCommand(args) {
498
505
  // In worktree flow, still push if there are commits ahead of main not yet in a PR
499
506
  if (worktreeFlow && commitsAheadOfMain(awHome) > 0) {
500
507
  fmt.logInfo(`${chalk.dim('mode:')} auto (no new changes — branching current state)`);
501
- doPush([], awHome, dryRun, worktreeFlow, false);
508
+ await doPush([], awHome, dryRun, worktreeFlow, false);
502
509
  return;
503
510
  }
504
511
  fmt.cancel('Nothing to push — no staged or modified files.\n\n Stage files in your IDE or use `aw status` to see changes.');
@@ -506,7 +513,7 @@ export function pushCommand(args) {
506
513
  }
507
514
 
508
515
  fmt.logInfo(`${chalk.dim('mode:')} auto (${files.length} file${files.length > 1 ? 's' : ''} — stage specific files to push a subset)`);
509
- doPush(files, awHome, dryRun, worktreeFlow, false);
516
+ await doPush(files, awHome, dryRun, worktreeFlow, false);
510
517
  return;
511
518
  }
512
519
 
@@ -546,7 +553,7 @@ export function pushCommand(args) {
546
553
  fmt.cancel(`Nothing to push in ${chalk.cyan(input)} — no agents, skills, commands, or evals found.`);
547
554
  return;
548
555
  }
549
- doPush(files, awHome, dryRun, worktreeFlow);
556
+ await doPush(files, awHome, dryRun, worktreeFlow);
550
557
  return;
551
558
  }
552
559
 
@@ -580,7 +587,7 @@ export function pushCommand(args) {
580
587
  ? `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}`
581
588
  : `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}.md`;
582
589
 
583
- doPush([{
590
+ await doPush([{
584
591
  absPath,
585
592
  registryTarget,
586
593
  type: parentDir,
package/git.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // git.mjs — Git helpers: sparse checkout (temp) + persistent clone operations.
2
2
 
3
3
  import { execSync, exec as execCb } from 'node:child_process';
4
- import { mkdtempSync, existsSync, lstatSync, rmSync } from 'node:fs';
4
+ import { mkdtempSync, existsSync, lstatSync, rmSync, readFileSync } from 'node:fs';
5
5
  import { join, basename, dirname } from 'node:path';
6
6
  import { homedir, tmpdir } from 'node:os';
7
7
  import { promisify } from 'node:util';
@@ -349,9 +349,9 @@ export function updatePushBranch(awHome, pushBranchName) {
349
349
  *
350
350
  * Branch stays on disk for iteration. Returns the branch name.
351
351
  */
352
- export function createPushBranch(awHome, branchName, files, commitMsg, preStaged = false) {
352
+ export async function createPushBranch(awHome, branchName, files, commitMsg, preStaged = false) {
353
353
  try {
354
- execSync(`git -C "${awHome}" checkout -b "${branchName}"`, { stdio: 'pipe' });
354
+ await exec(`git -C "${awHome}" checkout -b "${branchName}"`);
355
355
  } catch (e) {
356
356
  throw new Error(`Failed to create branch ${branchName}: ${e.message}`);
357
357
  }
@@ -360,21 +360,21 @@ export function createPushBranch(awHome, branchName, files, commitMsg, preStaged
360
360
  if (!preStaged) {
361
361
  try {
362
362
  const quotedFiles = files.map(f => `"${f}"`).join(' ');
363
- execSync(`git -C "${awHome}" add ${quotedFiles}`, { stdio: 'pipe' });
363
+ await exec(`git -C "${awHome}" add ${quotedFiles}`);
364
364
  } catch (e) {
365
365
  throw new Error(`Failed to stage files: ${e.message}`);
366
366
  }
367
367
  }
368
368
 
369
369
  try {
370
- execSync(`git -C "${awHome}" commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { stdio: 'pipe' });
370
+ await exec(`git -C "${awHome}" commit -m "${commitMsg.replace(/"/g, '\\"')}"`);
371
371
  } catch (e) {
372
372
  throw new Error(`Failed to commit: ${e.message}`);
373
373
  }
374
374
  }
375
375
 
376
376
  try {
377
- execSync(`git -C "${awHome}" push -u origin "${branchName}"`, { stdio: 'pipe' });
377
+ await exec(`git -C "${awHome}" push -u origin "${branchName}"`);
378
378
  } catch (e) {
379
379
  throw new Error(`Failed to push branch: ${e.message}`);
380
380
  }
@@ -496,7 +496,12 @@ export function findNearestWorktree(startDir, stopDir) {
496
496
  export function isWorktree(dir) {
497
497
  const gitPath = join(dir, '.git');
498
498
  try {
499
- return lstatSync(gitPath).isFile();
499
+ if (!lstatSync(gitPath).isFile()) return false;
500
+ // Verify the gitdir it points to actually exists (guards against stale worktrees)
501
+ const content = readFileSync(gitPath, 'utf8').trim();
502
+ const match = content.match(/^gitdir:\s*(.+)$/);
503
+ if (!match) return false;
504
+ return existsSync(match[1]);
500
505
  } catch {
501
506
  return false;
502
507
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.36-beta.33",
3
+ "version": "0.1.36-beta.35",
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",