@ghl-ai/aw 0.1.70 → 0.1.71

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++;
@@ -150,8 +153,8 @@ function printHelp() {
150
153
 
151
154
  sec('Upload'),
152
155
  cmd('aw push', 'Push all modified files (creates one PR)'),
153
- cmd('aw push --aw-docs-only', 'Publish generated .aw_docs companions and print share links'),
154
156
  cmd('aw push --aw-docs-only --feature <slug>', 'Publish one .aw_docs feature folder and print share links'),
157
+ cmd('aw push --aw-docs-only --all', 'Publish ALL .aw_docs feature folders (explicit opt-in; otherwise scope with --feature)'),
155
158
  cmd('aw push <path>', 'Push file, folder, or namespace to registry'),
156
159
  cmd('aw push-rules [path]', 'Push platform rules to platform-docs'),
157
160
  cmd('aw push --dry-run [path]', 'Preview what would be pushed'),
@@ -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'),
@@ -236,7 +240,11 @@ export async function run(argv) {
236
240
  process.exit(0);
237
241
  }
238
242
 
239
- if (args['--help'] && !command) {
243
+ // Help must short-circuit BEFORE dispatching any command. Otherwise a
244
+ // command combined with --help (e.g. `aw push --help`) skips this gate and
245
+ // executes the command — which is how `aw push --help` triggered a real
246
+ // publish instead of printing help.
247
+ if (args['--help']) {
240
248
  printHelp();
241
249
  process.exit(0);
242
250
  }
@@ -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,329 @@ 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
+ setDoctorExitCode(report);
1373
+ fmt.outro(`⟁ aw doctor auto-fix dry run complete (${report.status.toUpperCase()})`);
1374
+ return;
1375
+ }
1376
+
1377
+ if (plan.commands.length === 0) {
1378
+ setDoctorExitCode(report);
1379
+ fmt.outro(`⟁ aw doctor auto-fix complete (${report.status.toUpperCase()})`);
1380
+ return;
1381
+ }
1382
+
1383
+ if (!assumeYes && !process.stdin.isTTY) {
1384
+ process.exitCode = 1;
1385
+ 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');
1386
+ fmt.outro('⟁ aw doctor auto-fix cancelled');
1387
+ return;
1388
+ }
1389
+
1390
+ if (!assumeYes) {
1391
+ const confirmed = await confirmDoctorFixPlan(plan);
1392
+ if (!confirmed) {
1393
+ setDoctorExitCode(report);
1394
+ fmt.outro('⟁ aw doctor auto-fix cancelled');
1395
+ return;
1396
+ }
1397
+ }
1398
+
1399
+ const results = executeDoctorFixPlan(plan, { cwd, env: process.env });
1400
+ const failedResult = results.find(result => !result.ok);
1401
+ const afterReport = getDoctorReport(homeDir, cwd);
1402
+ const diff = diffDoctorReports(report, afterReport);
1403
+
1404
+ fmt.note(formatExecutedFixes(results), 'Executed Fixes');
1405
+
1406
+ if (failedResult) {
1407
+ const details = [
1408
+ `${failedResult.command.display} failed with exit ${failedResult.exitCode}.`,
1409
+ failedResult.stderr.trim() ? `stderr:\n${failedResult.stderr.trim()}` : null,
1410
+ failedResult.stdout.trim() ? `stdout:\n${failedResult.stdout.trim()}` : null,
1411
+ failedResult.error ? `error: ${failedResult.error.message}` : null,
1412
+ ].filter(Boolean).join('\n\n');
1413
+ fmt.note(details, 'Auto-fix command failed');
1414
+ }
1415
+
1416
+ fmt.note(formatDoctorDiff(diff), 'Before/After');
1417
+
1418
+ const remainingPlan = buildDoctorFixPlan(afterReport);
1419
+ if (remainingPlan.manualFixes.length > 0) {
1420
+ fmt.note(formatFixes(remainingPlan.manualFixes), 'Remaining Manual Fixes');
1421
+ }
1422
+
1423
+ if (failedResult || afterReport.status === 'fail') {
1424
+ process.exitCode = 1;
1425
+ }
1426
+
1427
+ fmt.outro(`⟁ aw doctor auto-fix complete (${afterReport.status.toUpperCase()})`);
1089
1428
  }
1429
+
1430
+ export const __test__ = {
1431
+ extractFixCommands,
1432
+ buildDoctorFixPlan,
1433
+ sortFixCommands,
1434
+ diffDoctorReports,
1435
+ };
package/commands/push.mjs CHANGED
@@ -188,6 +188,20 @@ function resolveAwDocsScope(input, featureFlag) {
188
188
  return flagScope || inputScope;
189
189
  }
190
190
 
191
+ // A docs-only publish with no resolved scope would sweep EVERY
192
+ // .aw_docs/features/** folder into the shared docs repo. Require an explicit
193
+ // --all opt-in so an unscoped invocation (or one that lands here accidentally)
194
+ // fails closed instead of mass-publishing unrelated feature folders.
195
+ function assertDocsOnlyScopeOrAll(scope, all) {
196
+ if (!scope && all !== true) {
197
+ throw new Error(
198
+ 'Refusing to publish ALL .aw_docs/features/** at once. Pass --feature <slug> ' +
199
+ '(or a .aw_docs/features/<slug> path) to publish one feature folder, or pass ' +
200
+ '--all to publish everything intentionally.'
201
+ );
202
+ }
203
+ }
204
+
191
205
  function collectProjectAwDocs(cwd, home, scope = null) {
192
206
  const projectRoot = getProjectRoot(cwd, home);
193
207
  const source = join(projectRoot, AW_DOCS_DIR);
@@ -1284,6 +1298,7 @@ export async function pushCommand(args) {
1284
1298
  if (docsOnly) {
1285
1299
  try {
1286
1300
  const scope = resolveAwDocsScope(input, args['--feature']);
1301
+ assertDocsOnlyScopeOrAll(scope, args['--all'] === true);
1287
1302
  const result = await publishProjectAwDocs(cwd, HOME, dryRun, scope);
1288
1303
  if (!result.hasDocs) {
1289
1304
  fmt.cancel(scope
@@ -1608,3 +1623,10 @@ function groupBy(arr, key) {
1608
1623
  }
1609
1624
  return result;
1610
1625
  }
1626
+
1627
+ export const __test__ = {
1628
+ featureScopeFromInput,
1629
+ awDocsFeatureScope,
1630
+ resolveAwDocsScope,
1631
+ assertDocsOnlyScopeOrAll,
1632
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {