@ghl-ai/aw 0.1.36-beta.83 → 0.1.36-beta.86

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
@@ -220,8 +220,15 @@ export async function initCommand(args) {
220
220
  const s = fmt.spinner();
221
221
  if (!silent) s.start('Fetching latest from registry...');
222
222
  try {
223
- await fetchAndMerge(AW_HOME);
224
- if (!silent) s.stop('Registry up to date');
223
+ const { conflicts } = await fetchAndMerge(AW_HOME, { silent });
224
+ if (!silent) {
225
+ if (conflicts.length > 0) {
226
+ s.stop(chalk.yellow(`Conflicts in ${conflicts.length} file${conflicts.length > 1 ? 's' : ''} — resolve then run aw init again`));
227
+ fmt.logWarn(conflicts.map(f => ` • ${f}`).join('\n'));
228
+ } else {
229
+ s.stop('Registry up to date');
230
+ }
231
+ }
225
232
  } catch (e) {
226
233
  if (!silent) s.stop(chalk.yellow('Fetch failed — continuing with local cache'));
227
234
  }
package/commands/push.mjs CHANGED
@@ -548,17 +548,26 @@ export async function pushCommand(args) {
548
548
  else if (existsSync(awBased + '.md')) absPath = awBased + '.md';
549
549
  }
550
550
  }
551
+ let isDeletedFile = false;
551
552
  if (!absPath || !existsSync(absPath)) {
552
553
  if (absPath && !absPath.endsWith('.md') && existsSync(absPath + '.md')) {
553
554
  absPath = absPath + '.md';
554
555
  } else {
555
- fmt.cancel(`Path not found: ${absPath || input}\n\n Only files inside .aw_registry/ can be pushed.\n Use ${chalk.dim('aw status')} to see modified files.`);
556
- return;
556
+ // File may have been deleted check detectChanges before giving up
557
+ const preCheck = detectChanges(awHome, REGISTRY_DIR);
558
+ const deletedPaths = new Set(preCheck.deleted.map(e => e.path));
559
+ const candidate = `${REGISTRY_DIR}/${resolved.registryPath}`;
560
+ if (deletedPaths.has(candidate) || deletedPaths.has(candidate + '.md')) {
561
+ isDeletedFile = true;
562
+ } else {
563
+ fmt.cancel(`Path not found: ${absPath || input}\n\n Only files inside .aw_registry/ can be pushed.\n Use ${chalk.dim('aw status')} to see modified files.`);
564
+ return;
565
+ }
557
566
  }
558
567
  }
559
568
 
560
569
  // Folder/namespace input → batch push using registry content in awHome
