@ghl-ai/aw 0.1.34-beta.10 → 0.1.34-beta.11
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 +76 -41
- 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/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';
|
|
@@ -12,7 +16,8 @@ import * as fmt from '../fmt.mjs';
|
|
|
12
16
|
import { chalk } from '../fmt.mjs';
|
|
13
17
|
import {
|
|
14
18
|
gitInit, gitPull, gitSparseAdd, gitLsRemote,
|
|
15
|
-
isGitRepo, withLock, includeToSparsePaths,
|
|
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,29 +214,24 @@ 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);
|
|
214
237
|
}
|
|
@@ -217,7 +240,6 @@ export async function initCommand(args) {
|
|
|
217
240
|
}
|
|
218
241
|
}, { skipIfLocked: silent });
|
|
219
242
|
|
|
220
|
-
// Re-link IDE dirs + hooks (idempotent)
|
|
221
243
|
ensureDocsSymlink();
|
|
222
244
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
223
245
|
linkWorkspace(HOME);
|
|
@@ -228,7 +250,6 @@ export async function initCommand(args) {
|
|
|
228
250
|
if (cwd !== HOME) setupMcp(cwd, freshCfg?.namespace || team);
|
|
229
251
|
installGlobalHooks();
|
|
230
252
|
|
|
231
|
-
// Link current project if needed
|
|
232
253
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
233
254
|
try {
|
|
234
255
|
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
@@ -252,11 +273,21 @@ export async function initCommand(args) {
|
|
|
252
273
|
|
|
253
274
|
// ── Full init: first time setup ───────────────────────────────────────
|
|
254
275
|
|
|
255
|
-
// Backup old non-git ~/.aw_registry/ if it
|
|
256
|
-
if (existsSync(GLOBAL_AW_DIR)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
276
|
+
// Backup old non-git ~/.aw_registry/ if it's a real directory (not a symlink)
|
|
277
|
+
if (existsSync(GLOBAL_AW_DIR)) {
|
|
278
|
+
try {
|
|
279
|
+
if (!lstatSync(GLOBAL_AW_DIR).isSymbolicLink()) {
|
|
280
|
+
const backupDir = `${GLOBAL_AW_DIR}.bak-${new Date().toISOString().slice(0, 10)}`;
|
|
281
|
+
fmt.logWarn(`Existing ~/.aw_registry/ directory found — backing up to ${backupDir}`);
|
|
282
|
+
renameSync(GLOBAL_AW_DIR, backupDir);
|
|
283
|
+
}
|
|
284
|
+
} catch { /* */ }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Backup old ~/.aw_repo/ if it exists but isn't a git repo
|
|
288
|
+
if (existsSync(GLOBAL_GIT_DIR) && !existsSync(join(GLOBAL_GIT_DIR, '.git'))) {
|
|
289
|
+
const backupDir = `${GLOBAL_GIT_DIR}.bak-${new Date().toISOString().slice(0, 10)}`;
|
|
290
|
+
renameSync(GLOBAL_GIT_DIR, backupDir);
|
|
260
291
|
}
|
|
261
292
|
|
|
262
293
|
// Auto-detect user
|
|
@@ -266,8 +297,6 @@ export async function initCommand(args) {
|
|
|
266
297
|
} catch { /* git not configured */ }
|
|
267
298
|
}
|
|
268
299
|
|
|
269
|
-
const cfg = config.exists(GLOBAL_AW_DIR) ? config.load(GLOBAL_AW_DIR) : null;
|
|
270
|
-
|
|
271
300
|
fmt.note([
|
|
272
301
|
`${chalk.dim('source:')} ~/.aw_registry/`,
|
|
273
302
|
folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
|
|
@@ -275,7 +304,7 @@ export async function initCommand(args) {
|
|
|
275
304
|
`${chalk.dim('version:')} v${VERSION}`,
|
|
276
305
|
].filter(Boolean).join('\n'), 'Config created');
|
|
277
306
|
|
|
278
|
-
// Step 1: Clone registry
|
|
307
|
+
// Step 1: Clone registry to ~/.aw_repo/
|
|
279
308
|
const s = fmt.spinner();
|
|
280
309
|
const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
|
|
281
310
|
s.start(`Cloning registry (${pullTargets})...`);
|
|
@@ -283,18 +312,27 @@ export async function initCommand(args) {
|
|
|
283
312
|
const initialPaths = includeToSparsePaths(['platform']);
|
|
284
313
|
|
|
285
314
|
try {
|
|
286
|
-
|
|
287
|
-
gitInit(repo, REGISTRY_BASE_BRANCH, GLOBAL_AW_DIR, initialPaths);
|
|
315
|
+
gitInit(REGISTRY_REPO, REGISTRY_BASE_BRANCH, GLOBAL_GIT_DIR, initialPaths);
|
|
288
316
|
s.stop('Repository cloned');
|
|
289
317
|
} catch (e) {
|
|
290
318
|
s.stop(chalk.red('Clone failed'));
|
|
291
319
|
fmt.cancel(e.message);
|
|
292
320
|
}
|
|
293
321
|
|
|
294
|
-
// Step 2:
|
|
322
|
+
// Step 2: Symlink ~/.aw_registry → ~/.aw_repo/.aw_registry
|
|
323
|
+
ensureWorkspaceSymlink();
|
|
324
|
+
|
|
325
|
+
// Step 3: Add git excludes for local config files
|
|
326
|
+
addGitExcludes(GLOBAL_GIT_DIR, [
|
|
327
|
+
`${REGISTRY_DIR}/.sync-config.json`,
|
|
328
|
+
`${REGISTRY_DIR}/.aw-manifest.json`,
|
|
329
|
+
'.aw-lock/',
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
// Step 4: Write config (goes into the symlinked dir)
|
|
295
333
|
const newCfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
296
334
|
|
|
297
|
-
// Step
|
|
335
|
+
// Step 5: Add namespace via sparse-checkout
|
|
298
336
|
if (folderName) {
|
|
299
337
|
const s2 = fmt.spinner();
|
|
300
338
|
s2.start(`Setting up namespace ${chalk.cyan(folderName)}...`);
|
|
@@ -304,7 +342,6 @@ export async function initCommand(args) {
|
|
|
304
342
|
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/${folderName}`]);
|
|
305
343
|
s2.stop(`Namespace ${chalk.cyan(folderName)} checked out`);
|
|
306
344
|
} else {
|
|
307
|
-
// Template bootstrap
|
|
308
345
|
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/[template]`]);
|
|
309
346
|
bootstrapFromTemplate(folderName, teamNS);
|
|
310
347
|
s2.stop(`Namespace ${chalk.cyan(folderName)} bootstrapped from template`);
|
|
@@ -313,15 +350,14 @@ export async function initCommand(args) {
|
|
|
313
350
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
314
351
|
}
|
|
315
352
|
|
|
316
|
-
// Always track platform
|
|
317
353
|
if (!newCfg.include.includes('platform')) {
|
|
318
354
|
config.addPattern(GLOBAL_AW_DIR, 'platform');
|
|
319
355
|
}
|
|
320
356
|
|
|
321
|
-
// Step
|
|
357
|
+
// Step 6: Docs symlink
|
|
322
358
|
ensureDocsSymlink();
|
|
323
359
|
|
|
324
|
-
// Step
|
|
360
|
+
// Step 7: Link IDE dirs + setup tasks
|
|
325
361
|
fmt.logStep('Linking IDE symlinks...');
|
|
326
362
|
linkWorkspace(HOME);
|
|
327
363
|
generateCommands(HOME);
|
|
@@ -332,7 +368,7 @@ export async function initCommand(args) {
|
|
|
332
368
|
const hooksInstalled = installGlobalHooks();
|
|
333
369
|
installIdeTasks();
|
|
334
370
|
|
|
335
|
-
// Step
|
|
371
|
+
// Step 8: Symlink in current directory
|
|
336
372
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
337
373
|
try {
|
|
338
374
|
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
@@ -340,11 +376,12 @@ export async function initCommand(args) {
|
|
|
340
376
|
} catch { /* best effort */ }
|
|
341
377
|
}
|
|
342
378
|
|
|
343
|
-
// Step
|
|
379
|
+
// Step 9: Write install manifest for nuke cleanup
|
|
344
380
|
const manifest = {
|
|
345
381
|
version: 2,
|
|
346
382
|
installedAt: new Date().toISOString(),
|
|
347
383
|
globalDir: GLOBAL_AW_DIR,
|
|
384
|
+
gitDir: GLOBAL_GIT_DIR,
|
|
348
385
|
createdFiles: [
|
|
349
386
|
...instructionFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
350
387
|
...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
@@ -353,10 +390,8 @@ export async function initCommand(args) {
|
|
|
353
390
|
};
|
|
354
391
|
saveManifest(manifest);
|
|
355
392
|
|
|
356
|
-
// Offer to update if a newer version is available
|
|
357
393
|
await promptUpdate(await args._updateCheck);
|
|
358
394
|
|
|
359
|
-
// Done
|
|
360
395
|
fmt.outro([
|
|
361
396
|
'Install complete',
|
|
362
397
|
'',
|
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();
|