@ghl-ai/aw 0.1.70-beta.7 → 0.1.71-beta.0

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
@@ -60,6 +60,9 @@ function parseArgs(argv) {
60
60
  } else if (arg === '-v' || arg === '--verbose') {
61
61
  args['-v'] = true;
62
62
  i++;
63
+ } else if (arg === '-y') {
64
+ args['-y'] = true;
65
+ i++;
63
66
  } else if (arg === '--version') {
64
67
  args['--version'] = true;
65
68
  i++;
@@ -163,6 +166,7 @@ function printHelp() {
163
166
  cmd('aw docs validate --feature <slug>', 'Validate .aw_docs planning HTML companions before build handoff'),
164
167
  cmd('aw status', 'Show synced paths, modified files & conflicts'),
165
168
  cmd('aw doctor', 'Run a health check for routing, MCP, plugin, and AW ECC surfaces'),
169
+ cmd('aw doctor --fix', 'Apply safe, deduped doctor fixes after confirmation'),
166
170
  cmd('aw link', 'Link current project as a git worktree (wires IDE symlinks)'),
167
171
  cmd('aw routing status', 'Show global AW session-routing mode for Claude/Cursor/Codex'),
168
172
  cmd('aw routing disable', 'Disable default AW router/rules injection globally'),
@@ -1,8 +1,10 @@
1
- import { execSync } from 'node:child_process';
1
+ import { execSync, spawnSync } from 'node:child_process';
2
2
  import { existsSync, lstatSync, readFileSync, readlinkSync, readdirSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
 
7
+ import * as p from '@clack/prompts';
6
8
  import * as fmt from '../fmt.mjs';
7
9
  import { chalk } from '../fmt.mjs';
8
10
  import { AW_ECC_TAG } from '../ecc.mjs';
@@ -16,6 +18,45 @@ const STATUS_LABEL = {
16
18
  warn: chalk.yellow('WARN'),
17
19
  fail: chalk.red('FAIL'),
18
20
  };
21
+ const AW_BIN_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin.js');
22
+ const FIX_COMMANDS = [
23
+ { key: 'init', display: 'aw init --silent', argv: ['init', '--silent'], snippets: ['aw init'] },
24
+ { key: 'pull-platform', display: 'aw pull platform --silent', argv: ['pull', 'platform', '--silent'], snippets: ['aw pull platform'] },
25
+ { key: 'routing-enable', display: 'aw routing enable', argv: ['routing', 'enable'], snippets: ['aw routing enable'] },
26
+ { key: 'link', display: 'aw link', argv: ['link'], snippets: ['aw link'] },
27
+ {
28
+ key: 'integrations-add-context-mode',
29
+ display: 'aw integrations add context-mode',
30
+ argv: ['integrations', 'add', 'context-mode'],
31
+ snippets: ['aw integrations add context-mode'],
32
+ },
33
+ ];
34
+ const FIX_COMMAND_BY_SNIPPET = new Map(FIX_COMMANDS.flatMap(command =>
35
+ command.snippets.map(snippet => [snippet, command]),
36
+ ));
37
+ const FIX_COMMAND_ORDER = new Map(FIX_COMMANDS.map((command, index) => [command.key, index]));
38
+ const GLOBAL_INSTALL_REPAIR_CHECK_IDS = new Set([
39
+ 'claude-home-hooks',
40
+ 'claude-install-state',
41
+ 'claude-mcp',
42
+ 'claude-home-instructions',
43
+ 'codex-install-state',
44
+ 'codex-mcp',
45
+ 'codex-prompts',
46
+ 'codex-agents-md',
47
+ 'cursor-install-state',
48
+ 'cursor-mcp',
49
+ 'cursor-home-instructions',
50
+ 'cursor-commands',
51
+ 'aw-ecc',
52
+ ]);
53
+ const ROUTING_REPAIR_CHECK_IDS = new Set([
54
+ 'routing-mode',
55
+ 'repo-legacy-routing',
56
+ 'claude-session-start',
57
+ 'codex-routing',
58
+ 'cursor-routing',
59
+ ]);
19
60
 
20
61
  const GENERATED_RULE_HEADER = '<!-- Generated by aw — do not edit manually -->';
21
62
  const AW_ROUTER_BRIDGE_START_MARKER = '<!-- aw-managed:start router-bridge -->';
@@ -1066,24 +1107,328 @@ function statusIcon(status) {
1066
1107
  }
1067
1108
  }
1068
1109
 
1069
- export function doctorCommand() {
1070
- const homeDir = homedir();
1071
- const report = getDoctorReport(homeDir, process.cwd());
1110
+ function normalizeAwSnippet(snippet) {
1111
+ return String(snippet || '').trim().replace(/\s+/g, ' ');
1112
+ }
1072
1113
 
1073
- fmt.intro('aw doctor');
1114
+ function backtickedAwSnippets(fix) {
1115
+ const snippets = [];
1116
+ for (const match of String(fix || '').matchAll(/`([^`]+)`/g)) {
1117
+ const snippet = normalizeAwSnippet(match[1]);
1118
+ if (snippet.startsWith('aw ')) snippets.push(snippet);
1119
+ }
1120
+ return snippets;
1121
+ }
1122
+
1123
+ export function extractFixCommands(fix) {
1124
+ const commands = [];
1125
+ const seen = new Set();
1126
+ for (const snippet of backtickedAwSnippets(fix)) {
1127
+ const command = FIX_COMMAND_BY_SNIPPET.get(snippet);
1128
+ if (!command || seen.has(command.key)) continue;
1129
+ commands.push(command);
1130
+ seen.add(command.key);
1131
+ }
1132
+ return commands;
1133
+ }
1134
+
1135
+ function hasGlobalInstallRepair(report) {
1136
+ return report.checks.some(check =>
1137
+ check.status !== 'pass' && (
1138
+ GLOBAL_INSTALL_REPAIR_CHECK_IDS.has(check.id)
1139
+ || String(check.id || '').endsWith('-install-state')
1140
+ ),
1141
+ );
1142
+ }
1143
+
1144
+ function isRoutingRepairCheck(check) {
1145
+ return ROUTING_REPAIR_CHECK_IDS.has(check.id) || String(check.id || '').includes('routing');
1146
+ }
1147
+
1148
+ function commandByKey(commands, key) {
1149
+ return commands.find(command => command.key === key) || null;
1150
+ }
1151
+
1152
+ function chooseFixCommand(check, commands, report) {
1153
+ const init = commandByKey(commands, 'init');
1154
+ const pullPlatform = commandByKey(commands, 'pull-platform');
1155
+ const routingEnable = commandByKey(commands, 'routing-enable');
1156
+ const link = commandByKey(commands, 'link');
1157
+ const hasGlobalRepair = hasGlobalInstallRepair(report);
1158
+
1159
+ if (init && pullPlatform) {
1160
+ return hasGlobalRepair ? init : pullPlatform;
1161
+ }
1162
+
1163
+ if (init && routingEnable) {
1164
+ return hasGlobalRepair && !isRoutingRepairCheck(check) ? init : routingEnable;
1165
+ }
1166
+
1167
+ if (init && link) {
1168
+ return hasGlobalRepair ? init : link;
1169
+ }
1170
+
1171
+ return sortFixCommands(commands)[0] || null;
1172
+ }
1173
+
1174
+ export function sortFixCommands(commands) {
1175
+ return [...commands].sort((left, right) => (
1176
+ (FIX_COMMAND_ORDER.get(left.key) ?? Number.MAX_SAFE_INTEGER)
1177
+ - (FIX_COMMAND_ORDER.get(right.key) ?? Number.MAX_SAFE_INTEGER)
1178
+ ));
1179
+ }
1180
+
1181
+ export function buildDoctorFixPlan(report) {
1182
+ const commandsByKey = new Map();
1183
+ const manualFixes = [];
1184
+ const seenManualFixes = new Set();
1185
+
1186
+ for (const check of report.checks) {
1187
+ if (!check.fix || check.status === 'pass') continue;
1188
+
1189
+ const commands = extractFixCommands(check.fix);
1190
+ const selected = commands.length > 0 ? chooseFixCommand(check, commands, report) : null;
1191
+
1192
+ if (selected) {
1193
+ const existing = commandsByKey.get(selected.key) || {
1194
+ ...selected,
1195
+ sourceCheckIds: [],
1196
+ sourceFixes: [],
1197
+ };
1198
+ existing.sourceCheckIds.push(check.id);
1199
+ if (!existing.sourceFixes.includes(check.fix)) {
1200
+ existing.sourceFixes.push(check.fix);
1201
+ }
1202
+ commandsByKey.set(selected.key, existing);
1203
+ continue;
1204
+ }
1205
+
1206
+ if (!seenManualFixes.has(check.fix)) {
1207
+ manualFixes.push(check.fix);
1208
+ seenManualFixes.add(check.fix);
1209
+ }
1210
+ }
1211
+
1212
+ return {
1213
+ commands: sortFixCommands(commandsByKey.values()),
1214
+ manualFixes,
1215
+ };
1216
+ }
1217
+
1218
+ export function diffDoctorReports(beforeReport, afterReport) {
1219
+ const beforeById = new Map(beforeReport.checks.map(check => [check.id, check]));
1220
+ const afterById = new Map(afterReport.checks.map(check => [check.id, check]));
1221
+ const ids = [...new Set([...beforeById.keys(), ...afterById.keys()])];
1222
+
1223
+ return ids
1224
+ .map(id => {
1225
+ const before = beforeById.get(id);
1226
+ const after = afterById.get(id);
1227
+ if (!before || !after) {
1228
+ return {
1229
+ id,
1230
+ title: after?.title || before?.title || id,
1231
+ beforeStatus: before?.status || 'missing',
1232
+ beforeSummary: before?.summary || '',
1233
+ afterStatus: after?.status || 'missing',
1234
+ afterSummary: after?.summary || '',
1235
+ };
1236
+ }
1237
+ if (before.status === after.status && before.summary === after.summary) return null;
1238
+ return {
1239
+ id,
1240
+ title: after.title || before.title || id,
1241
+ beforeStatus: before.status,
1242
+ beforeSummary: before.summary,
1243
+ afterStatus: after.status,
1244
+ afterSummary: after.summary,
1245
+ };
1246
+ })
1247
+ .filter(Boolean);
1248
+ }
1074
1249
 
1075
- const summary = report.checks
1250
+ function formatDoctorSummary(report) {
1251
+ return report.checks
1076
1252
  .map(check => `${statusIcon(check.status)} ${check.title}: ${check.summary}`)
1077
1253
  .join('\n');
1078
- fmt.note(summary, `Health ${STATUS_LABEL[report.status]}`);
1254
+ }
1255
+
1256
+ function formatFixPlan(plan) {
1257
+ const lines = [];
1258
+ if (plan.commands.length > 0) {
1259
+ lines.push('Will run:');
1260
+ lines.push(...plan.commands.map(command => `- ${command.display}`));
1261
+ } else {
1262
+ lines.push('No automatic fixes are available for the current doctor report.');
1263
+ }
1079
1264
 
1080
- if (report.fixes.length > 0) {
1081
- fmt.note(report.fixes.map(fix => `- ${fix}`).join('\n'), 'Suggested Fixes');
1265
+ if (plan.manualFixes.length > 0) {
1266
+ lines.push('');
1267
+ lines.push('Not fixable automatically:');
1268
+ lines.push(...plan.manualFixes.map(fix => `- ${fix}`));
1082
1269
  }
1083
1270
 
1271
+ return lines.join('\n');
1272
+ }
1273
+
1274
+ function formatFixes(fixes) {
1275
+ return fixes.map(fix => `- ${fix}`).join('\n');
1276
+ }
1277
+
1278
+ function formatExecutedFixes(results) {
1279
+ if (results.length === 0) return 'No automatic fixes were executed.';
1280
+ return results
1281
+ .map(result => {
1282
+ const icon = result.ok ? chalk.green('✓') : chalk.red('✖');
1283
+ const suffix = result.ok ? '' : ` (exit ${result.exitCode})`;
1284
+ return `${icon} ${result.command.display}${suffix}`;
1285
+ })
1286
+ .join('\n');
1287
+ }
1288
+
1289
+ function formatDoctorDiff(diff) {
1290
+ if (diff.length === 0) return 'No check status changes detected.';
1291
+ return diff
1292
+ .map(change => {
1293
+ const statusChange = `${change.beforeStatus.toUpperCase()} -> ${change.afterStatus.toUpperCase()}`;
1294
+ return `${change.title}: ${statusChange}\n before: ${change.beforeSummary}\n after: ${change.afterSummary}`;
1295
+ })
1296
+ .join('\n');
1297
+ }
1298
+
1299
+ function runAwFixCommand(command, { cwd = process.cwd(), env = process.env } = {}) {
1300
+ const awBin = env.AW_DOCTOR_FIX_AW_BIN || AW_BIN_PATH;
1301
+ const result = spawnSync(process.execPath, [awBin, ...command.argv], {
1302
+ cwd,
1303
+ env: {
1304
+ ...env,
1305
+ AW_DOCTOR_FIX: '1',
1306
+ },
1307
+ encoding: 'utf8',
1308
+ stdio: ['ignore', 'pipe', 'pipe'],
1309
+ shell: false,
1310
+ });
1311
+
1312
+ return {
1313
+ command,
1314
+ ok: !result.error && result.status === 0,
1315
+ exitCode: result.status ?? 1,
1316
+ stdout: result.stdout || '',
1317
+ stderr: result.stderr || '',
1318
+ error: result.error || null,
1319
+ };
1320
+ }
1321
+
1322
+ function executeDoctorFixPlan(plan, options = {}) {
1323
+ const results = [];
1324
+ for (const command of plan.commands) {
1325
+ const result = runAwFixCommand(command, options);
1326
+ results.push(result);
1327
+ if (!result.ok) break;
1328
+ }
1329
+ return results;
1330
+ }
1331
+
1332
+ async function confirmDoctorFixPlan(plan) {
1333
+ const answer = await p.confirm({
1334
+ message: `Run ${plan.commands.length} automatic doctor fix${plan.commands.length === 1 ? '' : 'es'}?`,
1335
+ initialValue: false,
1336
+ });
1337
+ return !p.isCancel(answer) && answer === true;
1338
+ }
1339
+
1340
+ function setDoctorExitCode(report) {
1084
1341
  if (report.status === 'fail') {
1085
1342
  process.exitCode = 1;
1086
1343
  }
1344
+ }
1345
+
1346
+ export async function doctorCommand(args = {}) {
1347
+ const homeDir = homedir();
1348
+ const cwd = process.cwd();
1349
+ const report = getDoctorReport(homeDir, cwd);
1350
+ const shouldFix = args['--fix'] === true;
1351
+ const assumeYes = args['--yes'] === true || args['-y'] === true;
1352
+ const dryRun = args['--dry-run'] === true;
1353
+
1354
+ fmt.intro('aw doctor');
1355
+
1356
+ fmt.note(formatDoctorSummary(report), `Health ${STATUS_LABEL[report.status]}`);
1357
+
1358
+ if (!shouldFix) {
1359
+ if (report.fixes.length > 0) {
1360
+ fmt.note(formatFixes(report.fixes), 'Suggested Fixes');
1361
+ }
1087
1362
 
1088
- fmt.outro(`⟁ aw doctor complete (${report.status.toUpperCase()})`);
1363
+ setDoctorExitCode(report);
1364
+ fmt.outro(`⟁ aw doctor complete (${report.status.toUpperCase()})`);
1365
+ return;
1366
+ }
1367
+
1368
+ const plan = buildDoctorFixPlan(report);
1369
+ fmt.note(formatFixPlan(plan), 'Auto-Fix Plan');
1370
+
1371
+ if (dryRun) {
1372
+ fmt.outro('⟁ aw doctor auto-fix dry run complete');
1373
+ return;
1374
+ }
1375
+
1376
+ if (plan.commands.length === 0) {
1377
+ setDoctorExitCode(report);
1378
+ fmt.outro(`⟁ aw doctor auto-fix complete (${report.status.toUpperCase()})`);
1379
+ return;
1380
+ }
1381
+
1382
+ if (!assumeYes && !process.stdin.isTTY) {
1383
+ process.exitCode = 1;
1384
+ fmt.note('Auto-fix needs confirmation in an interactive terminal. Re-run with `aw doctor --fix --yes` to execute the plan non-interactively.', 'Confirmation Required');
1385
+ fmt.outro('⟁ aw doctor auto-fix cancelled');
1386
+ return;
1387
+ }
1388
+
1389
+ if (!assumeYes) {
1390
+ const confirmed = await confirmDoctorFixPlan(plan);
1391
+ if (!confirmed) {
1392
+ setDoctorExitCode(report);
1393
+ fmt.outro('⟁ aw doctor auto-fix cancelled');
1394
+ return;
1395
+ }
1396
+ }
1397
+
1398
+ const results = executeDoctorFixPlan(plan, { cwd, env: process.env });
1399
+ const failedResult = results.find(result => !result.ok);
1400
+ const afterReport = getDoctorReport(homeDir, cwd);
1401
+ const diff = diffDoctorReports(report, afterReport);
1402
+
1403
+ fmt.note(formatExecutedFixes(results), 'Executed Fixes');
1404
+
1405
+ if (failedResult) {
1406
+ const details = [
1407
+ `${failedResult.command.display} failed with exit ${failedResult.exitCode}.`,
1408
+ failedResult.stderr.trim() ? `stderr:\n${failedResult.stderr.trim()}` : null,
1409
+ failedResult.stdout.trim() ? `stdout:\n${failedResult.stdout.trim()}` : null,
1410
+ failedResult.error ? `error: ${failedResult.error.message}` : null,
1411
+ ].filter(Boolean).join('\n\n');
1412
+ fmt.note(details, 'Auto-fix command failed');
1413
+ }
1414
+
1415
+ fmt.note(formatDoctorDiff(diff), 'Before/After');
1416
+
1417
+ const remainingPlan = buildDoctorFixPlan(afterReport);
1418
+ if (remainingPlan.manualFixes.length > 0) {
1419
+ fmt.note(formatFixes(remainingPlan.manualFixes), 'Remaining Manual Fixes');
1420
+ }
1421
+
1422
+ if (failedResult || afterReport.status === 'fail') {
1423
+ process.exitCode = 1;
1424
+ }
1425
+
1426
+ fmt.outro(`⟁ aw doctor auto-fix complete (${afterReport.status.toUpperCase()})`);
1089
1427
  }
1428
+
1429
+ export const __test__ = {
1430
+ extractFixCommands,
1431
+ buildDoctorFixPlan,
1432
+ sortFixCommands,
1433
+ diffDoctorReports,
1434
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.70-beta.7",
3
+ "version": "0.1.71-beta.0",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {