@ghl-ai/aw 0.1.38-beta.1 → 0.1.38-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/cli.mjs +26 -5
- package/commands/doctor.mjs +21 -19
- package/commands/init.mjs +30 -8
- package/commands/link-project.mjs +4 -0
- package/commands/nuke.mjs +19 -14
- package/commands/pull.mjs +27 -8
- package/commands/push-rules.mjs +6 -17
- package/commands/push.mjs +11 -4
- package/commands/slack-sim.mjs +128 -0
- package/commands/telemetry.mjs +31 -0
- package/config.mjs +1 -1
- package/constants.mjs +8 -1
- package/ecc.mjs +79 -3
- package/fmt.mjs +14 -0
- package/hooks.mjs +106 -5
- package/integrate.mjs +3 -3
- package/package.json +8 -5
- package/render-rules.mjs +6 -4
- package/slack-sim/fake-slack.mjs +200 -0
- package/slack-sim/http.mjs +170 -0
- package/slack-sim/in-process.mjs +263 -0
- package/slack-sim/render.mjs +42 -0
- package/slack-sim/scenario.mjs +64 -0
- package/slack-sim/scenarios/checkpoint-approve.json +21 -0
- package/slack-sim/scenarios/image-thread.json +27 -0
- package/slack-sim/scenarios/implementation-basic.json +18 -0
- package/slack-sim/scenarios/poll-webhook-race.json +18 -0
- package/slack-sim/scenarios/review-pr.json +14 -0
- package/telemetry.mjs +233 -0
package/cli.mjs
CHANGED
|
@@ -4,8 +4,9 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import * as fmt from './fmt.mjs';
|
|
7
|
-
import { chalk } from './fmt.mjs';
|
|
7
|
+
import { chalk, CancelError } from './fmt.mjs';
|
|
8
8
|
import { checkForUpdate, notifyUpdate } from './update.mjs';
|
|
9
|
+
import { startSpan } from './telemetry.mjs';
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const VERSION = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version;
|
|
@@ -24,6 +25,7 @@ const COMMANDS = {
|
|
|
24
25
|
link: () => import('./commands/link-project.mjs').then(m => m.linkProjectCommand),
|
|
25
26
|
nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
|
|
26
27
|
daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
|
|
28
|
+
telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
function parseArgs(argv) {
|
|
@@ -109,6 +111,13 @@ function printHelp() {
|
|
|
109
111
|
cmd('aw daemon install --interval 30m', 'Set custom interval (e.g. 30m, 2h, 3600)'),
|
|
110
112
|
cmd('aw daemon uninstall', 'Stop the background daemon'),
|
|
111
113
|
cmd('aw daemon status', 'Check if daemon is running'),
|
|
114
|
+
cmd('aw slack-sim run <scenario>', 'Replay Slack-like scenarios against real runtime'),
|
|
115
|
+
cmd('aw slack-sim list-scenarios', 'List built-in Slack simulator scenarios'),
|
|
116
|
+
|
|
117
|
+
sec('Settings'),
|
|
118
|
+
cmd('aw telemetry status', 'Show telemetry status'),
|
|
119
|
+
cmd('aw telemetry disable', 'Opt out of anonymous analytics'),
|
|
120
|
+
cmd('aw telemetry enable', 'Re-enable analytics'),
|
|
112
121
|
|
|
113
122
|
sec('Examples'),
|
|
114
123
|
'',
|
|
@@ -125,7 +134,7 @@ function printHelp() {
|
|
|
125
134
|
cmd('aw push .aw_registry/agents/<name>.md', 'Push a single agent'),
|
|
126
135
|
cmd('aw push .aw_registry/skills/<name>/', 'Push a single skill folder'),
|
|
127
136
|
cmd('aw push .aw_rules', 'Auto-redirects to aw push-rules'),
|
|
128
|
-
cmd('aw push-rules', 'Pushes .aw_rules
|
|
137
|
+
cmd('aw push-rules', 'Pushes .aw_rules'),
|
|
129
138
|
'',
|
|
130
139
|
` ${chalk.dim('# Remove content from workspace')}`,
|
|
131
140
|
cmd('aw drop <team>', 'Stop syncing a namespace (removes all files)'),
|
|
@@ -158,9 +167,21 @@ export async function run(argv) {
|
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
if (command && COMMANDS[command]) {
|
|
170
|
+
const span = await startSpan(command, args);
|
|
171
|
+
span.notice();
|
|
161
172
|
args._updateCheck = updateCheck;
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
try {
|
|
174
|
+
const handler = await COMMANDS[command]();
|
|
175
|
+
await handler(args);
|
|
176
|
+
await span.end({ status: 'completed' });
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err instanceof CancelError) {
|
|
179
|
+
await span.end({ status: 'cancelled', error_type: 'CancelError' });
|
|
180
|
+
process.exit(err.exitCode ?? 1);
|
|
181
|
+
}
|
|
182
|
+
await span.end({ status: 'failed', error_type: err.constructor.name });
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
164
185
|
notifyUpdate(await updateCheck);
|
|
165
186
|
return;
|
|
166
187
|
}
|
|
@@ -170,5 +191,5 @@ export async function run(argv) {
|
|
|
170
191
|
process.exit(0);
|
|
171
192
|
}
|
|
172
193
|
|
|
173
|
-
fmt.
|
|
194
|
+
fmt.cancelAndExit(`Unknown command: ${command}`);
|
|
174
195
|
}
|
package/commands/doctor.mjs
CHANGED
|
@@ -162,10 +162,12 @@ function resolveAwRegistryDir(homeDir) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
function resolveRulesDir(homeDir) {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
const candidates = [
|
|
166
|
+
join(homeDir, '.aw_rules'),
|
|
167
|
+
join(homeDir, '.aw_registry', '.aw_rules'),
|
|
168
|
+
join(homeDir, '.aw', '.aw_registry', '.aw_rules'),
|
|
169
|
+
];
|
|
170
|
+
return candidates.find(existsSync) || null;
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
function findClaudePlugin(homeDir) {
|
|
@@ -471,7 +473,7 @@ function getProjectClaudeSessionStartStatus(cwd) {
|
|
|
471
473
|
}
|
|
472
474
|
|
|
473
475
|
function textHasRulesReference(text) {
|
|
474
|
-
return text.includes('Platform Rules') && text.includes('.
|
|
476
|
+
return text.includes('Platform Rules') && text.includes('.aw_rules/platform/');
|
|
475
477
|
}
|
|
476
478
|
|
|
477
479
|
function textHasManagedRouterBridge(text) {
|
|
@@ -484,9 +486,9 @@ function textHasManagedRouterBridge(text) {
|
|
|
484
486
|
&& text.includes('incremental-implementation');
|
|
485
487
|
}
|
|
486
488
|
|
|
487
|
-
function
|
|
489
|
+
function extractRuleReferencePaths(text) {
|
|
488
490
|
const matches = [];
|
|
489
|
-
const pattern = /(?:\]\(|`)([^)\n`]
|
|
491
|
+
const pattern = /(?:\]\(|`)([^)\n`]*(?:\.aw_rules|\.aw_registry\/\.aw_rules)\/[^)\n`]+\.md)(?:\)|`)/g;
|
|
490
492
|
for (const match of text.matchAll(pattern)) {
|
|
491
493
|
if (match[1]) matches.push(match[1]);
|
|
492
494
|
}
|
|
@@ -505,11 +507,11 @@ function listGeneratedRuleFiles(dirPath, extension) {
|
|
|
505
507
|
}
|
|
506
508
|
}
|
|
507
509
|
|
|
508
|
-
function
|
|
510
|
+
function findBrokenRuleReferences(filePaths) {
|
|
509
511
|
const broken = [];
|
|
510
512
|
for (const filePath of filePaths) {
|
|
511
513
|
const content = readText(filePath);
|
|
512
|
-
const refs =
|
|
514
|
+
const refs = extractRuleReferencePaths(content);
|
|
513
515
|
for (const ref of refs) {
|
|
514
516
|
if (ref.includes('<domain>')) continue;
|
|
515
517
|
const resolvedPath = join(dirname(filePath), ref);
|
|
@@ -565,8 +567,8 @@ function buildDoctorChecks(homeDir, cwd) {
|
|
|
565
567
|
'aw-rules-source',
|
|
566
568
|
'AW rules source',
|
|
567
569
|
'fail',
|
|
568
|
-
'No synced .aw_rules tree found under ~/.
|
|
569
|
-
'Run `aw init` or `aw pull platform` to sync the AW rules source into ~/.
|
|
570
|
+
'No synced .aw_rules tree found under ~/.aw_rules',
|
|
571
|
+
'Run `aw init` or `aw pull platform` to sync the AW rules source into ~/.aw_rules.',
|
|
570
572
|
));
|
|
571
573
|
} else {
|
|
572
574
|
const topLevelRulesAgents = join(rulesDir, 'AGENTS.md');
|
|
@@ -578,7 +580,7 @@ function buildDoctorChecks(homeDir, cwd) {
|
|
|
578
580
|
'aw-rules-source',
|
|
579
581
|
'AW rules source',
|
|
580
582
|
'fail',
|
|
581
|
-
'Rules source is incomplete under ~/.
|
|
583
|
+
'Rules source is incomplete under ~/.aw_rules',
|
|
582
584
|
'Run `aw pull platform` to refresh .aw_rules, then rerun `aw doctor`.',
|
|
583
585
|
),
|
|
584
586
|
);
|
|
@@ -784,7 +786,7 @@ function buildDoctorChecks(homeDir, cwd) {
|
|
|
784
786
|
);
|
|
785
787
|
|
|
786
788
|
const claudeRuleFiles = listGeneratedRuleFiles(claudeRulesDir, '.md');
|
|
787
|
-
const brokenClaudeRuleRefs =
|
|
789
|
+
const brokenClaudeRuleRefs = findBrokenRuleReferences(claudeRuleFiles);
|
|
788
790
|
checks.push(
|
|
789
791
|
claudeRuleFiles.length === 0
|
|
790
792
|
? makeCheck(
|
|
@@ -795,13 +797,13 @@ function buildDoctorChecks(homeDir, cwd) {
|
|
|
795
797
|
'Run `aw pull platform` or `aw init` to render Claude rules, then rerun `aw doctor`.',
|
|
796
798
|
)
|
|
797
799
|
: brokenClaudeRuleRefs.length === 0
|
|
798
|
-
? makeCheck('claude-rule-references', 'Claude rule references', 'pass', 'Claude generated rule references resolve to real files in ~/.
|
|
800
|
+
? makeCheck('claude-rule-references', 'Claude rule references', 'pass', 'Claude generated rule references resolve to real files in ~/.aw_rules')
|
|
799
801
|
: makeCheck(
|
|
800
802
|
'claude-rule-references',
|
|
801
803
|
'Claude rule references',
|
|
802
804
|
'fail',
|
|
803
805
|
`Claude rule reference is broken in ${brokenClaudeRuleRefs[0].filePath.replace(`${homeDir}/`, '~/')}: ${brokenClaudeRuleRefs[0].ref}`,
|
|
804
|
-
'Refresh the rendered Claude rules so embedded reference links point at real files under ~/.
|
|
806
|
+
'Refresh the rendered Claude rules so embedded reference links point at real files under ~/.aw_rules.',
|
|
805
807
|
),
|
|
806
808
|
);
|
|
807
809
|
|
|
@@ -894,7 +896,7 @@ function buildDoctorChecks(homeDir, cwd) {
|
|
|
894
896
|
);
|
|
895
897
|
|
|
896
898
|
const codexAgentsText = readText(codexAgentsPath);
|
|
897
|
-
const codexBrokenRuleRefs = existsSync(codexAgentsPath) ?
|
|
899
|
+
const codexBrokenRuleRefs = existsSync(codexAgentsPath) ? findBrokenRuleReferences([codexAgentsPath]) : [];
|
|
898
900
|
checks.push(
|
|
899
901
|
existsSync(codexAgentsPath) && textHasRulesReference(codexAgentsText)
|
|
900
902
|
? codexBrokenRuleRefs.length === 0
|
|
@@ -1004,7 +1006,7 @@ function buildDoctorChecks(homeDir, cwd) {
|
|
|
1004
1006
|
|
|
1005
1007
|
const cursorRulesDir = join(homeDir, '.cursor', 'rules');
|
|
1006
1008
|
const cursorRuleFiles = listGeneratedRuleFiles(cursorRulesDir, '.mdc');
|
|
1007
|
-
const brokenCursorRuleRefs =
|
|
1009
|
+
const brokenCursorRuleRefs = findBrokenRuleReferences(cursorRuleFiles);
|
|
1008
1010
|
checks.push(
|
|
1009
1011
|
cursorRuleFiles.length === 0
|
|
1010
1012
|
? makeCheck(
|
|
@@ -1015,13 +1017,13 @@ function buildDoctorChecks(homeDir, cwd) {
|
|
|
1015
1017
|
'Run `aw pull platform` or `aw init` to render Cursor rules, then rerun `aw doctor`.',
|
|
1016
1018
|
)
|
|
1017
1019
|
: brokenCursorRuleRefs.length === 0
|
|
1018
|
-
? makeCheck('cursor-rule-references', 'Cursor rule references', 'pass', 'Cursor generated rule references resolve to real files in ~/.
|
|
1020
|
+
? makeCheck('cursor-rule-references', 'Cursor rule references', 'pass', 'Cursor generated rule references resolve to real files in ~/.aw_rules')
|
|
1019
1021
|
: makeCheck(
|
|
1020
1022
|
'cursor-rule-references',
|
|
1021
1023
|
'Cursor rule references',
|
|
1022
1024
|
'fail',
|
|
1023
1025
|
`Cursor rule reference is broken in ${brokenCursorRuleRefs[0].filePath.replace(`${homeDir}/`, '~/')}: ${brokenCursorRuleRefs[0].ref}`,
|
|
1024
|
-
'Refresh the rendered Cursor rules so embedded reference links point at real files under ~/.
|
|
1026
|
+
'Refresh the rendered Cursor rules so embedded reference links point at real files under ~/.aw_rules.',
|
|
1025
1027
|
),
|
|
1026
1028
|
);
|
|
1027
1029
|
|
package/commands/init.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { linkWorkspace } from '../link.mjs';
|
|
|
26
26
|
import { generateCommands, copyInstructions, initAwDocs, syncHomeHarnessInstructions } from '../integrate.mjs';
|
|
27
27
|
import { setupMcp } from '../mcp.mjs';
|
|
28
28
|
import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.mjs';
|
|
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';
|
|
@@ -58,6 +59,21 @@ const HOME = (() => { try { return realpathSync(_rawHome); } catch { return _raw
|
|
|
58
59
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
59
60
|
const AW_HOME = join(HOME, '.aw');
|
|
60
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_SOURCE_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
|
+
} catch {
|
|
73
|
+
// best effort cleanup
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
function syncInstructionsAndAwDocs(targetDir, namespace) {
|
|
62
78
|
copyInstructions(targetDir, null, namespace);
|
|
63
79
|
initAwDocs(targetDir);
|
|
@@ -100,7 +116,7 @@ function installIdeTasks() {
|
|
|
100
116
|
{
|
|
101
117
|
label: 'aw: sync registry',
|
|
102
118
|
type: 'shell',
|
|
103
|
-
command: 'aw init --silent',
|
|
119
|
+
command: 'AW_TRIGGER=ide:task aw init --silent',
|
|
104
120
|
presentation: { reveal: 'silent', panel: 'shared', close: true },
|
|
105
121
|
runOptions: { runOn: 'folderOpen' },
|
|
106
122
|
problemMatcher: [],
|
|
@@ -221,7 +237,7 @@ export async function initCommand(args) {
|
|
|
221
237
|
}
|
|
222
238
|
|
|
223
239
|
if (choice === 'platform-only') {
|
|
224
|
-
namespace =
|
|
240
|
+
namespace = 'platform'; team = 'platform'; subTeam = null; folderName = null;
|
|
225
241
|
}
|
|
226
242
|
}
|
|
227
243
|
|
|
@@ -261,9 +277,11 @@ export async function initCommand(args) {
|
|
|
261
277
|
|
|
262
278
|
ensureAwGitignore(AW_HOME);
|
|
263
279
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
264
|
-
|
|
265
|
-
|
|
280
|
+
syncRulesTargets(HOME);
|
|
281
|
+
if (cwd !== HOME) {
|
|
282
|
+
syncRulesTargets(cwd);
|
|
266
283
|
}
|
|
284
|
+
removeLegacyRegistryRules();
|
|
267
285
|
|
|
268
286
|
// Ensure project worktree sparse checkout matches the global clone.
|
|
269
287
|
// Covers the case where a namespace was added from HOME (or another project)
|
|
@@ -310,6 +328,7 @@ export async function initCommand(args) {
|
|
|
310
328
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
311
329
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
312
330
|
const commands = generateCommands(HOME, { silent: true });
|
|
331
|
+
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
313
332
|
|
|
314
333
|
if (silent) {
|
|
315
334
|
autoUpdate(await args._updateCheck);
|
|
@@ -397,14 +416,16 @@ export async function initCommand(args) {
|
|
|
397
416
|
}
|
|
398
417
|
}
|
|
399
418
|
|
|
400
|
-
// Create sync config
|
|
401
|
-
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
419
|
+
// Create sync config — default to 'platform' when no namespace specified
|
|
420
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
|
|
402
421
|
if (folderName) {
|
|
403
422
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
404
423
|
}
|
|
405
|
-
|
|
406
|
-
|
|
424
|
+
syncRulesTargets(HOME);
|
|
425
|
+
if (cwd !== HOME) {
|
|
426
|
+
syncRulesTargets(cwd);
|
|
407
427
|
}
|
|
428
|
+
removeLegacyRegistryRules();
|
|
408
429
|
|
|
409
430
|
// Step 3: Setup tasks, MCP, hooks
|
|
410
431
|
await installAwEcc(cwd, { silent });
|
|
@@ -445,6 +466,7 @@ export async function initCommand(args) {
|
|
|
445
466
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
446
467
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
447
468
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
469
|
+
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
448
470
|
ideSpinner.message('Generating commands...');
|
|
449
471
|
const commands = generateCommands(HOME, { silent: true });
|
|
450
472
|
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
@@ -11,6 +11,7 @@ import { linkWorkspace } from '../link.mjs';
|
|
|
11
11
|
import { generateCommands } from '../integrate.mjs';
|
|
12
12
|
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
13
13
|
import { applyStoredStartupPreferences } from '../startup.mjs';
|
|
14
|
+
import { installLocalCommitHook } from '../hooks.mjs';
|
|
14
15
|
|
|
15
16
|
const HOME = homedir();
|
|
16
17
|
const AW_HOME = join(HOME, '.aw');
|
|
@@ -43,8 +44,10 @@ export function linkProjectCommand(args) {
|
|
|
43
44
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
44
45
|
const commands = generateCommands(HOME, { silent: true });
|
|
45
46
|
applyStoredStartupPreferences(HOME);
|
|
47
|
+
installLocalCommitHook(cwd);
|
|
46
48
|
const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
|
|
47
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
|
+
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -55,6 +58,7 @@ export function linkProjectCommand(args) {
|
|
|
55
58
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
56
59
|
const commands = generateCommands(HOME, { silent: true });
|
|
57
60
|
applyStoredStartupPreferences(HOME);
|
|
61
|
+
installLocalCommitHook(cwd);
|
|
58
62
|
const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
|
|
59
63
|
fmt.logSuccess([
|
|
60
64
|
`Project linked — ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`,
|
package/commands/nuke.mjs
CHANGED
|
@@ -148,19 +148,24 @@ async function removeProjectSymlinks() {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// Also remove legacy local .git/hooks/post-checkout installed by old aw versions
|
|
151
|
+
// and prepare-commit-msg hooks installed by installLocalCommitHook
|
|
151
152
|
let hooksRemoved = 0;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
153
|
+
const hookNames = ['post-checkout', 'prepare-commit-msg'];
|
|
154
|
+
for (const hookName of hookNames) {
|
|
155
|
+
const { stdout: hookFiles } = await exec(
|
|
156
|
+
`find "${HOME}" -maxdepth 5 -path "*/.git/hooks/${hookName}" -type f 2>/dev/null || true`,
|
|
157
|
+
{ encoding: 'utf8', timeout: 30000 }
|
|
158
|
+
);
|
|
159
|
+
for (const hookPath of hookFiles.trim().split('\n').filter(Boolean)) {
|
|
160
|
+
try {
|
|
161
|
+
const content = readFileSync(hookPath, 'utf8');
|
|
162
|
+
// Only remove hooks that AW installed — identified by our marker comment
|
|
163
|
+
if (content.includes('aw:') || content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
164
|
+
unlinkSync(hookPath);
|
|
165
|
+
hooksRemoved++;
|
|
166
|
+
}
|
|
167
|
+
} catch { /* best effort */ }
|
|
168
|
+
}
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
return { removed, hooksRemoved };
|
|
@@ -209,8 +214,8 @@ function removeIdeTasks() {
|
|
|
209
214
|
|
|
210
215
|
export async function nukeCommand(args) {
|
|
211
216
|
// Catch unhandled errors and surface them instead of letting clack show generic "Something went wrong"
|
|
212
|
-
process.on('uncaughtException', (e) => { fmt.
|
|
213
|
-
process.on('unhandledRejection', (e) => { fmt.
|
|
217
|
+
process.on('uncaughtException', (e) => { fmt.cancelAndExit(`Unexpected error: ${e.message}`); });
|
|
218
|
+
process.on('unhandledRejection', (e) => { fmt.cancelAndExit(`Unexpected error: ${e?.message ?? e}`); });
|
|
214
219
|
|
|
215
220
|
fmt.intro('aw nuke');
|
|
216
221
|
|
package/commands/pull.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
existsSync,
|
|
5
5
|
lstatSync,
|
|
6
|
+
rmSync,
|
|
6
7
|
} from 'node:fs';
|
|
7
8
|
import { dirname, join, extname } from 'node:path';
|
|
8
9
|
import { homedir } from 'node:os';
|
|
@@ -38,13 +39,28 @@ const HOME = homedir();
|
|
|
38
39
|
const AW_HOME = join(HOME, '.aw');
|
|
39
40
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
40
41
|
|
|
42
|
+
function syncRulesTargets(targetDir) {
|
|
43
|
+
const rulesSrc = join(AW_HOME, RULES_SOURCE_DIR);
|
|
44
|
+
if (!existsSync(rulesSrc)) return false;
|
|
45
|
+
syncFileTree(rulesSrc, join(targetDir, RULES_SOURCE_DIR));
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function removeLegacyRegistryRules() {
|
|
50
|
+
try {
|
|
51
|
+
rmSync(join(GLOBAL_AW_DIR, RULES_SOURCE_DIR), { recursive: true, force: true });
|
|
52
|
+
} catch {
|
|
53
|
+
// best effort cleanup
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
export async function pullCommand(args) {
|
|
42
58
|
const input = args._positional?.[0] || '';
|
|
43
59
|
const cwd = process.cwd();
|
|
44
60
|
const silent = args['--silent'] === true || args._silent === true;
|
|
45
61
|
|
|
46
62
|
const log = {
|
|
47
|
-
cancel: silent ? () => {
|
|
63
|
+
cancel: silent ? (msg) => { throw new fmt.CancelError(msg || 'silent cancel', { exitCode: 0 }); } : fmt.cancel,
|
|
48
64
|
logInfo: silent ? () => {} : fmt.logInfo,
|
|
49
65
|
logSuccess: silent ? () => {} : fmt.logSuccess,
|
|
50
66
|
logStep: silent ? () => {} : fmt.logStep,
|
|
@@ -179,13 +195,6 @@ export async function pullCommand(args) {
|
|
|
179
195
|
log.logWarn(`Conflicts in: ${fetchResult.conflicts.join(', ')}`);
|
|
180
196
|
}
|
|
181
197
|
|
|
182
|
-
const rulesSrc = join(AW_HOME, RULES_SOURCE_DIR);
|
|
183
|
-
if (existsSync(rulesSrc)) {
|
|
184
|
-
const rulesDest = join(GLOBAL_AW_DIR, RULES_SOURCE_DIR);
|
|
185
|
-
syncFileTree(rulesSrc, rulesDest);
|
|
186
|
-
if (!silent) log.logSuccess('Synced .aw_rules');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
198
|
// Rebase project worktree branch onto origin/main — only for legacy git worktrees.
|
|
190
199
|
// In the symlink model, <project>/.aw IS ~/.aw (same repo), so fetchAndMerge already
|
|
191
200
|
// brought it up to date. Nothing to rebase.
|
|
@@ -244,6 +253,16 @@ export async function pullCommand(args) {
|
|
|
244
253
|
}
|
|
245
254
|
}
|
|
246
255
|
|
|
256
|
+
let rulesSynced = syncRulesTargets(HOME);
|
|
257
|
+
const workspaceRoot = localAw ? dirname(localAw) : (cwd !== HOME ? cwd : null);
|
|
258
|
+
if (workspaceRoot && workspaceRoot !== HOME) {
|
|
259
|
+
rulesSynced = syncRulesTargets(workspaceRoot) || rulesSynced;
|
|
260
|
+
}
|
|
261
|
+
removeLegacyRegistryRules();
|
|
262
|
+
if (rulesSynced && !silent) {
|
|
263
|
+
log.logSuccess('Synced .aw_rules');
|
|
264
|
+
}
|
|
265
|
+
|
|
247
266
|
if (!args._skipIntegrate) {
|
|
248
267
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
249
268
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
package/commands/push-rules.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
|
|
6
6
|
import * as fmt from '../fmt.mjs';
|
|
7
7
|
import { chalk } from '../fmt.mjs';
|
|
8
|
-
import { REGISTRY_BASE_BRANCH, REGISTRY_REPO, RULES_SOURCE_DIR } from '../constants.mjs';
|
|
8
|
+
import { REGISTRY_BASE_BRANCH, REGISTRY_REPO, RULES_SOURCE_DIR, AW_CO_AUTHOR } from '../constants.mjs';
|
|
9
9
|
import { syncFileTree } from '../file-tree.mjs';
|
|
10
10
|
|
|
11
11
|
function normalizeSlashes(path) {
|
|
@@ -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,13 +136,11 @@ 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
|
|
|
150
|
-
execSync(
|
|
143
|
+
execSync(`git commit -m "registry: sync platform rules\n\n${AW_CO_AUTHOR}"`, { cwd: tempDir, stdio: 'pipe' });
|
|
151
144
|
s2.stop('Rules sync prepared');
|
|
152
145
|
|
|
153
146
|
const s3 = fmt.spinner();
|
|
@@ -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
|
|
package/commands/push.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// commands/push.mjs — Push local agents/skills to registry via PR using persistent git clone
|
|
2
2
|
|
|
3
3
|
import { existsSync, statSync, readFileSync, appendFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { exec as execCb, execFile as execFileCb } from 'node:child_process';
|
|
6
7
|
import { promisify } from 'node:util';
|
|
7
8
|
import { homedir } from 'node:os';
|
|
@@ -10,7 +11,7 @@ const exec = promisify(execCb);
|
|
|
10
11
|
const execFile = promisify(execFileCb);
|
|
11
12
|
import * as fmt from '../fmt.mjs';
|
|
12
13
|
import { chalk } from '../fmt.mjs';
|
|
13
|
-
import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR } from '../constants.mjs';
|
|
14
|
+
import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR, AW_CO_AUTHOR } from '../constants.mjs';
|
|
14
15
|
import { resolveInput } from '../paths.mjs';
|
|
15
16
|
import { walkRegistryTree } from '../registry.mjs';
|
|
16
17
|
import {
|
|
@@ -25,6 +26,9 @@ import {
|
|
|
25
26
|
} from '../git.mjs';
|
|
26
27
|
import { hasRulesChanges, isRulesPushInput, pushRulesCommand } from './push-rules.mjs';
|
|
27
28
|
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
31
|
+
|
|
28
32
|
const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals', 'references'];
|
|
29
33
|
|
|
30
34
|
// ── PR content generation ────────────────────────────────────────────
|
|
@@ -187,11 +191,14 @@ function generateCommitMsg(files) {
|
|
|
187
191
|
const deletedParts = Object.entries(groupBy(deleted, 'type')).map(([t, items]) => `${items.length} ${singular(t, items.length)} removed`);
|
|
188
192
|
const countParts = [...addedParts, ...deletedParts];
|
|
189
193
|
|
|
194
|
+
const version = VERSION;
|
|
195
|
+
const trailer = `\n\nGenerated-By: aw/${version}\n${AW_CO_AUTHOR}`;
|
|
196
|
+
|
|
190
197
|
if (files.length === 1) {
|
|
191
198
|
const f = files[0];
|
|
192
|
-
return `registry: ${f.deleted ? 'remove' : 'add'} ${f.type}/${f.slug} ${f.deleted ? 'from' : 'to'} ${f.namespace}`;
|
|
199
|
+
return `registry: ${f.deleted ? 'remove' : 'add'} ${f.type}/${f.slug} ${f.deleted ? 'from' : 'to'} ${f.namespace}${trailer}`;
|
|
193
200
|
}
|
|
194
|
-
return `registry: sync ${files.length} files (${countParts.join(', ')})`;
|
|
201
|
+
return `registry: sync ${files.length} files (${countParts.join(', ')})${trailer}`;
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
// ── Batch file collection from folder ────────────────────────────────
|