561
- if (statSync(absPath).isDirectory()) {
570
+ if (!isDeletedFile && statSync(absPath).isDirectory()) {
562
571
  const relFromRegistry = absPath.startsWith(registrySubDir + '/')
563
572
  ? absPath.slice(registrySubDir.length + 1)
564
573
  : absPath.startsWith(workspaceDir + '/')
@@ -578,7 +587,25 @@ export async function pushCommand(args) {
578
587
  ...folderChanges.deleted.map(e => e.path),
579
588
  ]);
580
589
  const files = allFiles.filter(f => changedPaths.has(f.registryTarget));
581
- if (files.length === 0) {
590
+ // Also include deleted files within this folder (collectBatchFiles misses them — files don't exist on disk)
591
+ const folderPrefix = relFromRegistry ? relFromRegistry + '/' : '';
592
+ const deletedInFolder = folderChanges.deleted
593
+ .filter(e => !folderPrefix || e.registryPath.startsWith(folderPrefix))
594
+ .map(e => {
595
+ const meta = parseRegistryPath(e.registryPath);
596
+ const parts = e.registryPath.split('/');
597
+ return {
598
+ absPath: join(awHome, e.path),
599
+ registryTarget: e.path,
600
+ type: meta?.type ?? 'file',
601
+ namespace: meta?.namespace ?? parts.slice(0, -1).join('/'),
602
+ slug: meta?.slug ?? parts[parts.length - 1].replace(/\.md$/, ''),
603
+ isDir: false,
604
+ deleted: true,
605
+ };
606
+ });
607
+ const allChangedFiles = [...files, ...deletedInFolder];
608
+ if (allChangedFiles.length === 0) {
582
609
  if (commitsAheadOfMain(awHome) > 0) {
583
610
  fmt.logInfo(`${chalk.dim('mode:')} selected folder (no new changes — branching current state)`);
584
611
  await doPush([], awHome, dryRun, worktreeFlow);
@@ -587,7 +614,7 @@ export async function pushCommand(args) {
587
614
  fmt.cancel(`Nothing to push in ${chalk.cyan(input)} — no changes found.\n\n Files in this folder are already up to date in the registry.\n Edit a file first, then push.`);
588
615
  return;
589
616
  }
590
- await doPush(files, awHome, dryRun, worktreeFlow);
617
+ await doPush(allChangedFiles, awHome, dryRun, worktreeFlow);
591
618
  return;
592
619
  }
593
620
 
@@ -616,7 +643,7 @@ export async function pushCommand(args) {
616
643
  const parentDir = regParts[typeIdx];
617
644
  const slug = regParts[typeIdx + 1];
618
645
  const namespacePath = namespaceParts.join('/');
619
- const isDir = statSync(absPath).isDirectory();
646
+ const isDir = !isDeletedFile && statSync(absPath).isDirectory();
620
647
  const registryTarget = isDir
621
648
  ? `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}`
622
649
  : `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}.md`;
@@ -648,7 +675,7 @@ export async function pushCommand(args) {
648
675
  namespace: namespacePath,
649
676
  slug,
650
677
  isDir,
651
- deleted: false,
678
+ deleted: isDeletedFile,
652
679
  }], awHome, dryRun, worktreeFlow);
653
680
  }
654
681
 
package/constants.mjs CHANGED
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
 
6
6
  /** Base branch for PRs and sync checkout */
7
- export const REGISTRY_BASE_BRANCH = 'main';
7
+ export const REGISTRY_BASE_BRANCH = 'chore/stream-registry';
8
8
 
9
9
  /** Default registry repository */
10
10
  export const REGISTRY_REPO = 'GoHighLevel/platform-docs';
package/git.mjs CHANGED
@@ -212,10 +212,51 @@ export function removeFromSparseCheckout(awHome, removePaths) {
212
212
  }
213
213
 
214
214
  /**
215
- * Fetch and merge latest from origin/main into the persistent clone.
215
+ * Fetch and sync the persistent clone with origin/REGISTRY_BASE_BRANCH.
216
216
  * Returns { updated: boolean, conflicts: string[] }
217
+ *
218
+ * Strategy:
219
+ * 1. Branch guard — if ~/.aw drifted off REGISTRY_BASE_BRANCH (e.g. partial
220
+ * init left it on `main`), stash + checkout the right branch first.
221
+ * 2. Fetch from origin.
222
+ * 3. Fast-forward if possible (clean case, no local commits).
223
+ * 4. Rebase local commits on top of remote if ff-only fails.
224
+ * 5. On rebase conflict in silent mode: abort to keep repo usable.
225
+ * In interactive mode: leave conflict markers for the user.
226
+ * NEVER uses --no-edit merge — a merge commit on blob:none + sparse-checkout
227
+ * disables sparse checkout, emptying the working tree.
228
+ *
229
+ * @param {string} awHome
230
+ * @param {{ silent?: boolean }} opts
217
231
  */
218
- export async function fetchAndMerge(awHome) {
232
+ export async function fetchAndMerge(awHome, { silent = true } = {}) {
233
+ // ── 1. Branch guard ──────────────────────────────────────────────────────
234
+ // If ~/.aw is on the wrong branch (e.g. initPersistentClone cloned but the
235
+ // `git checkout chore/stream-registry` step failed, leaving HEAD on main),
236
+ // stash any local changes and switch back before doing anything.
237
+ try {
238
+ const { stdout: branchOut } = await exec(
239
+ `git -C "${awHome}" rev-parse --abbrev-ref HEAD`,
240
+ );
241
+ const currentBranch = branchOut.trim();
242
+ if (currentBranch !== REGISTRY_BASE_BRANCH && currentBranch !== 'HEAD') {
243
+ let stashed = false;
244
+ try {
245
+ const { stdout: stashOut } = await exec(
246
+ `git -C "${awHome}" stash push --include-untracked -m "aw: branch-guard stash"`,
247
+ );
248
+ stashed = !stashOut.includes('No local changes to save');
249
+ } catch { /* best effort */ }
250
+ try {
251
+ await exec(`git -C "${awHome}" checkout ${REGISTRY_BASE_BRANCH}`);
252
+ } catch { /* if checkout fails, proceed — fetch will still work */ }
253
+ if (stashed) {
254
+ try { await exec(`git -C "${awHome}" stash pop`); } catch { /* best effort */ }
255
+ }
256
+ }
257
+ } catch { /* if branch detection fails, proceed */ }
258
+
259
+ // ── 2. Fetch ──────────────────────────────────────────────────────────────
219
260
  try {
220
261
  await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}`);
221
262
  } catch (e) {
@@ -225,21 +266,36 @@ export async function fetchAndMerge(awHome) {
225
266
  let updated = false;
226
267
  const conflicts = [];
227
268
 
269
+ // ── 3. Fast-forward (clean case — no local commits ahead of remote) ──────
228
270
  try {
229
- const { stdout } = await exec(`git -C "${awHome}" merge origin/${REGISTRY_BASE_BRANCH} --ff-only`);
271
+ const { stdout } = await exec(
272
+ `git -C "${awHome}" merge origin/${REGISTRY_BASE_BRANCH} --ff-only`,
273
+ );
230
274
  updated = !stdout.includes('Already up to date');
275
+ return { updated, conflicts };
276
+ } catch { /* ff-only failed — local commits exist, fall through to rebase */ }
277
+
278
+ // ── 4. Rebase local commits onto remote ───────────────────────────────────
279
+ // Rebase keeps history linear and never disables sparse checkout.
280
+ try {
281
+ await exec(`git -C "${awHome}" rebase origin/${REGISTRY_BASE_BRANCH}`);
282
+ updated = true;
231
283
  } catch {
232
- // ff-only failedtry no-edit merge
284
+ // Rebase has conflicts collect them
233
285
  try {
234
- await exec(`git -C "${awHome}" merge origin/${REGISTRY_BASE_BRANCH} --no-edit`);
235
- updated = true;
236
- } catch {
237
- // Parse conflicts
238
- try {
239
- const { stdout } = await exec(`git -C "${awHome}" diff --name-only --diff-filter=U`);
240
- conflicts.push(...stdout.trim().split('\n').filter(Boolean));
241
- } catch { /* best effort */ }
286
+ const { stdout } = await exec(
287
+ `git -C "${awHome}" diff --name-only --diff-filter=U`,
288
+ );
289
+ conflicts.push(...stdout.trim().split('\n').filter(Boolean));
290
+ } catch { /* best effort */ }
291
+
292
+ if (silent) {
293
+ // In background (hook/IDE) mode: abort to leave the repo in a clean,
294
+ // usable state. The next explicit `aw init` will surface the conflict.
295
+ try { await exec(`git -C "${awHome}" rebase --abort`); } catch { /* best effort */ }
242
296
  }
297
+ // In interactive mode: leave conflict markers in place so the user can
298
+ // resolve them directly in their editor.
243
299
  }
244
300
 
245
301
  return { updated, conflicts };
@@ -253,7 +309,8 @@ export async function fetchAndMerge(awHome) {
253
309
  export function detectChanges(awHome, registryDir) {
254
310
  let statusOut = '';
255
311
  try {
256
- statusOut = execSync(`git -C "${awHome}" status --porcelain "${registryDir}/"`, {
312
+ // -u expands untracked directories to individual files so we get exact paths
313
+ statusOut = execSync(`git -C "${awHome}" status --porcelain -u "${registryDir}/"`, {
257
314
  stdio: 'pipe',
258
315
  encoding: 'utf8',
259
316
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.36-beta.83",
3
+ "version": "0.1.36-beta.86",
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",