@ghl-ai/aw 0.1.35 → 0.1.36-beta.10

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/cli.mjs CHANGED
@@ -72,7 +72,8 @@ function printHelp() {
72
72
  const sec = (title) => `\n ${chalk.bold.underline(title)}`;
73
73
  const help = [
74
74
  sec('Setup'),
75
- cmd('aw init --namespace <team/sub-team>', 'Initialize workspace (required)'),
75
+ cmd('aw init', 'Initialize workspace (platform/ only)'),
76
+ cmd('aw init --namespace <team/sub-team>', 'Add a team namespace (optional)'),
76
77
  ` ${chalk.dim('Teams: platform, revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
77
78
  ` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
78
79
 
package/commands/drop.mjs CHANGED
@@ -2,87 +2,85 @@
2
2
 
3
3
  import { join, resolve } from 'node:path';
4
4
  import { rmSync, existsSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
5
6
  import * as config from '../config.mjs';
6
7
  import * as fmt from '../fmt.mjs';
7
8
  import { chalk } from '../fmt.mjs';
8
- import { matchesAny } from '../glob.mjs';
9
9
  import { resolveInput } from '../paths.mjs';
10
- import { load as loadManifest, save as saveManifest } from '../manifest.mjs';
10
+ import { removeFromSparseCheckout, isValidClone, getLocalRegistryDir } from '../git.mjs';
11
+ import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
12
+ import { linkWorkspace } from '../link.mjs';
11
13
 
12
14
  export function dropCommand(args) {
13
15
  const input = args._positional?.[0];
14
16
  const cwd = process.cwd();
15
- const workspaceDir = join(cwd, '.aw_registry');
17
+
18
+ const HOME = homedir();
19
+ const AW_HOME = join(HOME, '.aw');
20
+ const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
21
+ const workspaceDir = getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
16
22
 
17
23
  fmt.intro('aw drop');
18
24
 
19
25
  if (!input) {
20
- fmt.cancel('Missing target. Usage:\n aw drop example-team (stop syncing namespace)\n aw drop example-team/skills/example-skill (stop syncing skill)\n aw drop example-team/skills/example-skill/SKILL (delete single file)');
26
+ fmt.cancel('Missing target. Usage:\n aw drop example-team (stop syncing namespace)\n aw drop example-team/skills/example-skill (stop syncing skill)');
27
+ return;
21
28
  }
22
29
 
23
- const cfg = config.load(workspaceDir);
24
- if (!cfg) fmt.cancel('No .sync-config.json found. Run: aw init');
30
+ const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
31
+ if (!isValidClone(AW_HOME, repoUrl)) {
32
+ fmt.cancel('Registry not initialized. Run: aw init');
33
+ return;
34
+ }
25
35
 
26
- // Resolve to registry path (accepts both local and registry paths)
36
+ const cfg = config.load(GLOBAL_AW_DIR);
37
+ if (!cfg) {
38
+ fmt.cancel('No .sync-config.json found. Run: aw init');
39
+ return;
40
+ }
41
+
42
+ // Resolve to registry path
27
43
  const resolved = resolveInput(input, workspaceDir);
28
44
  const regPath = resolved.registryPath;
29
45
 
30
46
  if (!regPath) {
31
47
  fmt.cancel(`Could not resolve "${input}" to a registry path`);
48
+ return;
32
49
  }
33
50
 
34
- // Check if this path (or a parent) is in config → remove from config + delete files
51
+ // Check if this path (or a parent) is in config
35
52
  const isConfigPath = cfg.include.some(p => p === regPath || p.startsWith(regPath + '/'));
36
53
 
37
54
  if (isConfigPath) {
38
- config.removePattern(workspaceDir, regPath);
55
+ // Remove from sparse checkout
56
+ try {
57
+ removeFromSparseCheckout(AW_HOME, [`${REGISTRY_DIR}/${regPath}`]);
58
+ } catch (e) {
59
+ fmt.logWarn(`Could not update sparse checkout: ${e.message}`);
60
+ }
61
+
62
+ config.removePattern(GLOBAL_AW_DIR, regPath);
39
63
  fmt.logSuccess(`Removed ${chalk.cyan(regPath)} from sync config`);
40
64
  }
41
65
 
42
- // Delete matching local files
43
- const removed = deleteMatchingFiles(workspaceDir, regPath);
66
+ // Count removed files (they disappear from working tree via sparse checkout)
67
+ const registryAbsPath = join(AW_HOME, REGISTRY_DIR, regPath);
68
+ let removed = 0;
69
+ if (!existsSync(registryAbsPath)) {
70
+ removed = 1; // sparse checkout removed it
71
+ }
44
72
 
45
- if (removed > 0) {
46
- fmt.logInfo(`${chalk.bold(removed)} file${removed > 1 ? 's' : ''} removed from workspace`);
47
- } else if (!isConfigPath) {
73
+ if (!isConfigPath && removed === 0) {
48
74
  fmt.cancel(`Nothing found for ${chalk.cyan(regPath)}.\n\n Use ${chalk.dim('aw status')} to see synced paths.`);
75
+ return;
49
76
  }
50
77
 
51
- if (!isConfigPath && removed > 0) {
52
- fmt.logWarn(`Path still in sync config — next ${chalk.dim('aw pull')} will restore it`);
78
+ if (!isConfigPath) {
79
+ fmt.logWarn(`Path was not in sync config — no sparse checkout change made`);
53
80
  }
54
81
 
55
- fmt.outro('Done');
56
- }
82
+ // Re-link to remove dead symlinks
83
+ linkWorkspace(HOME);
57
84
 
58
- /**
59
- * Find and delete local files whose registry path matches the given path.
60
- */
61
- function deleteMatchingFiles(workspaceDir, path) {
62
- const manifest = loadManifest(workspaceDir);
63
- let removed = 0;
64
-
65
- for (const [manifestKey] of Object.entries(manifest.files)) {
66
- const registryPath = manifestKeyToRegistryPath(manifestKey);
67
-
68
- if (matchesAny(registryPath, [path])) {
69
- const filePath = join(workspaceDir, manifestKey);
70
- if (existsSync(filePath)) {
71
- rmSync(filePath, { recursive: true, force: true });
72
- removed++;
73
- }
74
- delete manifest.files[manifestKey];
75
- }
76
- }
77
-
78
- saveManifest(workspaceDir, manifest);
79
- return removed;
80
- }
81
-
82
- /**
83
- * Convert manifest key to registry path.
84
- * Manifest key now mirrors registry: "platform/agents/architecture-reviewer.md" → "platform/agents/architecture-reviewer"
85
- */
86
- function manifestKeyToRegistryPath(manifestKey) {
87
- return manifestKey.replace(/\.md$/, '');
85
+ fmt.outro('Done');
88
86
  }
package/commands/init.mjs CHANGED
@@ -1,36 +1,46 @@
1
- // commands/init.mjs — Clean init: clone registry, link IDEs, global git hooks.
1
+ // commands/init.mjs — Clean init: persistent git clone of registry, link IDEs, global git hooks.
2
2
  //
3
3
  // No shell profile modifications. No daemons. No background processes.
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 { mkdirSync, existsSync, writeFileSync, symlinkSync } 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';
16
- import { pullCommand, pullAsync } from './pull.mjs';
17
15
  import { linkWorkspace } from '../link.mjs';
18
16
  import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
19
17
  import { setupMcp } from '../mcp.mjs';
20
18
  import { autoUpdate, promptUpdate } from '../update.mjs';
21
19
  import { installGlobalHooks } from '../hooks.mjs';
20
+ import { installAwEcc } from '../ecc.mjs';
21
+ import {
22
+ initPersistentClone,
23
+ isValidClone,
24
+ fetchAndMerge,
25
+ addToSparseCheckout,
26
+ addProjectWorktree,
27
+ isWorktree,
28
+ includeToSparsePaths,
29
+ sparseCheckoutAsync,
30
+ cleanup,
31
+ } from '../git.mjs';
32
+ import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
22
33
 
23
34
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
35
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
25
36
 
26
37
  const HOME = homedir();
27
38
  const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
28
- const MANIFEST_PATH = join(GLOBAL_AW_DIR, '.aw-manifest.json');
39
+ const AW_HOME = join(HOME, '.aw');
29
40
 
30
41
  // ── IDE tasks for auto-pull ─────────────────────────────────────────────
31
42
 
32
43
  function installIdeTasks() {
33
- // VS Code / Cursor task — runs aw pull on folder open
34
44
  const vscodeTask = {
35
45
  version: '2.0.0',
36
46
  tasks: [
@@ -45,18 +55,15 @@ function installIdeTasks() {
45
55
  ],
46
56
  };
47
57
 
48
- // Install globally for VS Code and Cursor
49
58
  for (const ide of ['Code', 'Cursor']) {
50
59
  const userDir = join(HOME, 'Library', 'Application Support', ide, 'User');
51
60
  if (!existsSync(userDir)) continue;
52
61
 
53
62
  const tasksPath = join(userDir, 'tasks.json');
54
63
  if (existsSync(tasksPath)) {
55
- // Don't override existing tasks — check if aw task already there
56
64
  try {
57
65
  const existing = JSON.parse(readFileSync(tasksPath, 'utf8'));
58
66
  if (existing.tasks?.some(t => t.label === 'aw: sync registry' || t.label === 'aw: pull registry')) continue;
59
- // Add our task to existing
60
67
  existing.tasks = existing.tasks || [];
61
68
  existing.tasks.push(vscodeTask.tasks[0]);
62
69
  writeFileSync(tasksPath, JSON.stringify(existing, null, 2) + '\n');
@@ -69,33 +76,10 @@ function installIdeTasks() {
69
76
  }
70
77
  }
71
78
 
72
- function saveManifest(data) {
73
- writeFileSync(MANIFEST_PATH, JSON.stringify(data, null, 2) + '\n');
74
- }
75
-
76
-
77
- function printPullSummary(pattern, actions) {
78
- for (const type of ['agents', 'skills', 'commands', 'evals']) {
79
- const typeActions = actions.filter(a => a.type === type);
80
- if (typeActions.length === 0) continue;
81
-
82
- const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0 };
83
- for (const a of typeActions) counts[a.action] = (counts[a.action] || 0) + 1;
84
-
85
- const parts = [];
86
- if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} new`));
87
- if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
88
- if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
89
- const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
90
-
91
- fmt.logSuccess(`${chalk.cyan(pattern)}: ${typeActions.length} ${type}${detail}`);
92
- }
93
- }
94
-
95
79
  const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
96
80
 
97
81
  export async function initCommand(args) {
98
- const namespace = args['--namespace'] || null;
82
+ let namespace = args['--namespace'] || null;
99
83
  let user = args['--user'] || '';
100
84
  const silent = args['--silent'] === true;
101
85
 
@@ -103,24 +87,11 @@ export async function initCommand(args) {
103
87
 
104
88
  // ── Validate ──────────────────────────────────────────────────────────
105
89
 
106
- if (!namespace && !silent) {
107
- const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
108
- fmt.cancel([
109
- `Missing required ${chalk.bold('--namespace')} flag`,
110
- '',
111
- ` ${chalk.dim('Usage:')} aw init --namespace <team/sub-team>`,
112
- ` ${chalk.dim('Teams:')} ${list}`,
113
- '',
114
- ` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce/payments')}`,
115
- ].join('\n'));
116
- }
117
-
118
- // Parse team/sub-team
119
- const nsParts = namespace ? namespace.split('/') : [];
120
- const team = nsParts[0] || null;
121
- const subTeam = nsParts[1] || null;
122
- const teamNS = subTeam ? `${team}-${subTeam}` : team; // for $TEAM_NS replacement
123
- const folderName = subTeam ? `${team}/${subTeam}` : team; // for .aw_registry/ path
90
+ let nsParts = namespace ? namespace.split('/') : [];
91
+ let team = nsParts[0] || null;
92
+ let subTeam = nsParts[1] || null;
93
+ let teamNS = subTeam ? `${team}-${subTeam}` : team;
94
+ let folderName = subTeam ? `${team}/${subTeam}` : team;
124
95
 
125
96
  if (team && !ALLOWED_NAMESPACES.includes(team)) {
126
97
  const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
@@ -151,70 +122,113 @@ export async function initCommand(args) {
151
122
  fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
152
123
  }
153
124
 
154
- const hasConfig = config.exists(GLOBAL_AW_DIR);
155
- const hasPlatform = existsSync(join(GLOBAL_AW_DIR, 'platform'));
156
- const isExisting = hasConfig && hasPlatform;
125
+ // ── Detect installation state ─────────────────────────────────────────
126
+
127
+ const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
128
+ const isGitNative = isValidClone(AW_HOME, repoUrl);
129
+ const isLegacy = !isGitNative && existsSync(GLOBAL_AW_DIR) && !lstatSync(GLOBAL_AW_DIR).isSymbolicLink();
130
+
131
+ // ── Probe remote registry to check if namespace exists (fresh install only) ──
132
+
133
+ let namespaceExistsInRemote = false;
134
+ if (folderName && !silent && !isGitNative && !isLegacy) {
135
+ try {
136
+ const probePaths = includeToSparsePaths([folderName]);
137
+ const probeDir = await sparseCheckoutAsync(REGISTRY_REPO, probePaths);
138
+ try {
139
+ const fullNsPath = join(probeDir, REGISTRY_DIR, ...folderName.split('/'));
140
+ namespaceExistsInRemote = existsSync(fullNsPath) &&
141
+ readdirSync(fullNsPath, { withFileTypes: true })
142
+ .some(d => d.isDirectory() && !d.name.startsWith('.'));
143
+ } finally {
144
+ cleanup(probeDir);
145
+ }
146
+ } catch {
147
+ namespaceExistsInRemote = true;
148
+ }
149
+ }
150
+
151
+ if (folderName && !silent && !isGitNative && !isLegacy && !namespaceExistsInRemote && process.stdin.isTTY) {
152
+ const choice = await fmt.select({
153
+ message: `The namespace '${folderName}' does not exist in the registry yet.\nplatform/ includes shared agents, skills & commands that cover most use cases.\nHow would you like to proceed?`,
154
+ options: [
155
+ { value: 'platform-only', label: 'Continue with platform/ only (recommended for most users)' },
156
+ { value: 'create-namespace', label: `Use '${folderName}' namespace (will be created)` },
157
+ ],
158
+ });
159
+
160
+ if (fmt.isCancel(choice)) {
161
+ fmt.cancel('Operation cancelled.');
162
+ process.exit(0);
163
+ }
164
+
165
+ if (choice === 'platform-only') {
166
+ namespace = null; team = null; subTeam = null; teamNS = null; folderName = null;
167
+ }
168
+ }
169
+
157
170
  const cwd = process.cwd();
158
171
 
159
- // ── Fast path: already initialized just pull + link ─────────────────
172
+ // ── Re-init path: already set up with native git clone ────────────────
160
173
 
161
- if (isExisting) {
174
+ if (isGitNative) {
162
175
  const cfg = config.load(GLOBAL_AW_DIR);
163
176
 
164
- // Add new sub-team if not already tracked
165
177
  const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
166
178
  if (isNewSubTeam) {
167
179
  if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
168
- await pullAsync({
169
- ...args,
170
- _positional: ['[template]'],
171
- _renameNamespace: folderName,
172
- _teamNS: teamNS,
173
- _workspaceDir: GLOBAL_AW_DIR,
174
- _skipIntegrate: true,
175
- });
180
+ const newSparsePaths = [`.aw_registry/${folderName}`, `content`];
181
+ addToSparseCheckout(AW_HOME, newSparsePaths);
176
182
  config.addPattern(GLOBAL_AW_DIR, folderName);
177
183
  } else {
178
184
  if (!silent) fmt.logStep('Already initialized — syncing...');
179
185
  }
180
186
 
181
- // Pull latest (parallel)
182
- // cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
183
- // only has '.aw_registry/[template]/' — remap non-platform entries back.
184
- const freshCfg = config.load(GLOBAL_AW_DIR);
185
- if (freshCfg && freshCfg.include.length > 0) {
186
- const pullJobs = freshCfg.include.map(p => {
187
- const isTeamNs = p !== 'platform';
188
- const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
189
- return pullAsync({
190
- ...args,
191
- _positional: [isTeamNs ? '[template]' : p],
192
- _workspaceDir: GLOBAL_AW_DIR,
193
- _skipIntegrate: true,
194
- _renameNamespace: isTeamNs ? p : undefined,
195
- _teamNS: derivedTeamNS,
196
- });
197
- });
198
- await Promise.all(pullJobs);
187
+ const s = fmt.spinner ? fmt.spinner() : { start: () => {}, stop: () => {} };
188
+ if (!silent) s.start('Fetching latest...');
189
+ try {
190
+ fetchAndMerge(AW_HOME);
191
+ if (!silent) s.stop('Registry updated');
192
+ } catch (e) {
193
+ if (!silent) s.stop(chalk.yellow('Fetch failed (continuing with local)'));
199
194
  }
200
195
 
201
- // Re-link IDE dirs + hooks (idempotent)
202
- linkWorkspace(HOME);
203
- generateCommands(HOME);
196
+ const freshCfg = config.load(GLOBAL_AW_DIR);
197
+
198
+ await installAwEcc(cwd, { silent });
204
199
  copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
205
200
  initAwDocs(HOME);
206
201
  await setupMcp(HOME, freshCfg?.namespace || team, { silent });
207
202
  if (cwd !== HOME) await setupMcp(cwd, freshCfg?.namespace || team, { silent });
208
203
  installGlobalHooks();
209
204
 
210
- // Link current project if needed
211
- 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');
212
208
  try {
213
- symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
214
- if (!silent) fmt.logStep('Linked .aw_registry in current project');
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'))) {
219
+ try {
220
+ addProjectWorktree(AW_HOME, cwd);
221
+ if (!silent) fmt.logStep('Linked current project as git worktree');
215
222
  } catch { /* best effort */ }
216
223
  }
217
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
+
218
232
  if (silent) {
219
233
  autoUpdate(await args._updateCheck);
220
234
  } else {
@@ -223,64 +237,76 @@ export async function initCommand(args) {
223
237
  '',
224
238
  ` ${chalk.green('✓')} Registry updated`,
225
239
  ` ${chalk.green('✓')} IDE integration refreshed`,
226
- 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,
227
241
  ].filter(Boolean).join('\n'));
228
242
  }
229
243
  return;
230
244
  }
231
245
 
246
+ // ── Legacy migration: old ~/.aw_registry/ dir → notify user ──────────
247
+
248
+ if (isLegacy) {
249
+ if (!silent) {
250
+ fmt.logWarn([
251
+ 'Legacy installation detected (~/.aw_registry/ is a plain directory).',
252
+ '',
253
+ ` Run ${chalk.bold('aw nuke')} first to remove the old install, then ${chalk.bold('aw init')} again.`,
254
+ ` This will migrate to the new native git clone at ~/.aw/`,
255
+ ].join('\n'));
256
+ }
257
+ // Fall through to full init — create AW_HOME fresh
258
+ }
259
+
232
260
  // ── Full init: first time setup ───────────────────────────────────────
233
261
 
234
- // Auto-detect user
235
262
  if (!user) {
236
263
  try {
237
264
  user = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
238
265
  } catch { /* git not configured */ }
239
266
  }
240
267
 
241
- // Step 1: Create global source of truth
242
- mkdirSync(GLOBAL_AW_DIR, { recursive: true });
243
-
244
- const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
268
+ // Determine sparse paths
269
+ const sparsePaths = [`.aw_registry/platform`, `content`, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
270
+ if (folderName) {
271
+ sparsePaths.push(`.aw_registry/${folderName}`);
272
+ }
245
273
 
246
274
  fmt.note([
247
- `${chalk.dim('source:')} ~/.aw_registry/`,
275
+ `${chalk.dim('source:')} ~/.aw/ → ~/.aw_registry/ (symlink)`,
248
276
  folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
249
- user ? `${chalk.dim('user:')} ${cfg.user}` : null,
277
+ user ? `${chalk.dim('user:')} ${user}` : null,
250
278
  `${chalk.dim('version:')} v${VERSION}`,
251
- ].filter(Boolean).join('\n'), 'Config created');
279
+ ].filter(Boolean).join('\n'), 'Config');
252
280
 
253
- // Step 2: Pull registry content (parallel)
254
281
  const s = fmt.spinner();
255
- const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
256
- s.start(`Pulling ${pullTargets}...`);
282
+ s.start(`Cloning registry...`);
257
283
 
258
- const pullJobs = [
259
- pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
260
- ];
261
- if (folderName) {
262
- pullJobs.push(
263
- pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: folderName, _teamNS: teamNS, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
264
- );
265
- }
266
-
267
- let pullResults;
268
284
  try {
269
- pullResults = await Promise.all(pullJobs);
270
- s.stop(`Pulled ${pullTargets}`);
285
+ initPersistentClone(repoUrl, AW_HOME, sparsePaths);
286
+ s.stop('Registry cloned');
271
287
  } catch (e) {
272
- s.stop(chalk.red('Pull failed'));
288
+ s.stop(chalk.red('Clone failed'));
273
289
  fmt.cancel(e.message);
274
290
  }
275
291
 
276
- for (const { pattern, actions } of pullResults) {
277
- printPullSummary(pattern, actions);
292
+ // Create backward-compat symlink: ~/.aw_registry/ ~/.aw/.aw_registry/
293
+ if (!existsSync(GLOBAL_AW_DIR)) {
294
+ try {
295
+ symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
296
+ fmt.logStep('Created ~/.aw_registry/ symlink');
297
+ } catch (e) {
298
+ fmt.logWarn(`Could not create symlink ~/.aw_registry/: ${e.message}`);
299
+ }
278
300
  }
279
301
 
280
- // Step 3: Link IDE dirs + setup tasks
281
- fmt.logStep('Linking IDE symlinks...');
282
- linkWorkspace(HOME);
283
- generateCommands(HOME);
302
+ // Create sync config
303
+ const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
304
+ if (folderName) {
305
+ config.addPattern(GLOBAL_AW_DIR, folderName);
306
+ }
307
+
308
+ // Step 3: Setup tasks, MCP, hooks
309
+ await installAwEcc(cwd, { silent });
284
310
  const instructionFiles = copyInstructions(HOME, null, team) || [];
285
311
  initAwDocs(HOME);
286
312
  const mcpFiles = await setupMcp(HOME, team) || [];
@@ -288,39 +314,46 @@ export async function initCommand(args) {
288
314
  const hooksInstalled = installGlobalHooks();
289
315
  installIdeTasks();
290
316
 
291
- // Step 4: Symlink in current directory if it's a git repo
292
- 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');
320
+ try {
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'))) {
293
331
  try {
294
- symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
295
- fmt.logStep('Linked .aw_registry in current project');
332
+ addProjectWorktree(AW_HOME, cwd);
333
+ fmt.logStep('Linked current project as git worktree');
296
334
  } catch { /* best effort */ }
297
335
  }
298
336
 
299
- // Step 5: Write manifest for nuke cleanup
300
- const manifest = {
301
- version: 1,
302
- installedAt: new Date().toISOString(),
303
- globalDir: GLOBAL_AW_DIR,
304
- createdFiles: [
305
- ...instructionFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
306
- ...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
307
- ],
308
- globalHooksDir: hooksInstalled ? join(HOME, '.aw', 'hooks') : null,
309
- };
310
- saveManifest(manifest);
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);
311
344
 
312
345
  // Offer to update if a newer version is available
313
346
  await promptUpdate(await args._updateCheck);
314
347
 
315
- // Done
316
348
  fmt.outro([
317
349
  'Install complete',
318
350
  '',
319
- ` ${chalk.green('✓')} Source of truth: ~/.aw_registry/`,
351
+ ` ${chalk.green('✓')} Source of truth: ~/.aw/ (git clone)`,
352
+ ` ${chalk.green('✓')} Symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/`,
320
353
  ` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
321
354
  hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
322
355
  ` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
323
- 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,
324
357
  '',
325
358
  ` ${chalk.dim('Existing repos:')} ${chalk.bold('cd <project> && aw link')}`,
326
359
  ` ${chalk.dim('New clones:')} auto-linked via git hook`,