@ghl-ai/aw 0.1.36-beta.2 → 0.1.36-beta.21

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 CHANGED
@@ -7,7 +7,7 @@ import * as config from '../config.mjs';
7
7
  import * as fmt from '../fmt.mjs';
8
8
  import { chalk } from '../fmt.mjs';
9
9
  import { resolveInput } from '../paths.mjs';
10
- import { removeFromSparseCheckout, isValidClone } from '../git.mjs';
10
+ import { removeFromSparseCheckout, isValidClone, getLocalRegistryDir } from '../git.mjs';
11
11
  import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
12
12
  import { linkWorkspace } from '../link.mjs';
13
13
 
@@ -18,7 +18,7 @@ export function dropCommand(args) {
18
18
  const HOME = homedir();
19
19
  const AW_HOME = join(HOME, '.aw');
20
20
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
21
- const workspaceDir = join(cwd, '.aw_registry');
21
+ const workspaceDir = getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
22
22
 
23
23
  fmt.intro('aw drop');
24
24
 
package/commands/init.mjs CHANGED
@@ -4,12 +4,11 @@
4
4
  // Uses core.hooksPath (git-lfs pattern) for system-wide hook interception.
5
5
  // Uses IDE tasks for auto-pull on workspace open.
6
6
 
7
- import { existsSync, writeFileSync, symlinkSync, lstatSync, readdirSync } from 'node:fs';
7
+ import { existsSync, writeFileSync, symlinkSync, lstatSync, readdirSync, readFileSync, rmSync } from 'node:fs';
8
8
  import { execSync } from 'node:child_process';
9
- import { join, dirname } from 'node:path';
9
+ import { join, dirname, sep } from 'node:path';
10
10
  import { homedir } from 'node:os';
11
11
  import { fileURLToPath } from 'node:url';
12
- import { readFileSync } from 'node:fs';
13
12
  import * as config from '../config.mjs';
14
13
  import * as fmt from '../fmt.mjs';
15
14
  import { chalk } from '../fmt.mjs';
@@ -24,6 +23,8 @@ import {
24
23
  isValidClone,
25
24
  fetchAndMerge,
26
25
  addToSparseCheckout,
26
+ addProjectWorktree,
27
+ isWorktree,
27
28
  includeToSparsePaths,
28
29
  sparseCheckoutAsync,
29
30
  cleanup,
@@ -194,22 +195,40 @@ export async function initCommand(args) {
194
195
 
195
196
  const freshCfg = config.load(GLOBAL_AW_DIR);
196
197
 
197
- linkWorkspace(HOME);
198
198
  await installAwEcc(cwd, { silent });
199
- generateCommands(HOME);
200
199
  copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
201
200
  initAwDocs(HOME);
202
201
  await setupMcp(HOME, freshCfg?.namespace || team, { silent });
203
202
  if (cwd !== HOME) await setupMcp(cwd, freshCfg?.namespace || team, { silent });
204
203
  installGlobalHooks();
205
204
 
206
- if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
205
+ // Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath (creates stale .aw_registry symlink)
206
+ if (cwd !== HOME) {
207
+ const oldLocalHook = join(cwd, '.git', 'hooks', 'post-checkout');
208
+ try {
209
+ const content = readFileSync(oldLocalHook, 'utf8');
210
+ if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
211
+ rmSync(oldLocalHook);
212
+ if (!silent) fmt.logStep('Removed legacy .git/hooks/post-checkout');
213
+ }
214
+ } catch { /* not there, fine */ }
215
+ }
216
+
217
+ const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
218
+ if (cwd !== HOME && !isInsideAw && !isWorktree(join(cwd, '.aw'))) {
207
219
  try {
208
- symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
209
- if (!silent) fmt.logStep('Linked .aw_registry in current project');
220
+ addProjectWorktree(AW_HOME, cwd);
221
+ if (!silent) fmt.logStep('Linked current project as git worktree');
210
222
  } catch { /* best effort */ }
211
223
  }
212
224
 
225
+ // Wire ~/.claude/.cursor/.codex to the project's registry when in a project,
226
+ // so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
227
+ const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
228
+ const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
229
+ linkWorkspace(HOME, awDirForLinks);
230
+ generateCommands(HOME);
231
+
213
232
  if (silent) {
214
233
  autoUpdate(await args._updateCheck);
215
234
  } else {
@@ -218,7 +237,7 @@ export async function initCommand(args) {
218
237
  '',
219
238
  ` ${chalk.green('✓')} Registry updated`,
220
239
  ` ${chalk.green('✓')} IDE integration refreshed`,
221
- cwd !== HOME && existsSync(join(cwd, '.aw_registry')) ? ` ${chalk.green('✓')} Current project linked` : null,
240
+ cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Current project linked` : null,
222
241
  ].filter(Boolean).join('\n'));
223
242
  }
224
243
  return;
@@ -247,7 +266,7 @@ export async function initCommand(args) {
247
266
  }
248
267
 
249
268
  // Determine sparse paths
250
- const sparsePaths = [`.aw_registry/platform`, `content`, `.aw_registry/AW-PROTOCOL.md`];
269
+ const sparsePaths = [`.aw_registry/platform`, `content`, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
251
270
  if (folderName) {
252
271
  sparsePaths.push(`.aw_registry/${folderName}`);
253
272
  }
@@ -286,11 +305,8 @@ export async function initCommand(args) {
286
305
  config.addPattern(GLOBAL_AW_DIR, folderName);
287
306
  }
288
307
 
289
- // Step 3: Link IDE dirs + setup tasks
290
- fmt.logStep('Linking IDE symlinks...');
291
- linkWorkspace(HOME);
308
+ // Step 3: Setup tasks, MCP, hooks
292
309
  await installAwEcc(cwd, { silent });
293
- generateCommands(HOME);
294
310
  const instructionFiles = copyInstructions(HOME, null, team) || [];
295
311
  initAwDocs(HOME);
296
312
  const mcpFiles = await setupMcp(HOME, team) || [];
@@ -298,14 +314,34 @@ export async function initCommand(args) {
298
314
  const hooksInstalled = installGlobalHooks();
299
315
  installIdeTasks();
300
316
 
301
- // Step 4: Symlink in current directory if it's a git repo
302
- if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
317
+ // Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath
318
+ if (cwd !== HOME) {
319
+ const oldLocalHook = join(cwd, '.git', 'hooks', 'post-checkout');
303
320
  try {
304
- symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
305
- fmt.logStep('Linked .aw_registry in current project');
321
+ const content = readFileSync(oldLocalHook, 'utf8');
322
+ if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
323
+ rmSync(oldLocalHook);
324
+ }
325
+ } catch { /* not there, fine */ }
326
+ }
327
+
328
+ // Step 4: Link current project as a git worktree (gives IDE git panel)
329
+ const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
330
+ if (cwd !== HOME && !isInsideAw && !isWorktree(join(cwd, '.aw'))) {
331
+ try {
332
+ addProjectWorktree(AW_HOME, cwd);
333
+ fmt.logStep('Linked current project as git worktree');
306
334
  } catch { /* best effort */ }
307
335
  }
308
336
 
337
+ // Step 5: Wire ~/.claude/.cursor/.codex to the project's registry when in a project,
338
+ // so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
339
+ fmt.logStep('Linking IDE symlinks...');
340
+ const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
341
+ const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
342
+ linkWorkspace(HOME, awDirForLinks);
343
+ generateCommands(HOME);
344
+
309
345
  // Offer to update if a newer version is available
310
346
  await promptUpdate(await args._updateCheck);
311
347
 
@@ -317,7 +353,7 @@ export async function initCommand(args) {
317
353
  ` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
318
354
  hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
319
355
  ` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
320
- cwd !== HOME && existsSync(join(cwd, '.aw_registry')) ? ` ${chalk.green('✓')} Linked in current project` : null,
356
+ cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Linked in current project` : null,
321
357
  '',
322
358
  ` ${chalk.dim('Existing repos:')} ${chalk.bold('cd <project> && aw link')}`,
323
359
  ` ${chalk.dim('New clones:')} auto-linked via git hook`,
@@ -1,26 +1,66 @@
1
- // commands/link-project.mjs — Symlink ~/.aw_registry into current project
1
+ // commands/link-project.mjs — Link current project to registry via git worktree
2
2
 
3
- import { existsSync, symlinkSync } from 'node:fs';
3
+ import { existsSync, lstatSync, rmSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import * as fmt from '../fmt.mjs';
7
7
  import { chalk } from '../fmt.mjs';
8
+ import { addProjectWorktree, isWorktree, isValidClone } from '../git.mjs';
9
+ import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
10
+ import { linkWorkspace } from '../link.mjs';
11
+ import { generateCommands } from '../integrate.mjs';
8
12
 
9
- const GLOBAL_AW_DIR = join(homedir(), '.aw_registry');
13
+ const HOME = homedir();
14
+ const AW_HOME = join(HOME, '.aw');
10
15
 
11
16
  export function linkProjectCommand(args) {
12
17
  const cwd = process.cwd();
13
- const target = join(cwd, '.aw_registry');
14
18
 
15
- if (!existsSync(GLOBAL_AW_DIR)) {
16
- fmt.cancel('No global install found. Run: aw init');
19
+ fmt.intro('aw link');
20
+
21
+ const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
22
+ if (!isValidClone(AW_HOME, repoUrl)) {
23
+ fmt.cancel('Registry not initialized. Run: aw init');
24
+ return;
17
25
  }
18
26
 
19
- if (existsSync(target)) {
20
- fmt.logSuccess('.aw_registry already linked in this project');
27
+ if (cwd === HOME) {
28
+ fmt.cancel('Cannot link home directory as a project');
21
29
  return;
22
30
  }
23
31
 
24
- symlinkSync(GLOBAL_AW_DIR, target);
25
- fmt.logSuccess(`Linked .aw_registry → ~/.aw_registry/`);
32
+ const worktreeDir = join(cwd, '.aw');
33
+
34
+ // Remove stale project-root .aw_registry symlink from old installs
35
+ const staleSymlink = join(cwd, REGISTRY_DIR);
36
+ try { if (lstatSync(staleSymlink).isSymbolicLink()) rmSync(staleSymlink); } catch { /* fine */ }
37
+
38
+ if (isWorktree(worktreeDir)) {
39
+ // Worktree exists — refresh global IDE symlinks pointing to this project's registry
40
+ const projectRegistryDir = join(worktreeDir, REGISTRY_DIR);
41
+ const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
42
+ linkWorkspace(HOME, awDirForLinks);
43
+ generateCommands(HOME);
44
+ fmt.logSuccess(`Already linked — refreshed IDE symlinks`);
45
+ return;
46
+ }
47
+
48
+ try {
49
+ addProjectWorktree(AW_HOME, cwd);
50
+ const projectRegistryDir = join(worktreeDir, REGISTRY_DIR);
51
+ const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
52
+ linkWorkspace(HOME, awDirForLinks);
53
+ generateCommands(HOME);
54
+ fmt.logSuccess([
55
+ `Linked project as git worktree`,
56
+ '',
57
+ ` ${chalk.green('✓')} ${chalk.dim('.aw/')} git worktree (IDE git panel enabled)`,
58
+ ` ${chalk.green('✓')} ${chalk.dim(`.aw/${REGISTRY_DIR}/`)} registry content`,
59
+ ` ${chalk.green('✓')} ${chalk.dim('.claude/.cursor/.codex/')} IDE symlinks wired`,
60
+ ].join('\n'));
61
+ } catch (e) {
62
+ fmt.cancel(`Failed to link project: ${e.message}`);
63
+ }
64
+
65
+ fmt.outro('Done');
26
66
  }
package/commands/nuke.mjs CHANGED
@@ -10,6 +10,7 @@ import * as fmt from '../fmt.mjs';
10
10
  import { chalk } from '../fmt.mjs';
11
11
  import { removeGlobalHooks } from '../hooks.mjs';
12
12
  import { uninstallAwEcc } from '../ecc.mjs';
13
+ import { listProjectWorktrees } from '../git.mjs';
13
14
 
14
15
  const HOME = homedir();
15
16
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
@@ -169,6 +170,25 @@ function removeProjectSymlinks() {
169
170
  }
170
171
 
171
172
  if (removed > 0) fmt.logStep(`Removed ${removed} project .aw_registry symlink${removed > 1 ? 's' : ''}`);
173
+
174
+ // Also remove legacy local .git/hooks/post-checkout installed by old aw versions
175
+ let hooksRemoved = 0;
176
+ try {
177
+ const result = execSync(
178
+ `find "${HOME}" -maxdepth 5 -path "*/.git/hooks/post-checkout" -type f 2>/dev/null`,
179
+ { encoding: 'utf8', timeout: 10000 }
180
+ ).trim();
181
+ for (const hookPath of result.split('\n').filter(Boolean)) {
182
+ try {
183
+ const content = readFileSync(hookPath, 'utf8');
184
+ if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
185
+ unlinkSync(hookPath);
186
+ hooksRemoved++;
187
+ }
188
+ } catch { /* best effort */ }
189
+ }
190
+ } catch { /* find failed, skip */ }
191
+ if (hooksRemoved > 0) fmt.logStep(`Removed ${hooksRemoved} legacy .git/hooks/post-checkout script${hooksRemoved > 1 ? 's' : ''}`);
172
192
  }
173
193
 
174
194
  // 4. Remove git hooks (global core.hooksPath + legacy template)
@@ -298,8 +318,30 @@ export function nukeCommand(args) {
298
318
  }
299
319
  fmt.logStep('Removed ~/.aw_registry/');
300
320
 
301
- // 11. Remove ~/.aw/ (persistent git clone)
321
+ // 11. Remove project git worktrees (must happen before deleting ~/.aw/)
302
322
  const AW_HOME = join(HOME, '.aw');
323
+ if (existsSync(AW_HOME)) {
324
+ try {
325
+ const worktrees = listProjectWorktrees(AW_HOME);
326
+ let wtRemoved = 0;
327
+ for (const wt of worktrees) {
328
+ try {
329
+ execSync(`git -C "${AW_HOME}" worktree remove "${wt.path}" --force`, { stdio: 'pipe' });
330
+ wtRemoved++;
331
+ } catch {
332
+ // Manual cleanup if git command fails
333
+ try { rmSync(wt.path, { recursive: true, force: true }); } catch { /* best effort */ }
334
+ wtRemoved++;
335
+ }
336
+ }
337
+ if (worktrees.length > 0) {
338
+ try { execSync(`git -C "${AW_HOME}" worktree prune`, { stdio: 'pipe' }); } catch { /* best effort */ }
339
+ fmt.logStep(`Removed ${wtRemoved} project worktree${wtRemoved > 1 ? 's' : ''}`);
340
+ }
341
+ } catch { /* best effort */ }
342
+ }
343
+
344
+ // 12. Remove ~/.aw/ (persistent git clone)
303
345
  if (existsSync(AW_HOME)) {
304
346
  try {
305
347
  rmSync(AW_HOME, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
@@ -315,6 +357,7 @@ export function nukeCommand(args) {
315
357
  ` ${chalk.green('✓')} Generated files cleaned`,
316
358
  ` ${chalk.green('✓')} IDE symlinks cleaned`,
317
359
  ` ${chalk.green('✓')} aw-ecc engine removed`,
360
+ ` ${chalk.green('✓')} Project worktrees removed`,
318
361
  ` ${chalk.green('✓')} Project symlinks cleaned`,
319
362
  ` ${chalk.green('✓')} Git hooks removed`,
320
363
  ` ${chalk.green('✓')} IDE auto-sync tasks removed`,
package/commands/pull.mjs CHANGED
@@ -7,7 +7,7 @@ import { execSync } from 'node:child_process';
7
7
  import * as config from '../config.mjs';
8
8
  import * as fmt from '../fmt.mjs';
9
9
  import { chalk } from '../fmt.mjs';
10
- import { fetchAndMerge, addToSparseCheckout, isValidClone } from '../git.mjs';
10
+ import { fetchAndMerge, addToSparseCheckout, isValidClone, isWorktree, rebaseOntoOriginMain } from '../git.mjs';
11
11
  import { REGISTRY_DIR, REGISTRY_REPO, DOCS_SOURCE_DIR } from '../constants.mjs';
12
12
  import { linkWorkspace } from '../link.mjs';
13
13
  import { generateCommands, copyInstructions } from '../integrate.mjs';
@@ -73,18 +73,57 @@ export async function pullCommand(args) {
73
73
  log.logWarn(`Conflicts in: ${fetchResult.conflicts.join(', ')}`);
74
74
  }
75
75
 
76
+ // Always rebase project worktree's current branch onto origin/main
77
+ const localAw = join(cwd, '.aw');
78
+ if (cwd !== HOME && isWorktree(localAw)) {
79
+ try {
80
+ rebaseOntoOriginMain(localAw);
81
+ if (!silent) log.logStep('Local branch rebased onto latest');
82
+ } catch (e) {
83
+ // Check if the failure was due to merge conflicts
84
+ const isConflict = e.message?.includes('could not apply') || e.message?.includes('CONFLICT');
85
+ if (isConflict) {
86
+ // Get list of conflicted files
87
+ let conflictedFiles = [];
88
+ try {
89
+ const out = execSync(`git -C "${localAw}" diff --name-only --diff-filter=U`, {
90
+ stdio: 'pipe', encoding: 'utf8',
91
+ });
92
+ conflictedFiles = out.trim().split('\n').filter(Boolean);
93
+ } catch { /* best effort */ }
94
+
95
+ // Abort rebase to restore clean state
96
+ try {
97
+ execSync(`git -C "${localAw}" rebase --abort`, { stdio: 'pipe' });
98
+ } catch { /* best effort */ }
99
+
100
+ if (!silent) {
101
+ log.logWarn('Merge conflict detected — rebase aborted, your branch is unchanged.');
102
+ if (conflictedFiles.length > 0) {
103
+ log.logWarn(`Conflicting file${conflictedFiles.length > 1 ? 's' : ''}:`);
104
+ for (const f of conflictedFiles) log.logMessage(` ${chalk.red('✗')} ${f}`);
105
+ }
106
+ log.logWarn('Resolve the conflict and run `aw pull` again, or check with `aw status`.');
107
+ }
108
+ } else {
109
+ const msg = e.message?.split('\n').find(l => l.trim()) ?? e.message;
110
+ if (!silent) log.logWarn(`Rebase skipped: ${msg}`);
111
+ }
112
+ }
113
+ }
114
+
76
115
  // Sync content/ → platform/docs/
77
116
  syncDocs(AW_HOME, GLOBAL_AW_DIR);
78
117
 
79
118
  // Re-link IDE dirs
80
119
  if (!args._skipIntegrate) {
81
- linkWorkspace(HOME);
120
+ // Wire ~/.claude/.cursor/.codex to the project's registry when in a project,
121
+ // so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
122
+ const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
123
+ const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
124
+ linkWorkspace(HOME, awDirForLinks);
82
125
  generateCommands(HOME);
83
126
  copyInstructions(HOME, null, cfg.namespace);
84
- if (cwd !== HOME) {
85
- linkWorkspace(cwd);
86
- generateCommands(cwd);
87
- }
88
127
  }
89
128
 
90
129
  if (!silent) {