@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 +4 -0
- package/commands/doctor.mjs +355 -10
- package/package.json +1 -1
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'),
|
package/commands/doctor.mjs
CHANGED
|
@@ -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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1110
|
+
function normalizeAwSnippet(snippet) {
|
|
1111
|
+
return String(snippet || '').trim().replace(/\s+/g, ' ');
|
|
1112
|
+
}
|
|
1072
1113
|
|
|
1073
|
-
|
|
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
|
-
|
|
1250
|
+
function formatDoctorSummary(report) {
|
|
1251
|
+
return report.checks
|
|
1076
1252
|
.map(check => `${statusIcon(check.status)} ${check.title}: ${check.summary}`)
|
|
1077
1253
|
.join('\n');
|
|
1078
|
-
|
|
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 (
|
|
1081
|
-
|
|
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
|
-
|
|
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
|
+
};
|