@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 +9 -2
- package/commands/push.mjs +34 -7
- package/constants.mjs +1 -1
- package/git.mjs +70 -13
- package/package.json +1 -1
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)
|
|
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
|
-
|
|
556
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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 = '
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
284
|
+
// Rebase has conflicts — collect them
|
|
233
285
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
});
|