@ghl-ai/aw 0.1.39-beta.8 → 0.1.39
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 +8 -1
- package/codex.mjs +839 -0
- package/commands/doctor.mjs +1086 -0
- package/commands/init.mjs +71 -81
- package/commands/link-project.mjs +12 -1
- package/commands/nuke.mjs +14 -4
- package/commands/pull.mjs +111 -11
- package/commands/push-rules.mjs +4 -15
- package/commands/push.mjs +4 -4
- package/commands/search.mjs +1 -1
- package/commands/startup.mjs +87 -0
- package/constants.mjs +3 -1
- package/ecc.mjs +130 -42
- package/git.mjs +4 -23
- package/hook-manifest.mjs +195 -0
- package/hooks/codex-home.mjs +184 -0
- package/hooks/shared-phase-scripts.mjs +69 -0
- package/integrate.mjs +219 -47
- package/link.mjs +36 -1
- package/mcp.mjs +2 -10
- package/package.json +8 -2
- package/paths.mjs +1 -1
- package/registry.mjs +1 -1
- package/render-rules.mjs +267 -27
- package/startup.mjs +562 -0
package/commands/init.mjs
CHANGED
|
@@ -23,12 +23,14 @@ import * as config from '../config.mjs';
|
|
|
23
23
|
import * as fmt from '../fmt.mjs';
|
|
24
24
|
import { chalk } from '../fmt.mjs';
|
|
25
25
|
import { linkWorkspace } from '../link.mjs';
|
|
26
|
-
import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
|
|
26
|
+
import { generateCommands, copyInstructions, initAwDocs, syncHomeHarnessInstructions } from '../integrate.mjs';
|
|
27
27
|
import { setupMcp } from '../mcp.mjs';
|
|
28
|
+
import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.mjs';
|
|
28
29
|
import { installLocalCommitHook } from '../hooks.mjs';
|
|
29
30
|
import { autoUpdate, promptUpdate } from '../update.mjs';
|
|
30
31
|
import { installGlobalHooks } from '../hooks.mjs';
|
|
31
32
|
import { installAwEcc } from '../ecc.mjs';
|
|
33
|
+
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
32
34
|
import {
|
|
33
35
|
initPersistentClone,
|
|
34
36
|
isValidClone,
|
|
@@ -42,28 +44,11 @@ import {
|
|
|
42
44
|
syncWorktreeSparseCheckout,
|
|
43
45
|
findNearestWorktree,
|
|
44
46
|
} from '../git.mjs';
|
|
45
|
-
import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL, RULES_SOURCE_DIR } from '../constants.mjs';
|
|
47
|
+
import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL, RULES_SOURCE_DIR, RULES_RUNTIME_DIR } from '../constants.mjs';
|
|
46
48
|
import { syncFileTree } from '../file-tree.mjs';
|
|
47
49
|
|
|
48
50
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
49
51
|
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
50
|
-
const DEBUG_INIT_LOG = process.env.AW_DEBUG_INIT_LOG;
|
|
51
|
-
|
|
52
|
-
function debugInit(message) {
|
|
53
|
-
if (!DEBUG_INIT_LOG) return;
|
|
54
|
-
try {
|
|
55
|
-
appendFileSync(DEBUG_INIT_LOG, `${new Date().toISOString()} ${message}\n`);
|
|
56
|
-
} catch { /* best effort */ }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function createSpinner(enabled) {
|
|
60
|
-
if (enabled) return fmt.spinner();
|
|
61
|
-
return {
|
|
62
|
-
start() {},
|
|
63
|
-
stop() {},
|
|
64
|
-
message() {},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
52
|
|
|
68
53
|
// Resolve HOME to the real path — on macOS /var is a symlink to /private/var,
|
|
69
54
|
// so homedir() returns /var/... while process.cwd() returns /private/var/...
|
|
@@ -74,6 +59,35 @@ const HOME = (() => { try { return realpathSync(_rawHome); } catch { return _raw
|
|
|
74
59
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
75
60
|
const AW_HOME = join(HOME, '.aw');
|
|
76
61
|
|
|
62
|
+
function syncRulesTargets(targetDir) {
|
|
63
|
+
const rulesSrc = join(AW_HOME, RULES_SOURCE_DIR);
|
|
64
|
+
if (!existsSync(rulesSrc)) return false;
|
|
65
|
+
syncFileTree(rulesSrc, join(targetDir, RULES_RUNTIME_DIR));
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function removeLegacyRegistryRules() {
|
|
70
|
+
try {
|
|
71
|
+
rmSync(join(GLOBAL_AW_DIR, RULES_SOURCE_DIR), { recursive: true, force: true });
|
|
72
|
+
rmSync(join(HOME, RULES_SOURCE_DIR), { recursive: true, force: true });
|
|
73
|
+
} catch {
|
|
74
|
+
// best effort cleanup
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function syncInstructionsAndAwDocs(targetDir, namespace) {
|
|
79
|
+
copyInstructions(targetDir, null, namespace);
|
|
80
|
+
initAwDocs(targetDir);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function syncHomeAndProjectInstructions(cwd, namespace) {
|
|
84
|
+
syncHomeHarnessInstructions(HOME);
|
|
85
|
+
initAwDocs(HOME);
|
|
86
|
+
if (cwd !== HOME) {
|
|
87
|
+
syncInstructionsAndAwDocs(cwd, namespace);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
77
91
|
// ── Ensure ~/.aw/.gitignore has personal/local entries ───────────────────
|
|
78
92
|
|
|
79
93
|
const AW_GITIGNORE_ENTRIES = [
|
|
@@ -138,12 +152,8 @@ export async function initCommand(args) {
|
|
|
138
152
|
let namespace = args['--namespace'] || null;
|
|
139
153
|
let user = args['--user'] || '';
|
|
140
154
|
const silent = args['--silent'] === true;
|
|
141
|
-
const interactiveUi = !silent && Boolean(process.stdout.isTTY);
|
|
142
155
|
|
|
143
|
-
|
|
144
|
-
fmt.intro(`aw init ${chalk.dim('v' + VERSION)}`);
|
|
145
|
-
}
|
|
146
|
-
debugInit('init:start');
|
|
156
|
+
fmt.intro(`aw init ${chalk.dim('v' + VERSION)}`);
|
|
147
157
|
|
|
148
158
|
// ── Validate ──────────────────────────────────────────────────────────
|
|
149
159
|
|
|
@@ -191,7 +201,7 @@ export async function initCommand(args) {
|
|
|
191
201
|
|
|
192
202
|
let namespaceExistsInRemote = false;
|
|
193
203
|
if (folderName && !silent && !isGitNative && !isLegacy) {
|
|
194
|
-
const probeSpinner =
|
|
204
|
+
const probeSpinner = fmt.spinner();
|
|
195
205
|
probeSpinner.start(`Checking namespace ${chalk.cyan(folderName)} in registry...`);
|
|
196
206
|
try {
|
|
197
207
|
const probePaths = includeToSparsePaths([folderName]);
|
|
@@ -204,15 +214,11 @@ export async function initCommand(args) {
|
|
|
204
214
|
} finally {
|
|
205
215
|
cleanup(probeDir);
|
|
206
216
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
: `Namespace ${chalk.cyan(folderName)} not yet in registry`);
|
|
211
|
-
}
|
|
217
|
+
probeSpinner.stop(namespaceExistsInRemote
|
|
218
|
+
? `Namespace ${chalk.cyan(folderName)} found in registry`
|
|
219
|
+
: `Namespace ${chalk.cyan(folderName)} not yet in registry`);
|
|
212
220
|
} catch {
|
|
213
|
-
|
|
214
|
-
probeSpinner.stop(chalk.dim('Could not verify namespace (continuing)'));
|
|
215
|
-
}
|
|
221
|
+
probeSpinner.stop(chalk.dim('Could not verify namespace (continuing)'));
|
|
216
222
|
namespaceExistsInRemote = true;
|
|
217
223
|
}
|
|
218
224
|
}
|
|
@@ -254,7 +260,7 @@ export async function initCommand(args) {
|
|
|
254
260
|
if (!silent) fmt.logStep('Already initialized — syncing...');
|
|
255
261
|
}
|
|
256
262
|
|
|
257
|
-
const s =
|
|
263
|
+
const s = fmt.spinner();
|
|
258
264
|
if (!silent) s.start('Fetching latest from registry...');
|
|
259
265
|
try {
|
|
260
266
|
const { conflicts } = await fetchAndMerge(AW_HOME, { silent });
|
|
@@ -272,9 +278,11 @@ export async function initCommand(args) {
|
|
|
272
278
|
|
|
273
279
|
ensureAwGitignore(AW_HOME);
|
|
274
280
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
275
|
-
|
|
276
|
-
|
|
281
|
+
syncRulesTargets(HOME);
|
|
282
|
+
if (cwd !== HOME) {
|
|
283
|
+
syncRulesTargets(cwd);
|
|
277
284
|
}
|
|
285
|
+
removeLegacyRegistryRules();
|
|
278
286
|
|
|
279
287
|
// Ensure project worktree sparse checkout matches the global clone.
|
|
280
288
|
// Covers the case where a namespace was added from HOME (or another project)
|
|
@@ -285,9 +293,11 @@ export async function initCommand(args) {
|
|
|
285
293
|
}
|
|
286
294
|
|
|
287
295
|
await installAwEcc(cwd, { silent });
|
|
288
|
-
|
|
289
|
-
|
|
296
|
+
ensureAwRuntimeHook(HOME);
|
|
297
|
+
syncHomeAndProjectInstructions(cwd, freshCfg?.namespace || team);
|
|
290
298
|
await setupMcp(HOME, freshCfg?.namespace || team, { silent });
|
|
299
|
+
applyStoredStartupPreferences(HOME);
|
|
300
|
+
const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
|
|
291
301
|
installGlobalHooks();
|
|
292
302
|
|
|
293
303
|
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath (creates stale .aw_registry symlink)
|
|
@@ -329,6 +339,9 @@ export async function initCommand(args) {
|
|
|
329
339
|
'',
|
|
330
340
|
` ${chalk.green('✓')} Registry synced`,
|
|
331
341
|
` ${chalk.green('✓')} IDE refreshed — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`,
|
|
342
|
+
removedLegacyStartupFiles.length > 0
|
|
343
|
+
? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
|
|
344
|
+
: null,
|
|
332
345
|
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Project linked` : null,
|
|
333
346
|
].filter(Boolean).join('\n'));
|
|
334
347
|
}
|
|
@@ -363,26 +376,21 @@ export async function initCommand(args) {
|
|
|
363
376
|
sparsePaths.push(`.aw_registry/${folderName}`);
|
|
364
377
|
}
|
|
365
378
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
].filter(Boolean).join('\n'), 'Config');
|
|
373
|
-
}
|
|
379
|
+
fmt.note([
|
|
380
|
+
`${chalk.dim('source:')} ~/.aw/ → ~/.aw_registry/ (symlink)`,
|
|
381
|
+
folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
|
|
382
|
+
user ? `${chalk.dim('user:')} ${user}` : null,
|
|
383
|
+
`${chalk.dim('version:')} v${VERSION}`,
|
|
384
|
+
].filter(Boolean).join('\n'), 'Config');
|
|
374
385
|
|
|
375
|
-
const s =
|
|
386
|
+
const s = fmt.spinner();
|
|
376
387
|
s.start(`Cloning registry...`);
|
|
377
388
|
|
|
378
389
|
try {
|
|
379
|
-
debugInit(`clone:start repo=${repoUrl}`);
|
|
380
390
|
await initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
381
|
-
debugInit('clone:done');
|
|
382
391
|
ensureAwGitignore(AW_HOME);
|
|
383
392
|
s.stop('Registry cloned');
|
|
384
393
|
} catch (e) {
|
|
385
|
-
debugInit(`clone:error ${e.message}`);
|
|
386
394
|
s.stop(chalk.red('Clone failed'));
|
|
387
395
|
fmt.cancel(e.message);
|
|
388
396
|
}
|
|
@@ -393,55 +401,42 @@ export async function initCommand(args) {
|
|
|
393
401
|
try { awRegistryLstat = lstatSync(GLOBAL_AW_DIR); } catch { /* doesn't exist */ }
|
|
394
402
|
if (!awRegistryLstat) {
|
|
395
403
|
try {
|
|
396
|
-
debugInit('symlink:create:start');
|
|
397
404
|
symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
|
|
398
|
-
debugInit('symlink:create:done');
|
|
399
405
|
fmt.logStep('Created ~/.aw_registry/ symlink');
|
|
400
406
|
} catch (e) {
|
|
401
|
-
debugInit(`symlink:create:error ${e.message}`);
|
|
402
407
|
fmt.logWarn(`Could not create symlink ~/.aw_registry/: ${e.message}`);
|
|
403
408
|
}
|
|
404
409
|
} else if (awRegistryLstat.isSymbolicLink()) {
|
|
405
410
|
// Stale or dangling — re-point to the new clone
|
|
406
411
|
try {
|
|
407
|
-
debugInit('symlink:update:start');
|
|
408
412
|
rmSync(GLOBAL_AW_DIR);
|
|
409
413
|
symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
|
|
410
|
-
debugInit('symlink:update:done');
|
|
411
414
|
fmt.logStep('Updated ~/.aw_registry/ symlink');
|
|
412
415
|
} catch (e) {
|
|
413
|
-
debugInit(`symlink:update:error ${e.message}`);
|
|
414
416
|
fmt.logWarn(`Could not update symlink ~/.aw_registry/: ${e.message}`);
|
|
415
417
|
}
|
|
416
418
|
}
|
|
417
419
|
|
|
418
420
|
// Create sync config — default to 'platform' when no namespace specified
|
|
419
421
|
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
|
|
420
|
-
debugInit('config:create:done');
|
|
421
422
|
if (folderName) {
|
|
422
423
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
423
|
-
debugInit('config:add-pattern:done');
|
|
424
424
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
debugInit('sync-file-tree:done');
|
|
425
|
+
syncRulesTargets(HOME);
|
|
426
|
+
if (cwd !== HOME) {
|
|
427
|
+
syncRulesTargets(cwd);
|
|
429
428
|
}
|
|
429
|
+
removeLegacyRegistryRules();
|
|
430
430
|
|
|
431
431
|
// Step 3: Setup tasks, MCP, hooks
|
|
432
|
-
debugInit('ecc:start');
|
|
433
432
|
await installAwEcc(cwd, { silent });
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
debugInit(`instructions:done count=${instructionFiles.length}`);
|
|
437
|
-
initAwDocs(HOME);
|
|
438
|
-
debugInit('aw-docs:done');
|
|
433
|
+
ensureAwRuntimeHook(HOME);
|
|
434
|
+
syncHomeAndProjectInstructions(cwd, team);
|
|
439
435
|
const mcpFiles = await setupMcp(HOME, team, { silent }) || [];
|
|
440
|
-
|
|
436
|
+
applyStoredStartupPreferences(HOME);
|
|
437
|
+
const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
|
|
441
438
|
const hooksInstalled = installGlobalHooks();
|
|
442
|
-
debugInit(`hooks:done installed=${hooksInstalled}`);
|
|
443
439
|
installIdeTasks();
|
|
444
|
-
debugInit('ide-tasks:done');
|
|
445
440
|
|
|
446
441
|
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath
|
|
447
442
|
if (cwd !== HOME) {
|
|
@@ -460,33 +455,24 @@ export async function initCommand(args) {
|
|
|
460
455
|
const isAlreadySymlinkFresh = (() => { try { return lstatSync(awLinkFresh).isSymbolicLink() && existsSync(awLinkFresh); } catch { return false; } })();
|
|
461
456
|
if (cwd !== HOME && !isInsideAw && !isAlreadySymlinkFresh) {
|
|
462
457
|
try {
|
|
463
|
-
debugInit('link-project:start');
|
|
464
458
|
addProjectWorktree(AW_HOME, cwd);
|
|
465
|
-
debugInit('link-project:done');
|
|
466
459
|
fmt.logStep('Linked current project');
|
|
467
460
|
} catch { /* best effort */ }
|
|
468
461
|
}
|
|
469
462
|
|
|
470
463
|
// Step 5: Wire ~/.claude/.cursor/.codex to the project's registry when in a project,
|
|
471
464
|
// so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
|
|
472
|
-
const ideSpinner =
|
|
465
|
+
const ideSpinner = fmt.spinner();
|
|
473
466
|
ideSpinner.start('Wiring IDE symlinks...');
|
|
474
|
-
debugInit('ide-wire:start');
|
|
475
467
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
476
468
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
477
469
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
478
470
|
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
479
471
|
ideSpinner.message('Generating commands...');
|
|
480
472
|
const commands = generateCommands(HOME, { silent: true });
|
|
481
|
-
debugInit(`ide-wire:done symlinks=${symlinks} commands=${commands}`);
|
|
482
473
|
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
483
474
|
|
|
484
475
|
// Offer to update if a newer version is available
|
|
485
|
-
if (silent) {
|
|
486
|
-
autoUpdate(await args._updateCheck);
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
476
|
await promptUpdate(await args._updateCheck);
|
|
491
477
|
|
|
492
478
|
fmt.outro([
|
|
@@ -495,6 +481,10 @@ export async function initCommand(args) {
|
|
|
495
481
|
` ${chalk.green('✓')} Source of truth: ~/.aw/ (git clone)`,
|
|
496
482
|
` ${chalk.green('✓')} Symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/`,
|
|
497
483
|
` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
|
|
484
|
+
cwd !== HOME ? ` ${chalk.green('✓')} Global startup managed from ~/.claude/, ~/.cursor/, ~/.codex/` : null,
|
|
485
|
+
removedLegacyStartupFiles.length > 0
|
|
486
|
+
? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
|
|
487
|
+
: null,
|
|
498
488
|
hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
|
|
499
489
|
` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
|
|
500
490
|
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Linked in current project` : null,
|
|
@@ -9,6 +9,8 @@ import { addProjectWorktree, isWorktree, isValidClone } from '../git.mjs';
|
|
|
9
9
|
import { REGISTRY_DIR, REGISTRY_URL } from '../constants.mjs';
|
|
10
10
|
import { linkWorkspace } from '../link.mjs';
|
|
11
11
|
import { generateCommands } from '../integrate.mjs';
|
|
12
|
+
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
13
|
+
import { applyStoredStartupPreferences } from '../startup.mjs';
|
|
12
14
|
import { installLocalCommitHook } from '../hooks.mjs';
|
|
13
15
|
|
|
14
16
|
const HOME = homedir();
|
|
@@ -41,8 +43,11 @@ export function linkProjectCommand(args) {
|
|
|
41
43
|
const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
|
|
42
44
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
43
45
|
const commands = generateCommands(HOME, { silent: true });
|
|
46
|
+
applyStoredStartupPreferences(HOME);
|
|
44
47
|
installLocalCommitHook(cwd);
|
|
45
|
-
|
|
48
|
+
const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
|
|
49
|
+
fmt.logSuccess(`Already linked — refreshed ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands${removedLegacyStartupFiles.length > 0 ? ` · removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}` : ''}`);
|
|
50
|
+
|
|
46
51
|
return;
|
|
47
52
|
}
|
|
48
53
|
|
|
@@ -52,13 +57,19 @@ export function linkProjectCommand(args) {
|
|
|
52
57
|
const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
|
|
53
58
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
54
59
|
const commands = generateCommands(HOME, { silent: true });
|
|
60
|
+
applyStoredStartupPreferences(HOME);
|
|
55
61
|
installLocalCommitHook(cwd);
|
|
62
|
+
const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
|
|
56
63
|
fmt.logSuccess([
|
|
57
64
|
`Project linked — ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`,
|
|
58
65
|
'',
|
|
59
66
|
` ${chalk.green('✓')} ${chalk.dim('.aw/')} git worktree (IDE git panel enabled)`,
|
|
60
67
|
` ${chalk.green('✓')} ${chalk.dim(`.aw/${REGISTRY_DIR}/`)} registry content`,
|
|
61
68
|
` ${chalk.green('✓')} ${chalk.dim('.claude/.cursor/.codex/')} IDE symlinks wired`,
|
|
69
|
+
` ${chalk.green('✓')} ${chalk.dim('startup mode')} global-first via ~/.claude/, ~/.cursor/, ~/.codex/`,
|
|
70
|
+
removedLegacyStartupFiles.length > 0
|
|
71
|
+
? ` ${chalk.green('✓')} ${chalk.dim('legacy repo hooks')} removed ${removedLegacyStartupFiles.length} AW-managed file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
|
|
72
|
+
: null,
|
|
62
73
|
].join('\n'));
|
|
63
74
|
} catch (e) {
|
|
64
75
|
fmt.cancel(`Failed to link project: ${e.message}`);
|
package/commands/nuke.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Safety guarantee: NEVER deletes files that AW didn't create.
|
|
4
4
|
|
|
5
|
-
import { join } from 'node:path';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
6
|
import { existsSync, rmSync, lstatSync, unlinkSync, readdirSync, readFileSync, readlinkSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { execSync, exec as execCb } from 'node:child_process';
|
|
@@ -15,13 +15,14 @@ import { removeGlobalHooks } from '../hooks.mjs';
|
|
|
15
15
|
import { uninstallAwEcc } from '../ecc.mjs';
|
|
16
16
|
import { removeMcpConfig } from '../mcp.mjs';
|
|
17
17
|
import { listProjectWorktrees } from '../git.mjs';
|
|
18
|
+
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
18
19
|
|
|
19
20
|
const HOME = homedir();
|
|
20
21
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
21
22
|
const MANIFEST_PATH = join(GLOBAL_AW_DIR, '.aw-manifest.json');
|
|
22
23
|
|
|
23
24
|
const IDE_DIRS = ['.claude', '.cursor', '.codex', '.agents'];
|
|
24
|
-
const CONTENT_TYPES = ['agents', 'skills', 'commands', 'evals'];
|
|
25
|
+
const CONTENT_TYPES = ['agents', 'skills', 'commands', 'evals', 'references'];
|
|
25
26
|
|
|
26
27
|
function loadManifest() {
|
|
27
28
|
if (!existsSync(MANIFEST_PATH)) return null;
|
|
@@ -139,7 +140,11 @@ async function removeProjectSymlinks() {
|
|
|
139
140
|
{ encoding: 'utf8', timeout: 30000 }
|
|
140
141
|
);
|
|
141
142
|
for (const linkPath of registryLinks.trim().split('\n').filter(Boolean)) {
|
|
142
|
-
try {
|
|
143
|
+
try {
|
|
144
|
+
removeWorkspaceHookDefaults(dirname(linkPath));
|
|
145
|
+
unlinkSync(linkPath);
|
|
146
|
+
removed++;
|
|
147
|
+
} catch { /* best effort */ }
|
|
143
148
|
}
|
|
144
149
|
|
|
145
150
|
// Also remove legacy local .git/hooks/post-checkout installed by old aw versions
|
|
@@ -217,7 +222,12 @@ export async function nukeCommand(args) {
|
|
|
217
222
|
// Remove stale local .aw_registry symlink if present (skip if cwd IS home — that's the global one)
|
|
218
223
|
if (process.cwd() !== HOME) {
|
|
219
224
|
const local = join(process.cwd(), '.aw_registry');
|
|
220
|
-
try {
|
|
225
|
+
try {
|
|
226
|
+
if (lstatSync(local).isSymbolicLink()) {
|
|
227
|
+
removeWorkspaceHookDefaults(process.cwd());
|
|
228
|
+
unlinkSync(local);
|
|
229
|
+
}
|
|
230
|
+
} catch { /* fine */ }
|
|
221
231
|
}
|
|
222
232
|
|
|
223
233
|
const manifest = loadManifest();
|
package/commands/pull.mjs
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import {
|
|
4
4
|
existsSync,
|
|
5
5
|
lstatSync,
|
|
6
|
+
rmSync,
|
|
6
7
|
} from 'node:fs';
|
|
7
|
-
import { join, extname } from 'node:path';
|
|
8
|
+
import { dirname, join, extname } from 'node:path';
|
|
8
9
|
import { homedir } from 'node:os';
|
|
9
10
|
import { exec as execCb, execSync } from 'node:child_process';
|
|
10
11
|
import { promisify } from 'node:util';
|
|
@@ -27,15 +28,34 @@ import {
|
|
|
27
28
|
REGISTRY_URL,
|
|
28
29
|
DOCS_SOURCE_DIR,
|
|
29
30
|
RULES_SOURCE_DIR,
|
|
31
|
+
RULES_RUNTIME_DIR,
|
|
30
32
|
} from '../constants.mjs';
|
|
31
33
|
import { collectAllPaths, syncFileTree } from '../file-tree.mjs';
|
|
32
34
|
import { linkWorkspace } from '../link.mjs';
|
|
33
|
-
import { generateCommands, copyInstructions } from '../integrate.mjs';
|
|
35
|
+
import { generateCommands, copyInstructions, syncHomeHarnessInstructions } from '../integrate.mjs';
|
|
36
|
+
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
37
|
+
import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.mjs';
|
|
34
38
|
|
|
35
39
|
const HOME = homedir();
|
|
36
40
|
const AW_HOME = join(HOME, '.aw');
|
|
37
41
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
38
42
|
|
|
43
|
+
function syncRulesTargets(targetDir) {
|
|
44
|
+
const rulesSrc = join(AW_HOME, RULES_SOURCE_DIR);
|
|
45
|
+
if (!existsSync(rulesSrc)) return false;
|
|
46
|
+
syncFileTree(rulesSrc, join(targetDir, RULES_RUNTIME_DIR));
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function removeLegacyRegistryRules() {
|
|
51
|
+
try {
|
|
52
|
+
rmSync(join(GLOBAL_AW_DIR, RULES_SOURCE_DIR), { recursive: true, force: true });
|
|
53
|
+
rmSync(join(HOME, RULES_SOURCE_DIR), { recursive: true, force: true });
|
|
54
|
+
} catch {
|
|
55
|
+
// best effort cleanup
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
export async function pullCommand(args) {
|
|
40
60
|
const input = args._positional?.[0] || '';
|
|
41
61
|
const cwd = process.cwd();
|
|
@@ -177,13 +197,6 @@ export async function pullCommand(args) {
|
|
|
177
197
|
log.logWarn(`Conflicts in: ${fetchResult.conflicts.join(', ')}`);
|
|
178
198
|
}
|
|
179
199
|
|
|
180
|
-
const rulesSrc = join(AW_HOME, RULES_SOURCE_DIR);
|
|
181
|
-
if (existsSync(rulesSrc)) {
|
|
182
|
-
const rulesDest = join(GLOBAL_AW_DIR, RULES_SOURCE_DIR);
|
|
183
|
-
syncFileTree(rulesSrc, rulesDest);
|
|
184
|
-
if (!silent) log.logSuccess('Synced .aw_rules');
|
|
185
|
-
}
|
|
186
|
-
|
|
187
200
|
// Rebase project worktree branch onto origin/main — only for legacy git worktrees.
|
|
188
201
|
// In the symlink model, <project>/.aw IS ~/.aw (same repo), so fetchAndMerge already
|
|
189
202
|
// brought it up to date. Nothing to rebase.
|
|
@@ -242,9 +255,20 @@ export async function pullCommand(args) {
|
|
|
242
255
|
}
|
|
243
256
|
}
|
|
244
257
|
|
|
258
|
+
let rulesSynced = syncRulesTargets(HOME);
|
|
259
|
+
const workspaceRoot = localAw ? dirname(localAw) : (cwd !== HOME ? cwd : null);
|
|
260
|
+
if (workspaceRoot && workspaceRoot !== HOME) {
|
|
261
|
+
rulesSynced = syncRulesTargets(workspaceRoot) || rulesSynced;
|
|
262
|
+
}
|
|
263
|
+
removeLegacyRegistryRules();
|
|
264
|
+
if (rulesSynced && !silent) {
|
|
265
|
+
log.logSuccess('Synced .aw_rules');
|
|
266
|
+
}
|
|
267
|
+
|
|
245
268
|
if (!args._skipIntegrate) {
|
|
246
269
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
247
270
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
271
|
+
const startupCleanupDir = localAw ? dirname(localAw) : (cwd !== HOME ? cwd : null);
|
|
248
272
|
|
|
249
273
|
if (!silent) {
|
|
250
274
|
const ideSpinner = log.spinner();
|
|
@@ -252,12 +276,18 @@ export async function pullCommand(args) {
|
|
|
252
276
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
253
277
|
ideSpinner.message('Generating commands...');
|
|
254
278
|
const commands = generateCommands(HOME, { silent: true });
|
|
255
|
-
|
|
279
|
+
syncHomeHarnessInstructions(HOME);
|
|
280
|
+
ensureAwRuntimeHook(HOME);
|
|
281
|
+
applyStoredStartupPreferences(HOME);
|
|
282
|
+
if (startupCleanupDir) removeWorkspaceHookDefaults(startupCleanupDir);
|
|
256
283
|
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlink${symlinks !== 1 ? 's' : ''}, ${chalk.bold(commands)} command${commands !== 1 ? 's' : ''}`);
|
|
257
284
|
} else {
|
|
258
285
|
linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
259
286
|
generateCommands(HOME, { silent: true });
|
|
260
|
-
|
|
287
|
+
syncHomeHarnessInstructions(HOME);
|
|
288
|
+
ensureAwRuntimeHook(HOME);
|
|
289
|
+
applyStoredStartupPreferences(HOME);
|
|
290
|
+
if (startupCleanupDir) removeWorkspaceHookDefaults(startupCleanupDir);
|
|
261
291
|
}
|
|
262
292
|
}
|
|
263
293
|
|
|
@@ -290,3 +320,73 @@ function registerMcp(namespace) {
|
|
|
290
320
|
fmt.logWarn('MCP registration failed (pull still successful)');
|
|
291
321
|
}
|
|
292
322
|
}
|
|
323
|
+
function printDryRun(actions, verbose) {
|
|
324
|
+
const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0, ORPHAN: 0, UNCHANGED: 0 };
|
|
325
|
+
const lines = [];
|
|
326
|
+
|
|
327
|
+
for (const type of ['agents', 'skills', 'commands', 'evals', 'references']) {
|
|
328
|
+
const items = actions.filter(a => a.type === type);
|
|
329
|
+
if (items.length === 0) continue;
|
|
330
|
+
|
|
331
|
+
lines.push(chalk.bold(`${type}/`));
|
|
332
|
+
for (const act of items.sort((a, b) => a.targetFilename.localeCompare(b.targetFilename))) {
|
|
333
|
+
counts[act.action] = (counts[act.action] || 0) + 1;
|
|
334
|
+
if (!verbose && act.action === 'UNCHANGED') continue;
|
|
335
|
+
const ns = act.namespacePath ? chalk.dim(` [${act.namespacePath}]`) : '';
|
|
336
|
+
lines.push(` ${fmt.actionLabel(act.action)} ${act.targetFilename}${ns}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (lines.length > 0) {
|
|
341
|
+
fmt.note(lines.join('\n'), 'Dry Run');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fmt.logInfo(`Summary: ${fmt.countSummary(counts)}`);
|
|
345
|
+
fmt.logWarn('No files modified (--dry-run)');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function printSummary(actions, verbose, conflictCount) {
|
|
349
|
+
const conflicts = actions.filter(a => a.action === 'CONFLICT');
|
|
350
|
+
|
|
351
|
+
for (const type of ['agents', 'skills', 'commands', 'evals', 'references']) {
|
|
352
|
+
const typeActions = actions.filter(a => a.type === type);
|
|
353
|
+
if (typeActions.length === 0) continue;
|
|
354
|
+
|
|
355
|
+
const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0, ORPHAN: 0, UNCHANGED: 0 };
|
|
356
|
+
for (const a of typeActions) counts[a.action]++;
|
|
357
|
+
|
|
358
|
+
const parts = [];
|
|
359
|
+
if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} new`));
|
|
360
|
+
if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
|
|
361
|
+
if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
|
|
362
|
+
if (counts.ORPHAN > 0) parts.push(chalk.yellow(`${counts.ORPHAN} removed`));
|
|
363
|
+
const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
364
|
+
|
|
365
|
+
fmt.logSuccess(`${typeActions.length} ${type} pulled${detail}`);
|
|
366
|
+
|
|
367
|
+
if (verbose) {
|
|
368
|
+
for (const a of typeActions.filter(a => a.action !== 'UNCHANGED')) {
|
|
369
|
+
const ns = a.namespacePath ? chalk.dim(` [${a.namespacePath}]`) : '';
|
|
370
|
+
fmt.logMessage(` ${fmt.actionLabel(a.action)} ${a.targetFilename}${ns}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (conflicts.length > 0) {
|
|
376
|
+
const conflictLines = conflicts.map(c => {
|
|
377
|
+
return `${chalk.red('both modified:')} ${c.type}/${c.targetFilename}`;
|
|
378
|
+
}).join('\n');
|
|
379
|
+
|
|
380
|
+
fmt.note(
|
|
381
|
+
conflictLines + '\n\n' +
|
|
382
|
+
chalk.dim('Fix conflicts, then re-run pull to verify.\n') +
|
|
383
|
+
chalk.dim('grep -r "<<<<<<< " .aw_registry/'),
|
|
384
|
+
chalk.red('Merge Conflicts')
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
fmt.outro(chalk.red('Pull completed with conflicts — resolve and re-run'));
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
fmt.outro('Pull complete');
|
|
392
|
+
}
|
package/commands/push-rules.mjs
CHANGED
|
@@ -23,11 +23,9 @@ export function isRulesPushInput(input) {
|
|
|
23
23
|
|
|
24
24
|
export function resolveRulesPushSource(input, cwd = process.cwd()) {
|
|
25
25
|
const localRulesRoot = join(cwd, RULES_SOURCE_DIR);
|
|
26
|
-
const syncedRulesRoot = join(cwd, '.aw_registry', RULES_SOURCE_DIR);
|
|
27
26
|
|
|
28
27
|
if (!input) {
|
|
29
28
|
if (existsSync(localRulesRoot)) return { sourceRoot: localRulesRoot, sourceType: 'local' };
|
|
30
|
-
if (existsSync(syncedRulesRoot)) return { sourceRoot: syncedRulesRoot, sourceType: 'synced' };
|
|
31
29
|
return null;
|
|
32
30
|
}
|
|
33
31
|
|
|
@@ -39,22 +37,19 @@ export function resolveRulesPushSource(input, cwd = process.cwd()) {
|
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
if (normalizedInput === `.aw_registry/${RULES_SOURCE_DIR}` || normalizedInput.startsWith(`.aw_registry/${RULES_SOURCE_DIR}/`)) {
|
|
42
|
-
if (!existsSync(syncedRulesRoot)) return null;
|
|
43
|
-
|
|
44
40
|
const relativeRulesPath = normalizedInput.slice(`.aw_registry/${RULES_SOURCE_DIR}`.length).replace(/^\/+/, '');
|
|
45
41
|
const localOverridePath = relativeRulesPath ? join(localRulesRoot, relativeRulesPath) : localRulesRoot;
|
|
46
42
|
if (existsSync(localOverridePath)) {
|
|
47
43
|
return { sourceRoot: localRulesRoot, sourceType: 'local' };
|
|
48
44
|
}
|
|
49
|
-
|
|
50
|
-
return { sourceRoot: syncedRulesRoot, sourceType: 'synced' };
|
|
45
|
+
return null;
|
|
51
46
|
}
|
|
52
47
|
|
|
53
48
|
return null;
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
export function hasRulesChanges(cwd = process.cwd()) {
|
|
57
|
-
const candidateDirs = [RULES_SOURCE_DIR
|
|
52
|
+
const candidateDirs = [RULES_SOURCE_DIR]
|
|
58
53
|
.filter(rel => existsSync(join(cwd, rel)));
|
|
59
54
|
|
|
60
55
|
if (candidateDirs.length === 0) return false;
|
|
@@ -141,9 +136,7 @@ function pushRulesTree(sourceRoot, { repo, dryRun, cwd }) {
|
|
|
141
136
|
fmt.cancel('Nothing to push — remote rules already match local content.');
|
|
142
137
|
}
|
|
143
138
|
|
|
144
|
-
const sourceType =
|
|
145
|
-
? 'synced'
|
|
146
|
-
: 'local';
|
|
139
|
+
const sourceType = 'local';
|
|
147
140
|
const prTitle = buildRulesPrTitle(sourceRoot, cwd);
|
|
148
141
|
const prBody = buildRulesPrBody(sourceRoot, sourceType, cwd);
|
|
149
142
|
|
|
@@ -191,16 +184,12 @@ export function pushRulesCommand(args) {
|
|
|
191
184
|
fmt.cancel([
|
|
192
185
|
'Could not find a rules source to push.',
|
|
193
186
|
'',
|
|
194
|
-
` Checked ${chalk.cyan('.aw_rules/')}
|
|
187
|
+
` Checked ${chalk.cyan('.aw_rules/')}.`,
|
|
195
188
|
'',
|
|
196
189
|
' Use `aw pull platform` first or create a local `.aw_rules/` authoring tree.',
|
|
197
190
|
].join('\n'));
|
|
198
191
|
}
|
|
199
192
|
|
|
200
|
-
if (resolved.sourceType === 'synced') {
|
|
201
|
-
fmt.logWarn('Pushing from synced `.aw_registry/.aw_rules/`. Local `.aw_rules/` is safer for authoring.');
|
|
202
|
-
}
|
|
203
|
-
|
|
204
193
|
pushRulesTree(resolved.sourceRoot, { repo, dryRun, cwd });
|
|
205
194
|
}
|
|
206
195
|
|