@ghl-ai/aw 0.1.36-beta.91 → 0.1.36-beta.93

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/git.mjs +51 -77
  2. package/package.json +1 -1
package/git.mjs CHANGED
@@ -230,79 +230,56 @@ export function removeFromSparseCheckout(awHome, removePaths) {
230
230
  * @param {{ silent?: boolean }} opts
231
231
  */
232
232
  export async function fetchAndMerge(awHome, { silent = true } = {}) {
233
- // ── 1. Branch guard ──────────────────────────────────────────────────────
234
- // If ~/.aw drifted off REGISTRY_BASE_BRANCH (e.g. partial init left it on
235
- // `main`), repair the sparse checkout and switch back — then RETURN EARLY.
236
- // We never run fetch/rebase on a wrong-branch run: a --no-edit merge or a
237
- // rebase between unrelated histories can disable sparse checkout and wipe
238
- // the working tree. The NEXT aw init call (hook/IDE/user) will do the sync.
239
- try {
240
- const { stdout: branchOut } = await exec(
241
- `git -C "${awHome}" rev-parse --abbrev-ref HEAD`,
242
- );
243
- const currentBranch = branchOut.trim();
244
-
245
- // Only auto-correct if on `main` — the known partial-init drift state.
246
- // Push branches (upload/, remove/, sync/, feat/) are intentional and must
247
- // not be switched away from: doing so would make aw push changes disappear.
248
- const isDriftBranch = currentBranch === 'main';
249
- if (currentBranch !== REGISTRY_BASE_BRANCH && currentBranch !== 'HEAD' && isDriftBranch) {
250
- // Re-enable sparse checkout before switching branches.
251
- // If sparse-checkout was disabled (e.g. by an old --no-edit merge),
252
- // `init --no-cone` restores it from the existing sparse-checkout file.
253
- // If the file is also empty/gone we set a safe fallback covering the
254
- // whole registry so the checkout materialises *something*.
255
- try {
256
- await exec(`git -C "${awHome}" sparse-checkout init --no-cone`);
257
- const { stdout: listOut } = await exec(
258
- `git -C "${awHome}" sparse-checkout list`,
259
- );
260
- if (!listOut.trim()) {
261
- // Paths were cleared restore a safe default (full registry tree)
262
- await exec(
263
- `git -C "${awHome}" sparse-checkout set "${REGISTRY_DIR}/"`,
264
- );
265
- }
266
- } catch {
267
- // Completely broken sparse config — force a safe default
268
- try {
269
- await exec(`git -C "${awHome}" sparse-checkout init --no-cone`);
270
- await exec(
271
- `git -C "${awHome}" sparse-checkout set "${REGISTRY_DIR}/"`,
272
- );
273
- } catch { /* best effort */ }
233
+ // ── 1. Branch classification ──────────────────────────────────────────────
234
+ // There are three possible states for ~/.aw:
235
+ //
236
+ // A. REGISTRY_BASE_BRANCH — normal, fast-forward sync
237
+ // B. Push branch (upload/ sync/ remove/) intentional, rebase remote
238
+ // changes under our local commits (stacked PR model)
239
+ // C. Anything else (e.g. `main` from a failed initPersistentClone) —
240
+ // drift/broken state; re-enable sparse checkout and switch back to
241
+ // REGISTRY_BASE_BRANCH before syncing. We must NOT rebase here:
242
+ // `main` and REGISTRY_BASE_BRANCH are unrelated trees in platform-docs
243
+ // and rebasing between them on a blob:none clone wipes the working tree.
244
+
245
+ const PUSH_PREFIXES = ['upload/', 'remove/', 'sync/'];
246
+
247
+ let currentBranch = REGISTRY_BASE_BRANCH; // assume good unless detection succeeds
248
+ try {
249
+ const { stdout } = await exec(`git -C "${awHome}" rev-parse --abbrev-ref HEAD`);
250
+ currentBranch = stdout.trim();
251
+ } catch { /* proceed with assumption */ }
252
+
253
+ const isPushBranch = PUSH_PREFIXES.some(p => currentBranch.startsWith(p));
254
+ const isBaseBranch = currentBranch === REGISTRY_BASE_BRANCH;
255
+ const isDrift = !isBaseBranch && !isPushBranch && currentBranch !== 'HEAD';
256
+
257
+ if (isDrift) {
258
+ // Re-enable sparse checkout (may have been disabled by an old --no-edit merge)
259
+ try {
260
+ await exec(`git -C "${awHome}" sparse-checkout init --no-cone`);
261
+ const { stdout: listOut } = await exec(`git -C "${awHome}" sparse-checkout list`);
262
+ if (!listOut.trim()) {
263
+ await exec(`git -C "${awHome}" sparse-checkout set "${REGISTRY_DIR}/"`);
274
264
  }
275
-
276
- // Switch to the correct branch; this materialises files via lazy fetch.
265
+ } catch {
277
266
  try {
278
- await exec(`git -C "${awHome}" checkout ${REGISTRY_BASE_BRANCH}`);
279
- } catch {
280
- // checkout failed repo is in a bad state we cannot repair here.
281
- // Return without touching anything; the user can run `aw init` to
282
- // trigger a full re-clone via initPersistentClone.
283
- return { updated: false, conflicts: [] };
284
- }
285
-
286
- // Branch repaired. Do NOT continue to fetch/rebase on this same run —
287
- // that combination is what caused the original wipe.
288
- return { updated: false, conflicts: [] };
267
+ await exec(`git -C "${awHome}" sparse-checkout init --no-cone`);
268
+ await exec(`git -C "${awHome}" sparse-checkout set "${REGISTRY_DIR}/"`);
269
+ } catch { /* best effort */ }
289
270
  }
290
- } catch { /* branch detection failed — proceed with normal sync */ }
291
271
 
292
- // ── 2. Repair sparse checkout if disabled on correct branch ──────────────
293
- // Guard: if something disabled sparse checkout while on the right branch
294
- // (should be rare), re-enable before fetch so the working tree stays intact.
295
- try {
296
- await exec(`git -C "${awHome}" sparse-checkout list`);
297
- } catch {
272
+ // Switch to the correct branch materialises files via lazy blob fetch
298
273
  try {
299
- await exec(`git -C "${awHome}" sparse-checkout init --no-cone`);
300
- // Re-checkout to materialise files according to restored sparse config
301
274
  await exec(`git -C "${awHome}" checkout ${REGISTRY_BASE_BRANCH}`);
302
- } catch { /* best effort */ }
275
+ } catch {
276
+ return { updated: false, conflicts: [] }; // can't repair; leave for full re-init
277
+ }
278
+
279
+ // Drift repaired. Continue below to also fetch + fast-forward this run.
303
280
  }
304
281
 
305
- // ── 3. Fetch ──────────────────────────────────────────────────────────────
282
+ // ── 2. Fetch ──────────────────────────────────────────────────────────────
306
283
  try {
307
284
  await exec(`git -C "${awHome}" fetch origin ${REGISTRY_BASE_BRANCH}`);
308
285
  } catch (e) {
@@ -312,36 +289,33 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
312
289
  let updated = false;
313
290
  const conflicts = [];
314
291
 
315
- // ── 4. Fast-forward (clean case — no local commits ahead of remote) ──────
292
+ // ── 3. Fast-forward (no local commits ahead of remote) ───────────────────
316
293
  try {
317
294
  const { stdout } = await exec(
318
295
  `git -C "${awHome}" merge origin/${REGISTRY_BASE_BRANCH} --ff-only`,
319
296
  );
320
297
  updated = !stdout.includes('Already up to date');
321
298
  return { updated, conflicts };
322
- } catch { /* ff-only failed — local commits exist, fall through to rebase */ }
299
+ } catch { /* local commits exist fall through to rebase */ }
323
300
 
324
- // ── 5. Rebase local commits onto remote ───────────────────────────────────
325
- // Rebase keeps history linear and never disables sparse checkout.
301
+ // ── 4. Rebase current branch onto remote REGISTRY_BASE_BRANCH ────────────
302
+ // For push branches: stacks our local commits on top of latest remote.
303
+ // For base branch: only reached if local commits exist (unusual).
304
+ // Never uses --no-edit merge — that disables sparse checkout on blob:none.
326
305
  try {
327
306
  await exec(`git -C "${awHome}" rebase origin/${REGISTRY_BASE_BRANCH}`);
328
307
  updated = true;
329
308
  } catch {
330
- // Rebase has conflicts — collect them
331
309
  try {
332
- const { stdout } = await exec(
333
- `git -C "${awHome}" diff --name-only --diff-filter=U`,
334
- );
310
+ const { stdout } = await exec(`git -C "${awHome}" diff --name-only --diff-filter=U`);
335
311
  conflicts.push(...stdout.trim().split('\n').filter(Boolean));
336
312
  } catch { /* best effort */ }
337
313
 
338
314
  if (silent) {
339
- // In background (hook/IDE) mode: abort to leave the repo in a clean,
340
- // usable state. The next explicit `aw init` will surface the conflict.
315
+ // Background: abort so repo stays usable; surface on next explicit aw init
341
316
  try { await exec(`git -C "${awHome}" rebase --abort`); } catch { /* best effort */ }
342
317
  }
343
- // In interactive mode: leave conflict markers in place so the user can
344
- // resolve them directly in their editor.
318
+ // Interactive: leave conflict markers for the user to resolve
345
319
  }
346
320
 
347
321
  return { updated, conflicts };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.36-beta.91",
3
+ "version": "0.1.36-beta.93",
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",