@ghl-ai/aw 0.1.34-beta.10 → 0.1.34-beta.12
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/drop.mjs +2 -2
- package/commands/init.mjs +90 -42
- package/commands/nuke.mjs +14 -2
- package/commands/pull.mjs +7 -5
- package/commands/push.mjs +3 -1
- package/commands/status.mjs +1 -1
- package/git.mjs +85 -36
- package/link.mjs +1 -1
- package/package.json +1 -1
package/commands/drop.mjs
CHANGED
|
@@ -54,8 +54,8 @@ export function dropCommand(args) {
|
|
|
54
54
|
fmt.logSuccess(`Removed ${chalk.cyan(regPath)} from sparse checkout`);
|
|
55
55
|
}
|
|
56
56
|
} else {
|
|
57
|
-
// Sub-path: delete local files (
|
|
58
|
-
const targetPath = join(workspaceDir,
|
|
57
|
+
// Sub-path: delete local files (restored on next pull since still in sparse checkout)
|
|
58
|
+
const targetPath = join(workspaceDir, regPath);
|
|
59
59
|
if (existsSync(targetPath)) {
|
|
60
60
|
rmSync(targetPath, { recursive: true, force: true });
|
|
61
61
|
fmt.logInfo(`Removed ${chalk.cyan(regPath)} from workspace`);
|
package/commands/init.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// commands/init.mjs — Clean init: persistent sparse git checkout, link IDEs, global git hooks.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Layout:
|
|
4
|
+
// ~/.aw_repo/ — git clone (repo root)
|
|
5
|
+
// ~/.aw_registry/ — symlink → ~/.aw_repo/.aw_registry/
|
|
6
|
+
//
|
|
7
|
+
// No temp dirs. No manifest hashing. Git handles sync natively.
|
|
4
8
|
|
|
5
|
-
import { mkdirSync, existsSync, writeFileSync, symlinkSync, cpSync, readFileSync, readdirSync, renameSync } from 'node:fs';
|
|
9
|
+
import { mkdirSync, existsSync, writeFileSync, symlinkSync, cpSync, readFileSync, readdirSync, renameSync, lstatSync } from 'node:fs';
|
|
6
10
|
import { execSync } from 'node:child_process';
|
|
7
11
|
import { join, dirname } from 'node:path';
|
|
8
12
|
import { homedir } from 'node:os';
|
|
@@ -11,8 +15,9 @@ import * as config from '../config.mjs';
|
|
|
11
15
|
import * as fmt from '../fmt.mjs';
|
|
12
16
|
import { chalk } from '../fmt.mjs';
|
|
13
17
|
import {
|
|
14
|
-
gitInit, gitPull, gitSparseAdd, gitLsRemote,
|
|
15
|
-
isGitRepo, withLock, includeToSparsePaths,
|
|
18
|
+
gitInit, gitPull, gitSparseAdd, gitSparseList, gitSparseSet, gitLsRemote,
|
|
19
|
+
isGitRepo, withLock, includeToSparsePaths, addGitExcludes,
|
|
20
|
+
GLOBAL_GIT_DIR,
|
|
16
21
|
} from '../git.mjs';
|
|
17
22
|
import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
|
|
18
23
|
import { linkWorkspace } from '../link.mjs';
|
|
@@ -74,19 +79,18 @@ const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen'
|
|
|
74
79
|
|
|
75
80
|
/**
|
|
76
81
|
* Bootstrap a new namespace from [template].
|
|
77
|
-
* Copies [template]/ → folderName/, replaces $TEAM_NS
|
|
82
|
+
* Copies [template]/ → folderName/, replaces $TEAM_NS.
|
|
78
83
|
*/
|
|
79
84
|
function bootstrapFromTemplate(folderName, teamNS) {
|
|
80
|
-
|
|
81
|
-
const
|
|
85
|
+
// Template and target are inside GLOBAL_AW_DIR (which IS .aw_registry/)
|
|
86
|
+
const templateDir = join(GLOBAL_AW_DIR, '[template]');
|
|
87
|
+
const targetDir = join(GLOBAL_AW_DIR, ...folderName.split('/'));
|
|
82
88
|
|
|
83
89
|
if (!existsSync(templateDir)) {
|
|
84
90
|
throw new Error('Template directory not found. Ensure [template] is in sparse checkout.');
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
cpSync(templateDir, targetDir, { recursive: true });
|
|
88
|
-
|
|
89
|
-
// Replace $TEAM_NS in .md files
|
|
90
94
|
walkAndReplace(targetDir, '$TEAM_NS', teamNS);
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -105,20 +109,44 @@ function walkAndReplace(dir, search, replace) {
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
/**
|
|
108
|
-
* Ensure platform/docs symlink points to
|
|
112
|
+
* Ensure platform/docs symlink points to content/ in the git repo root.
|
|
109
113
|
*/
|
|
110
114
|
function ensureDocsSymlink() {
|
|
111
|
-
const docsLink = join(GLOBAL_AW_DIR,
|
|
112
|
-
const contentDir = join(
|
|
115
|
+
const docsLink = join(GLOBAL_AW_DIR, 'platform', 'docs');
|
|
116
|
+
const contentDir = join(GLOBAL_GIT_DIR, 'content');
|
|
113
117
|
|
|
114
118
|
if (existsSync(contentDir) && !existsSync(docsLink)) {
|
|
115
119
|
try {
|
|
116
120
|
mkdirSync(dirname(docsLink), { recursive: true });
|
|
117
|
-
|
|
121
|
+
// Relative symlink: .aw_registry/platform/docs → ../../../content
|
|
122
|
+
// (from ~/.aw_repo/.aw_registry/platform/docs to ~/.aw_repo/content)
|
|
123
|
+
symlinkSync('../../../content', docsLink);
|
|
118
124
|
} catch { /* best effort */ }
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Create the ~/.aw_registry symlink pointing to the .aw_registry/ subdir inside the git repo.
|
|
130
|
+
*/
|
|
131
|
+
function ensureWorkspaceSymlink() {
|
|
132
|
+
const target = join(GLOBAL_GIT_DIR, REGISTRY_DIR);
|
|
133
|
+
|
|
134
|
+
if (existsSync(GLOBAL_AW_DIR)) {
|
|
135
|
+
// Already exists — check if it's the right symlink
|
|
136
|
+
try {
|
|
137
|
+
if (lstatSync(GLOBAL_AW_DIR).isSymbolicLink()) return; // already a symlink, good
|
|
138
|
+
} catch { /* */ }
|
|
139
|
+
return; // exists as real dir, handled by caller (backup)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Ensure parent dir for the symlink target exists
|
|
143
|
+
if (!existsSync(target)) {
|
|
144
|
+
mkdirSync(target, { recursive: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
symlinkSync(target, GLOBAL_AW_DIR);
|
|
148
|
+
}
|
|
149
|
+
|
|
122
150
|
export async function initCommand(args) {
|
|
123
151
|
const namespace = args['--namespace'] || null;
|
|
124
152
|
let user = args['--user'] || '';
|
|
@@ -176,7 +204,7 @@ export async function initCommand(args) {
|
|
|
176
204
|
}
|
|
177
205
|
|
|
178
206
|
const cwd = process.cwd();
|
|
179
|
-
const hasGitRepo = existsSync(
|
|
207
|
+
const hasGitRepo = existsSync(GLOBAL_GIT_DIR) && existsSync(join(GLOBAL_GIT_DIR, '.git'));
|
|
180
208
|
const hasConfig = config.exists(GLOBAL_AW_DIR);
|
|
181
209
|
|
|
182
210
|
// ── Fast path: already initialized → just pull + link ─────────────────
|
|
@@ -186,38 +214,38 @@ export async function initCommand(args) {
|
|
|
186
214
|
const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
|
|
187
215
|
|
|
188
216
|
await withLock(GLOBAL_AW_DIR, async () => {
|
|
189
|
-
// Pull latest
|
|
190
217
|
if (!silent) fmt.logStep('Syncing registry...');
|
|
191
218
|
try {
|
|
192
219
|
const { updated, conflicts } = gitPull(GLOBAL_AW_DIR);
|
|
193
220
|
if (conflicts && !silent) {
|
|
194
|
-
fmt.logWarn('Merge conflicts detected — resolve manually in ~/.
|
|
221
|
+
fmt.logWarn('Merge conflicts detected — resolve manually in ~/.aw_repo/');
|
|
195
222
|
}
|
|
196
223
|
} catch (e) {
|
|
197
224
|
if (!silent) fmt.logWarn(`Pull failed: ${e.message}`);
|
|
198
225
|
}
|
|
199
226
|
|
|
200
|
-
// Add new namespace if needed
|
|
201
227
|
if (isNewSubTeam) {
|
|
202
228
|
if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
|
|
203
229
|
|
|
204
|
-
// Check if namespace exists in remote
|
|
205
230
|
const nsExists = gitLsRemote(GLOBAL_AW_DIR, folderName);
|
|
206
231
|
if (nsExists) {
|
|
207
232
|
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/${folderName}`]);
|
|
208
233
|
} else {
|
|
209
|
-
// Template bootstrap
|
|
210
234
|
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/[template]`]);
|
|
211
|
-
// Pull to get template content
|
|
212
235
|
try { gitPull(GLOBAL_AW_DIR); } catch { /* already up to date */ }
|
|
213
236
|
bootstrapFromTemplate(folderName, teamNS);
|
|
237
|
+
// Remove [template] from sparse checkout
|
|
238
|
+
const currentPaths = gitSparseList(GLOBAL_AW_DIR);
|
|
239
|
+
const cleanPaths = currentPaths.filter(p => !p.includes('[template]'));
|
|
240
|
+
if (cleanPaths.length !== currentPaths.length) {
|
|
241
|
+
gitSparseSet(GLOBAL_AW_DIR, cleanPaths);
|
|
242
|
+
}
|
|
214
243
|
}
|
|
215
244
|
|
|
216
245
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
217
246
|
}
|
|
218
247
|
}, { skipIfLocked: silent });
|
|
219
248
|
|
|
220
|
-
// Re-link IDE dirs + hooks (idempotent)
|
|
221
249
|
ensureDocsSymlink();
|
|
222
250
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
223
251
|
linkWorkspace(HOME);
|
|
@@ -228,7 +256,6 @@ export async function initCommand(args) {
|
|
|
228
256
|
if (cwd !== HOME) setupMcp(cwd, freshCfg?.namespace || team);
|
|
229
257
|
installGlobalHooks();
|
|
230
258
|
|
|
231
|
-
// Link current project if needed
|
|
232
259
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
233
260
|
try {
|
|
234
261
|
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
@@ -252,11 +279,21 @@ export async function initCommand(args) {
|
|
|
252
279
|
|
|
253
280
|
// ── Full init: first time setup ───────────────────────────────────────
|
|
254
281
|
|
|
255
|
-
// Backup old non-git ~/.aw_registry/ if it
|
|
256
|
-
if (existsSync(GLOBAL_AW_DIR)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
282
|
+
// Backup old non-git ~/.aw_registry/ if it's a real directory (not a symlink)
|
|
283
|
+
if (existsSync(GLOBAL_AW_DIR)) {
|
|
284
|
+
try {
|
|
285
|
+
if (!lstatSync(GLOBAL_AW_DIR).isSymbolicLink()) {
|
|
286
|
+
const backupDir = `${GLOBAL_AW_DIR}.bak-${new Date().toISOString().slice(0, 10)}`;
|
|
287
|
+
fmt.logWarn(`Existing ~/.aw_registry/ directory found — backing up to ${backupDir}`);
|
|
288
|
+
renameSync(GLOBAL_AW_DIR, backupDir);
|
|
289
|
+
}
|
|
290
|
+
} catch { /* */ }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Backup old ~/.aw_repo/ if it exists but isn't a git repo
|
|
294
|
+
if (existsSync(GLOBAL_GIT_DIR) && !existsSync(join(GLOBAL_GIT_DIR, '.git'))) {
|
|
295
|
+
const backupDir = `${GLOBAL_GIT_DIR}.bak-${new Date().toISOString().slice(0, 10)}`;
|
|
296
|
+
renameSync(GLOBAL_GIT_DIR, backupDir);
|
|
260
297
|
}
|
|
261
298
|
|
|
262
299
|
// Auto-detect user
|
|
@@ -266,8 +303,6 @@ export async function initCommand(args) {
|
|
|
266
303
|
} catch { /* git not configured */ }
|
|
267
304
|
}
|
|
268
305
|
|
|
269
|
-
const cfg = config.exists(GLOBAL_AW_DIR) ? config.load(GLOBAL_AW_DIR) : null;
|
|
270
|
-
|
|
271
306
|
fmt.note([
|
|
272
307
|
`${chalk.dim('source:')} ~/.aw_registry/`,
|
|
273
308
|
folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
|
|
@@ -275,7 +310,7 @@ export async function initCommand(args) {
|
|
|
275
310
|
`${chalk.dim('version:')} v${VERSION}`,
|
|
276
311
|
].filter(Boolean).join('\n'), 'Config created');
|
|
277
312
|
|
|
278
|
-
// Step 1: Clone registry
|
|
313
|
+
// Step 1: Clone registry to ~/.aw_repo/
|
|
279
314
|
const s = fmt.spinner();
|
|
280
315
|
const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
|
|
281
316
|
s.start(`Cloning registry (${pullTargets})...`);
|
|
@@ -283,18 +318,27 @@ export async function initCommand(args) {
|
|
|
283
318
|
const initialPaths = includeToSparsePaths(['platform']);
|
|
284
319
|
|
|
285
320
|
try {
|
|
286
|
-
|
|
287
|
-
gitInit(repo, REGISTRY_BASE_BRANCH, GLOBAL_AW_DIR, initialPaths);
|
|
321
|
+
gitInit(REGISTRY_REPO, REGISTRY_BASE_BRANCH, GLOBAL_GIT_DIR, initialPaths);
|
|
288
322
|
s.stop('Repository cloned');
|
|
289
323
|
} catch (e) {
|
|
290
324
|
s.stop(chalk.red('Clone failed'));
|
|
291
325
|
fmt.cancel(e.message);
|
|
292
326
|
}
|
|
293
327
|
|
|
294
|
-
// Step 2:
|
|
328
|
+
// Step 2: Symlink ~/.aw_registry → ~/.aw_repo/.aw_registry
|
|
329
|
+
ensureWorkspaceSymlink();
|
|
330
|
+
|
|
331
|
+
// Step 3: Add git excludes for local config files
|
|
332
|
+
addGitExcludes(GLOBAL_GIT_DIR, [
|
|
333
|
+
`${REGISTRY_DIR}/.sync-config.json`,
|
|
334
|
+
`${REGISTRY_DIR}/.aw-manifest.json`,
|
|
335
|
+
'.aw-lock/',
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
// Step 4: Write config (goes into the symlinked dir)
|
|
295
339
|
const newCfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
296
340
|
|
|
297
|
-
// Step
|
|
341
|
+
// Step 5: Add namespace via sparse-checkout
|
|
298
342
|
if (folderName) {
|
|
299
343
|
const s2 = fmt.spinner();
|
|
300
344
|
s2.start(`Setting up namespace ${chalk.cyan(folderName)}...`);
|
|
@@ -304,24 +348,29 @@ export async function initCommand(args) {
|
|
|
304
348
|
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/${folderName}`]);
|
|
305
349
|
s2.stop(`Namespace ${chalk.cyan(folderName)} checked out`);
|
|
306
350
|
} else {
|
|
307
|
-
//
|
|
351
|
+
// Temporarily add [template] to sparse checkout, bootstrap, then remove it
|
|
308
352
|
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/[template]`]);
|
|
309
353
|
bootstrapFromTemplate(folderName, teamNS);
|
|
354
|
+
// Remove [template] from sparse checkout — no longer needed
|
|
355
|
+
const currentPaths = gitSparseList(GLOBAL_AW_DIR);
|
|
356
|
+
const cleanPaths = currentPaths.filter(p => !p.includes('[template]'));
|
|
357
|
+
if (cleanPaths.length !== currentPaths.length) {
|
|
358
|
+
gitSparseSet(GLOBAL_AW_DIR, cleanPaths);
|
|
359
|
+
}
|
|
310
360
|
s2.stop(`Namespace ${chalk.cyan(folderName)} bootstrapped from template`);
|
|
311
361
|
}
|
|
312
362
|
|
|
313
363
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
314
364
|
}
|
|
315
365
|
|
|
316
|
-
// Always track platform
|
|
317
366
|
if (!newCfg.include.includes('platform')) {
|
|
318
367
|
config.addPattern(GLOBAL_AW_DIR, 'platform');
|
|
319
368
|
}
|
|
320
369
|
|
|
321
|
-
// Step
|
|
370
|
+
// Step 6: Docs symlink
|
|
322
371
|
ensureDocsSymlink();
|
|
323
372
|
|
|
324
|
-
// Step
|
|
373
|
+
// Step 7: Link IDE dirs + setup tasks
|
|
325
374
|
fmt.logStep('Linking IDE symlinks...');
|
|
326
375
|
linkWorkspace(HOME);
|
|
327
376
|
generateCommands(HOME);
|
|
@@ -332,7 +381,7 @@ export async function initCommand(args) {
|
|
|
332
381
|
const hooksInstalled = installGlobalHooks();
|
|
333
382
|
installIdeTasks();
|
|
334
383
|
|
|
335
|
-
// Step
|
|
384
|
+
// Step 8: Symlink in current directory
|
|
336
385
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
337
386
|
try {
|
|
338
387
|
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
@@ -340,11 +389,12 @@ export async function initCommand(args) {
|
|
|
340
389
|
} catch { /* best effort */ }
|
|
341
390
|
}
|
|
342
391
|
|
|
343
|
-
// Step
|
|
392
|
+
// Step 9: Write install manifest for nuke cleanup
|
|
344
393
|
const manifest = {
|
|
345
394
|
version: 2,
|
|
346
395
|
installedAt: new Date().toISOString(),
|
|
347
396
|
globalDir: GLOBAL_AW_DIR,
|
|
397
|
+
gitDir: GLOBAL_GIT_DIR,
|
|
348
398
|
createdFiles: [
|
|
349
399
|
...instructionFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
350
400
|
...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
@@ -353,10 +403,8 @@ export async function initCommand(args) {
|
|
|
353
403
|
};
|
|
354
404
|
saveManifest(manifest);
|
|
355
405
|
|
|
356
|
-
// Offer to update if a newer version is available
|
|
357
406
|
await promptUpdate(await args._updateCheck);
|
|
358
407
|
|
|
359
|
-
// Done
|
|
360
408
|
fmt.outro([
|
|
361
409
|
'Install complete',
|
|
362
410
|
'',
|
package/commands/nuke.mjs
CHANGED
|
@@ -286,8 +286,20 @@ export function nukeCommand(args) {
|
|
|
286
286
|
} catch { /* not installed via npm or no permissions */ }
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
// 9. Remove ~/.
|
|
290
|
-
|
|
289
|
+
// 9. Remove ~/.aw_repo/ (git clone) and ~/.aw_registry/ (symlink or dir)
|
|
290
|
+
const GLOBAL_GIT_DIR = join(HOME, '.aw_repo');
|
|
291
|
+
if (existsSync(GLOBAL_GIT_DIR)) {
|
|
292
|
+
rmSync(GLOBAL_GIT_DIR, { recursive: true, force: true });
|
|
293
|
+
fmt.logStep('Removed ~/.aw_repo/');
|
|
294
|
+
}
|
|
295
|
+
// Remove ~/.aw_registry/ — may be a symlink (new) or real dir (legacy)
|
|
296
|
+
try {
|
|
297
|
+
if (lstatSync(GLOBAL_AW_DIR).isSymbolicLink()) {
|
|
298
|
+
unlinkSync(GLOBAL_AW_DIR);
|
|
299
|
+
} else {
|
|
300
|
+
rmSync(GLOBAL_AW_DIR, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
} catch { rmSync(GLOBAL_AW_DIR, { recursive: true, force: true }); }
|
|
291
303
|
fmt.logStep('Removed ~/.aw_registry/');
|
|
292
304
|
|
|
293
305
|
fmt.outro([
|
package/commands/pull.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import * as config from '../config.mjs';
|
|
|
11
11
|
import * as fmt from '../fmt.mjs';
|
|
12
12
|
import { chalk } from '../fmt.mjs';
|
|
13
13
|
import {
|
|
14
|
-
gitPull, gitSparseAdd, gitLsRemote, isGitRepo, withLock,
|
|
14
|
+
gitPull, gitSparseAdd, gitLsRemote, isGitRepo, withLock, resolveRepoRoot,
|
|
15
15
|
} from '../git.mjs';
|
|
16
16
|
import { REGISTRY_BASE_BRANCH, REGISTRY_DIR } from '../constants.mjs';
|
|
17
17
|
import { resolveInput } from '../paths.mjs';
|
|
@@ -165,10 +165,11 @@ export async function pullCommand(args) {
|
|
|
165
165
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
166
166
|
|
|
167
167
|
function dryRunDiff(workspaceDir, log) {
|
|
168
|
+
const repoDir = resolveRepoRoot(workspaceDir);
|
|
168
169
|
try {
|
|
169
|
-
execSync('git fetch origin', { cwd:
|
|
170
|
+
execSync('git fetch origin', { cwd: repoDir, stdio: 'pipe' });
|
|
170
171
|
const diff = execSync(`git diff HEAD..origin/${REGISTRY_BASE_BRANCH} --stat`, {
|
|
171
|
-
cwd:
|
|
172
|
+
cwd: repoDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
172
173
|
});
|
|
173
174
|
|
|
174
175
|
if (diff.trim()) {
|
|
@@ -184,9 +185,10 @@ function dryRunDiff(workspaceDir, log) {
|
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
function printConflicts(workspaceDir, log) {
|
|
188
|
+
const repoDir = resolveRepoRoot(workspaceDir);
|
|
187
189
|
try {
|
|
188
190
|
const output = execSync('git diff --name-only --diff-filter=U', {
|
|
189
|
-
cwd:
|
|
191
|
+
cwd: repoDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
190
192
|
});
|
|
191
193
|
const conflictFiles = output.trim().split('\n').filter(Boolean);
|
|
192
194
|
|
|
@@ -195,7 +197,7 @@ function printConflicts(workspaceDir, log) {
|
|
|
195
197
|
log.note(
|
|
196
198
|
lines + '\n\n' +
|
|
197
199
|
chalk.dim('Resolve conflicts, then run:\n') +
|
|
198
|
-
chalk.dim(' cd ~/.
|
|
200
|
+
chalk.dim(' cd ~/.aw_repo && git add -A && git merge --continue'),
|
|
199
201
|
chalk.red('Merge Conflicts')
|
|
200
202
|
);
|
|
201
203
|
}
|
package/commands/push.mjs
CHANGED
|
@@ -87,11 +87,13 @@ function collectModifiedFiles(workspaceDir) {
|
|
|
87
87
|
// Only files under .aw_registry/
|
|
88
88
|
if (!entry.path.startsWith(REGISTRY_DIR + '/')) continue;
|
|
89
89
|
|
|
90
|
+
// regRelPath is relative to .aw_registry/ (e.g. "platform/agents/foo.md")
|
|
90
91
|
const regRelPath = entry.path.slice(REGISTRY_DIR.length + 1);
|
|
91
92
|
const meta = parseRegistryPath(regRelPath);
|
|
92
93
|
if (!meta) continue;
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
// workspaceDir IS .aw_registry/, so absPath uses regRelPath (not the full git-relative path)
|
|
96
|
+
const absPath = join(workspaceDir, regRelPath);
|
|
95
97
|
if (!existsSync(absPath)) continue;
|
|
96
98
|
|
|
97
99
|
files.push({
|
package/commands/status.mjs
CHANGED
|
@@ -138,7 +138,7 @@ export function statusCommand(args) {
|
|
|
138
138
|
|
|
139
139
|
// Hints
|
|
140
140
|
if (conflicts.length > 0) {
|
|
141
|
-
fmt.logWarn(`Resolve conflicts: ${chalk.dim('cd ~/.
|
|
141
|
+
fmt.logWarn(`Resolve conflicts: ${chalk.dim('cd ~/.aw_repo && git diff --name-only --diff-filter=U')}`);
|
|
142
142
|
}
|
|
143
143
|
if (untracked.length > 0 || modified.length > 0) {
|
|
144
144
|
fmt.logInfo(`Push changes: ${chalk.dim('aw push')} or ${chalk.dim('aw push <path>')}`);
|
package/git.mjs
CHANGED
|
@@ -1,28 +1,63 @@
|
|
|
1
|
-
// git.mjs — Git-native sync engine. Persistent sparse checkout
|
|
1
|
+
// git.mjs — Git-native sync engine. Persistent sparse checkout.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Layout:
|
|
4
|
+
// ~/.aw_repo/ — the git clone (repo root, has .git/)
|
|
5
|
+
// ~/.aw_registry/ — symlink → ~/.aw_repo/.aw_registry/
|
|
6
|
+
//
|
|
7
|
+
// All git commands run at the repo root. Callers pass workspaceDir
|
|
8
|
+
// (~/.aw_registry/) and functions auto-resolve to the repo root.
|
|
5
9
|
|
|
6
10
|
import { execSync } from 'node:child_process';
|
|
7
|
-
import { existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
|
|
8
|
-
import { join } from 'node:path';
|
|
11
|
+
import { existsSync, mkdirSync, rmSync, statSync, realpathSync, readFileSync, appendFileSync } from 'node:fs';
|
|
12
|
+
import { join, dirname } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
9
14
|
import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR } from './constants.mjs';
|
|
10
15
|
|
|
16
|
+
/** Where the git repo is cloned (the actual .git/ lives here). */
|
|
17
|
+
export const GLOBAL_GIT_DIR = join(homedir(), '.aw_repo');
|
|
18
|
+
|
|
19
|
+
// ── Repo root resolution ─────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a workspace/target dir to the git repo root (where .git/ lives).
|
|
23
|
+
*
|
|
24
|
+
* - ~/.aw_repo/ → ~/.aw_repo/ (direct)
|
|
25
|
+
* - ~/.aw_registry/ (symlink → ~/.aw_repo/.aw_registry/) → ~/.aw_repo/
|
|
26
|
+
* - Any dir with .git/ → that dir
|
|
27
|
+
*/
|
|
28
|
+
export function resolveRepoRoot(dir) {
|
|
29
|
+
// Direct .git check
|
|
30
|
+
if (existsSync(join(dir, '.git'))) return dir;
|
|
31
|
+
|
|
32
|
+
// Resolve symlinks and walk up to find .git/
|
|
33
|
+
try {
|
|
34
|
+
let resolved = realpathSync(dir);
|
|
35
|
+
for (let i = 0; i < 3; i++) {
|
|
36
|
+
const parent = dirname(resolved);
|
|
37
|
+
if (parent === resolved) break; // reached filesystem root
|
|
38
|
+
if (existsSync(join(parent, '.git'))) return parent;
|
|
39
|
+
resolved = parent;
|
|
40
|
+
}
|
|
41
|
+
} catch { /* not a valid path */ }
|
|
42
|
+
|
|
43
|
+
return dir; // fallback — caller will get a git error if it's not a repo
|
|
44
|
+
}
|
|
45
|
+
|
|
11
46
|
// ── Core operations ──────────────────────────────────────────────────
|
|
12
47
|
|
|
13
48
|
/**
|
|
14
|
-
* Clone + sparse-checkout init. Creates a persistent repo at
|
|
49
|
+
* Clone + sparse-checkout init. Creates a persistent repo at gitDir.
|
|
15
50
|
*
|
|
16
51
|
* @param {string} repo — GitHub owner/name or full URL
|
|
17
52
|
* @param {string} branch — branch to checkout
|
|
18
|
-
* @param {string}
|
|
53
|
+
* @param {string} gitDir — repo root (e.g. ~/.aw_repo)
|
|
19
54
|
* @param {string[]} sparsePaths — initial sparse-checkout paths
|
|
20
55
|
*/
|
|
21
|
-
export function gitInit(repo, branch,
|
|
56
|
+
export function gitInit(repo, branch, gitDir, sparsePaths) {
|
|
22
57
|
const repoUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
|
|
23
58
|
|
|
24
59
|
try {
|
|
25
|
-
execSync(`git clone --filter=blob:none --sparse "${repoUrl}" "${
|
|
60
|
+
execSync(`git clone --filter=blob:none --sparse "${repoUrl}" "${gitDir}"`, {
|
|
26
61
|
stdio: 'pipe',
|
|
27
62
|
});
|
|
28
63
|
} catch (e) {
|
|
@@ -30,13 +65,13 @@ export function gitInit(repo, branch, targetDir, sparsePaths) {
|
|
|
30
65
|
}
|
|
31
66
|
|
|
32
67
|
try {
|
|
33
|
-
execSync('git sparse-checkout init --cone', { cwd:
|
|
68
|
+
execSync('git sparse-checkout init --cone', { cwd: gitDir, stdio: 'pipe' });
|
|
34
69
|
if (sparsePaths.length > 0) {
|
|
35
70
|
execSync(`git sparse-checkout set --skip-checks ${sparsePaths.map(p => `"${p}"`).join(' ')}`, {
|
|
36
|
-
cwd:
|
|
71
|
+
cwd: gitDir, stdio: 'pipe',
|
|
37
72
|
});
|
|
38
73
|
}
|
|
39
|
-
execSync(`git checkout ${branch}`, { cwd:
|
|
74
|
+
execSync(`git checkout ${branch}`, { cwd: gitDir, stdio: 'pipe' });
|
|
40
75
|
} catch (e) {
|
|
41
76
|
throw new Error(`Failed sparse checkout: ${e.message}`);
|
|
42
77
|
}
|
|
@@ -44,12 +79,14 @@ export function gitInit(repo, branch, targetDir, sparsePaths) {
|
|
|
44
79
|
|
|
45
80
|
/**
|
|
46
81
|
* Fetch + merge. Returns { updated, conflicts, summary }.
|
|
82
|
+
* @param {string} targetDir — workspace or git dir (auto-resolved)
|
|
47
83
|
*/
|
|
48
84
|
export function gitPull(targetDir) {
|
|
85
|
+
const repoDir = resolveRepoRoot(targetDir);
|
|
49
86
|
const branch = REGISTRY_BASE_BRANCH;
|
|
50
87
|
|
|
51
88
|
try {
|
|
52
|
-
execSync('git fetch origin', { cwd:
|
|
89
|
+
execSync('git fetch origin', { cwd: repoDir, stdio: 'pipe' });
|
|
53
90
|
} catch (e) {
|
|
54
91
|
throw new Error(`Fetch failed: ${e.message}`);
|
|
55
92
|
}
|
|
@@ -60,11 +97,10 @@ export function gitPull(targetDir) {
|
|
|
60
97
|
|
|
61
98
|
try {
|
|
62
99
|
summary = execSync(`git merge origin/${branch} --no-edit`, {
|
|
63
|
-
cwd:
|
|
100
|
+
cwd: repoDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
64
101
|
});
|
|
65
102
|
updated = !summary.includes('Already up to date');
|
|
66
103
|
} catch (e) {
|
|
67
|
-
// Merge conflict
|
|
68
104
|
const stderr = e.stderr?.toString() || '';
|
|
69
105
|
const stdout = e.stdout?.toString() || '';
|
|
70
106
|
if (stderr.includes('CONFLICT') || stdout.includes('CONFLICT')) {
|
|
@@ -80,12 +116,13 @@ export function gitPull(targetDir) {
|
|
|
80
116
|
}
|
|
81
117
|
|
|
82
118
|
/**
|
|
83
|
-
* Add paths to sparse-checkout (additive
|
|
119
|
+
* Add paths to sparse-checkout (additive).
|
|
84
120
|
*/
|
|
85
121
|
export function gitSparseAdd(targetDir, paths) {
|
|
86
122
|
if (paths.length === 0) return;
|
|
123
|
+
const repoDir = resolveRepoRoot(targetDir);
|
|
87
124
|
execSync(`git sparse-checkout add --skip-checks ${paths.map(p => `"${p}"`).join(' ')}`, {
|
|
88
|
-
cwd:
|
|
125
|
+
cwd: repoDir, stdio: 'pipe',
|
|
89
126
|
});
|
|
90
127
|
}
|
|
91
128
|
|
|
@@ -93,8 +130,9 @@ export function gitSparseAdd(targetDir, paths) {
|
|
|
93
130
|
* Replace sparse-checkout paths entirely.
|
|
94
131
|
*/
|
|
95
132
|
export function gitSparseSet(targetDir, paths) {
|
|
133
|
+
const repoDir = resolveRepoRoot(targetDir);
|
|
96
134
|
execSync(`git sparse-checkout set --skip-checks ${paths.map(p => `"${p}"`).join(' ')}`, {
|
|
97
|
-
cwd:
|
|
135
|
+
cwd: repoDir, stdio: 'pipe',
|
|
98
136
|
});
|
|
99
137
|
}
|
|
100
138
|
|
|
@@ -102,9 +140,10 @@ export function gitSparseSet(targetDir, paths) {
|
|
|
102
140
|
* Get sparse-checkout list.
|
|
103
141
|
*/
|
|
104
142
|
export function gitSparseList(targetDir) {
|
|
143
|
+
const repoDir = resolveRepoRoot(targetDir);
|
|
105
144
|
try {
|
|
106
145
|
const output = execSync('git sparse-checkout list', {
|
|
107
|
-
cwd:
|
|
146
|
+
cwd: repoDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
108
147
|
});
|
|
109
148
|
return output.trim().split('\n').filter(Boolean);
|
|
110
149
|
} catch {
|
|
@@ -114,12 +153,13 @@ export function gitSparseList(targetDir) {
|
|
|
114
153
|
|
|
115
154
|
/**
|
|
116
155
|
* Git porcelain status. Returns array of { status, path }.
|
|
117
|
-
*
|
|
156
|
+
* Paths are relative to repo root (e.g. ".aw_registry/platform/agents/foo.md").
|
|
118
157
|
*/
|
|
119
158
|
export function gitStatus(targetDir) {
|
|
159
|
+
const repoDir = resolveRepoRoot(targetDir);
|
|
120
160
|
try {
|
|
121
161
|
const output = execSync('git status --porcelain', {
|
|
122
|
-
cwd:
|
|
162
|
+
cwd: repoDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
123
163
|
});
|
|
124
164
|
if (!output.trim()) return [];
|
|
125
165
|
return output.trim().split('\n').map(line => {
|
|
@@ -134,12 +174,12 @@ export function gitStatus(targetDir) {
|
|
|
134
174
|
|
|
135
175
|
/**
|
|
136
176
|
* Check if a path exists in the remote tree (without checking it out).
|
|
137
|
-
* Used to detect if a namespace already exists in the registry.
|
|
138
177
|
*/
|
|
139
178
|
export function gitLsRemote(targetDir, path) {
|
|
179
|
+
const repoDir = resolveRepoRoot(targetDir);
|
|
140
180
|
try {
|
|
141
181
|
const output = execSync(`git ls-tree -d HEAD "${REGISTRY_DIR}/${path}"`, {
|
|
142
|
-
cwd:
|
|
182
|
+
cwd: repoDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
143
183
|
});
|
|
144
184
|
return output.trim().length > 0;
|
|
145
185
|
} catch {
|
|
@@ -148,10 +188,13 @@ export function gitLsRemote(targetDir, path) {
|
|
|
148
188
|
}
|
|
149
189
|
|
|
150
190
|
/**
|
|
151
|
-
* Check if targetDir is a git repo.
|
|
191
|
+
* Check if targetDir (or its resolved parent) is backed by a git repo.
|
|
152
192
|
*/
|
|
153
193
|
export function isGitRepo(targetDir) {
|
|
154
|
-
|
|
194
|
+
if (existsSync(join(targetDir, '.git'))) return true;
|
|
195
|
+
// Check if it's a symlink into a git repo
|
|
196
|
+
const resolved = resolveRepoRoot(targetDir);
|
|
197
|
+
return existsSync(join(resolved, '.git'));
|
|
155
198
|
}
|
|
156
199
|
|
|
157
200
|
// ── Concurrency lock ─────────────────────────────────────────────────
|
|
@@ -160,15 +203,12 @@ const LOCK_DIR_NAME = '.aw-lock';
|
|
|
160
203
|
const LOCK_STALE_MS = 30_000;
|
|
161
204
|
|
|
162
205
|
/**
|
|
163
|
-
* Acquire a directory-based lock, run fn, release.
|
|
164
|
-
* @param {string} targetDir
|
|
165
|
-
* @param {Function} fn — async or sync function to run under lock
|
|
166
|
-
* @param {{ skipIfLocked?: boolean }} opts
|
|
206
|
+
* Acquire a directory-based lock on the git repo, run fn, release.
|
|
167
207
|
*/
|
|
168
208
|
export async function withLock(targetDir, fn, { skipIfLocked = false } = {}) {
|
|
169
|
-
const
|
|
209
|
+
const repoDir = resolveRepoRoot(targetDir);
|
|
210
|
+
const lockDir = join(repoDir, LOCK_DIR_NAME);
|
|
170
211
|
|
|
171
|
-
// Check for stale lock
|
|
172
212
|
if (existsSync(lockDir)) {
|
|
173
213
|
try {
|
|
174
214
|
const age = Date.now() - statSync(lockDir).mtimeMs;
|
|
@@ -177,7 +217,6 @@ export async function withLock(targetDir, fn, { skipIfLocked = false } = {}) {
|
|
|
177
217
|
} else if (skipIfLocked) {
|
|
178
218
|
return null;
|
|
179
219
|
} else {
|
|
180
|
-
// Wait briefly then retry
|
|
181
220
|
await new Promise(r => setTimeout(r, 1000));
|
|
182
221
|
if (existsSync(lockDir)) {
|
|
183
222
|
throw new Error('Another aw instance is running. Retry in a moment.');
|
|
@@ -205,14 +244,24 @@ export async function withLock(targetDir, fn, { skipIfLocked = false } = {}) {
|
|
|
205
244
|
}
|
|
206
245
|
}
|
|
207
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Add patterns to .git/info/exclude so local config files don't show in git status.
|
|
249
|
+
*/
|
|
250
|
+
export function addGitExcludes(gitDir, patterns) {
|
|
251
|
+
const excludePath = join(gitDir, '.git', 'info', 'exclude');
|
|
252
|
+
try {
|
|
253
|
+
const existing = existsSync(excludePath) ? readFileSync(excludePath, 'utf8') : '';
|
|
254
|
+
const toAdd = patterns.filter(p => !existing.includes(p));
|
|
255
|
+
if (toAdd.length > 0) {
|
|
256
|
+
appendFileSync(excludePath, '\n# aw local config\n' + toAdd.join('\n') + '\n');
|
|
257
|
+
}
|
|
258
|
+
} catch { /* best effort */ }
|
|
259
|
+
}
|
|
260
|
+
|
|
208
261
|
// ── Path helpers ─────────────────────────────────────────────────────
|
|
209
262
|
|
|
210
263
|
/**
|
|
211
264
|
* Compute sparse checkout paths from include paths.
|
|
212
|
-
* e.g., ["platform", "dev/agents/debugger"] -> [".aw_registry/platform", ".aw_registry/dev/agents/debugger"]
|
|
213
|
-
*
|
|
214
|
-
* When "platform" is in the paths, also includes the repo's docs source
|
|
215
|
-
* directory (content/) so docs are pulled on-the-fly into platform/docs/.
|
|
216
265
|
*/
|
|
217
266
|
export function includeToSparsePaths(paths) {
|
|
218
267
|
const result = new Set();
|
package/link.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const ALL_KNOWN_TYPES = new Set([...FILE_TYPES, 'skills', 'commands', 'evals', '
|
|
|
21
21
|
function listNamespaceDirs(awDir) {
|
|
22
22
|
if (!existsSync(awDir)) return [];
|
|
23
23
|
return readdirSync(awDir, { withFileTypes: true })
|
|
24
|
-
.filter(d => d.isDirectory() && !d.name.startsWith('.'))
|
|
24
|
+
.filter(d => d.isDirectory() && !d.name.startsWith('.') && d.name !== '[template]')
|
|
25
25
|
.map(d => d.name);
|
|
26
26
|
}
|
|
27
27
|
|