facult 2.6.0 → 2.7.1
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/README.md +145 -337
- package/package.json +1 -1
- package/src/adapters/codex.ts +1 -1
- package/src/audit/agent.ts +26 -24
- package/src/audit/fix.ts +875 -0
- package/src/audit/index.ts +51 -2
- package/src/audit/safe.ts +596 -0
- package/src/audit/static.ts +151 -34
- package/src/audit/status.ts +21 -0
- package/src/audit/suppressions.ts +266 -0
- package/src/audit/tui.ts +784 -174
- package/src/audit/update-index.ts +4 -17
- package/src/builtin.ts +7 -1
- package/src/cli-ui.ts +375 -0
- package/src/consolidate.ts +151 -55
- package/src/doctor.ts +327 -0
- package/src/global-docs.ts +43 -2
- package/src/index.ts +571 -292
- package/src/manage.ts +931 -88
- package/src/mcp-config.ts +132 -0
- package/src/project-sync.ts +288 -0
- package/src/remote.ts +387 -117
- package/src/trust.ts +119 -11
- package/src/util/git.ts +95 -0
package/src/consolidate.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { mkdir, mkdtemp, readdir, rename, rm, stat } from "node:fs/promises";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import { basename, dirname, extname, join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
confirm,
|
|
6
|
+
intro,
|
|
7
|
+
isCancel,
|
|
8
|
+
log,
|
|
9
|
+
note,
|
|
10
|
+
outro,
|
|
11
|
+
select,
|
|
12
|
+
spinner,
|
|
13
|
+
} from "@clack/prompts";
|
|
5
14
|
import {
|
|
6
15
|
type AutoDecision,
|
|
7
16
|
type AutoMode,
|
|
@@ -328,8 +337,42 @@ function locationLabel(loc: {
|
|
|
328
337
|
entryDir?: string;
|
|
329
338
|
configPath?: string;
|
|
330
339
|
}) {
|
|
331
|
-
|
|
332
|
-
|
|
340
|
+
return loc.entryDir ?? loc.configPath ?? "";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function locationHint(loc: {
|
|
344
|
+
sourceId?: string;
|
|
345
|
+
modified?: Date | null;
|
|
346
|
+
}): string {
|
|
347
|
+
const parts = [
|
|
348
|
+
loc.sourceId?.trim() || "unknown source",
|
|
349
|
+
`modified ${formatDate(loc.modified ?? null)}`,
|
|
350
|
+
];
|
|
351
|
+
return parts.join(" • ");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function pluralize(count: number, singular: string, plural = `${singular}s`) {
|
|
355
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function logConsolidateInfo(title: string, message: string) {
|
|
359
|
+
log.info(`${title}: ${message}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function logConsolidateStep(title: string, message: string) {
|
|
363
|
+
log.step(`${title}: ${message}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function logConsolidateSuccess(title: string, message: string) {
|
|
367
|
+
log.success(`${title}: ${message}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function logConsolidateWarn(title: string, message: string) {
|
|
371
|
+
log.warn(`${title}: ${message}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function logConsolidateError(title: string, message: string) {
|
|
375
|
+
log.error(`${title}: ${message}`);
|
|
333
376
|
}
|
|
334
377
|
|
|
335
378
|
function skillChoiceValue(loc: SkillLocation) {
|
|
@@ -365,10 +408,11 @@ function chooseByAutoMode<T extends { modified: Date | null }>(
|
|
|
365
408
|
|
|
366
409
|
async function promptViewSkillContents(locs: SkillLocation[]) {
|
|
367
410
|
const choice = await select({
|
|
368
|
-
message: "
|
|
411
|
+
message: "Which source do you want to preview?",
|
|
369
412
|
options: locs.map((loc) => ({
|
|
370
413
|
value: skillChoiceValue(loc),
|
|
371
414
|
label: locationLabel(loc),
|
|
415
|
+
hint: locationHint(loc),
|
|
372
416
|
})),
|
|
373
417
|
});
|
|
374
418
|
if (isCancel(choice)) {
|
|
@@ -392,13 +436,14 @@ async function promptViewMcpContents(
|
|
|
392
436
|
locs: { configPath: string; sourceId?: string }[]
|
|
393
437
|
) {
|
|
394
438
|
const choice = await select({
|
|
395
|
-
message: "
|
|
439
|
+
message: "Which MCP config do you want to preview?",
|
|
396
440
|
options: locs.map((loc) => ({
|
|
397
441
|
value: mcpChoiceValue({
|
|
398
442
|
sourceId: loc.sourceId ?? "",
|
|
399
443
|
configPath: loc.configPath,
|
|
400
444
|
}),
|
|
401
445
|
label: loc.configPath,
|
|
446
|
+
hint: locationHint(loc),
|
|
402
447
|
})),
|
|
403
448
|
});
|
|
404
449
|
if (isCancel(choice)) {
|
|
@@ -495,11 +540,14 @@ async function copySkillAndUpdateState({
|
|
|
495
540
|
target: dest,
|
|
496
541
|
consolidatedAt: new Date().toISOString(),
|
|
497
542
|
};
|
|
498
|
-
|
|
543
|
+
logConsolidateSuccess(`Skill ${name}`, `Copied to ${dest}`);
|
|
499
544
|
return true;
|
|
500
545
|
} catch (e: unknown) {
|
|
501
546
|
const err = e as { message?: string } | null;
|
|
502
|
-
|
|
547
|
+
logConsolidateError(
|
|
548
|
+
`Skill ${name}`,
|
|
549
|
+
`Copy failed: ${String(err?.message ?? e)}`
|
|
550
|
+
);
|
|
503
551
|
return false;
|
|
504
552
|
}
|
|
505
553
|
}
|
|
@@ -561,14 +609,17 @@ async function resolveSkillConflictAndCopy({
|
|
|
561
609
|
target: dest,
|
|
562
610
|
consolidatedAt: new Date().toISOString(),
|
|
563
611
|
};
|
|
564
|
-
|
|
612
|
+
logConsolidateStep(`Skill ${name}`, `Kept existing copy at ${dest}`);
|
|
565
613
|
return;
|
|
566
614
|
}
|
|
567
615
|
|
|
568
616
|
if (decision === "keep-incoming") {
|
|
569
617
|
const backup = await archiveExisting(dest);
|
|
570
618
|
if (backup) {
|
|
571
|
-
|
|
619
|
+
logConsolidateWarn(
|
|
620
|
+
`Skill ${name}`,
|
|
621
|
+
`Archived existing copy to ${backup}`
|
|
622
|
+
);
|
|
572
623
|
}
|
|
573
624
|
await copySkillAndUpdateState({
|
|
574
625
|
name,
|
|
@@ -602,7 +653,10 @@ async function resolveSkillConflictAndCopy({
|
|
|
602
653
|
consolidatedAt: new Date().toISOString(),
|
|
603
654
|
};
|
|
604
655
|
}
|
|
605
|
-
|
|
656
|
+
logConsolidateInfo(
|
|
657
|
+
`Skill ${name}`,
|
|
658
|
+
`Kept both copies as ${name} and ${newName}`
|
|
659
|
+
);
|
|
606
660
|
}
|
|
607
661
|
|
|
608
662
|
async function handleSingleSkillLocation({
|
|
@@ -622,7 +676,9 @@ async function handleSingleSkillLocation({
|
|
|
622
676
|
}): Promise<void> {
|
|
623
677
|
if (!autoMode) {
|
|
624
678
|
const ok = await confirm({
|
|
625
|
-
message: `
|
|
679
|
+
message: `Add skill "${name}" from this source?`,
|
|
680
|
+
active: "Add",
|
|
681
|
+
inactive: "Skip",
|
|
626
682
|
});
|
|
627
683
|
if (isCancel(ok) || !ok) {
|
|
628
684
|
return;
|
|
@@ -673,14 +729,15 @@ async function handleMultipleSkillLocations({
|
|
|
673
729
|
|
|
674
730
|
while (true) {
|
|
675
731
|
const selection = await select({
|
|
676
|
-
message: `Choose source for skill "${name}"`,
|
|
732
|
+
message: `Choose a source for skill "${name}"`,
|
|
677
733
|
options: [
|
|
678
734
|
...locs.map((loc) => ({
|
|
679
735
|
value: skillChoiceValue(loc),
|
|
680
736
|
label: locationLabel(loc),
|
|
737
|
+
hint: locationHint(loc),
|
|
681
738
|
})),
|
|
682
|
-
{ value: "view", label: "
|
|
683
|
-
{ value: "skip", label: "Skip" },
|
|
739
|
+
{ value: "view", label: "Preview SKILL.md", hint: "Open source text" },
|
|
740
|
+
{ value: "skip", label: "Skip", hint: "Leave this skill alone" },
|
|
684
741
|
],
|
|
685
742
|
});
|
|
686
743
|
if (isCancel(selection) || selection === "skip") {
|
|
@@ -717,15 +774,15 @@ async function consolidateSkills(
|
|
|
717
774
|
const skillMap = await buildSkillLocations(res);
|
|
718
775
|
const skillNames = [...skillMap.keys()].sort();
|
|
719
776
|
if (!skillNames.length) {
|
|
720
|
-
|
|
777
|
+
logConsolidateInfo("Skills", "No skills found to consolidate.");
|
|
721
778
|
return;
|
|
722
779
|
}
|
|
723
780
|
|
|
724
781
|
for (const name of skillNames) {
|
|
725
782
|
if (state.skills[name] && !force) {
|
|
726
|
-
|
|
727
|
-
`
|
|
728
|
-
`
|
|
783
|
+
logConsolidateStep(
|
|
784
|
+
`Skill ${name}`,
|
|
785
|
+
`Already consolidated from ${state.skills[name].source}`
|
|
729
786
|
);
|
|
730
787
|
continue;
|
|
731
788
|
}
|
|
@@ -979,7 +1036,10 @@ async function mergeServerAndSave({
|
|
|
979
1036
|
consolidatedPath,
|
|
980
1037
|
`${JSON.stringify(consolidatedObj, null, 2)}\n`
|
|
981
1038
|
);
|
|
982
|
-
|
|
1039
|
+
logConsolidateSuccess(
|
|
1040
|
+
`MCP server ${serverName}`,
|
|
1041
|
+
`Merged into ${consolidatedPath}`
|
|
1042
|
+
);
|
|
983
1043
|
}
|
|
984
1044
|
|
|
985
1045
|
async function resolveMcpServerConflictAndMerge({
|
|
@@ -1050,9 +1110,9 @@ async function resolveMcpServerConflictAndMerge({
|
|
|
1050
1110
|
consolidatedAt: nowIso(),
|
|
1051
1111
|
};
|
|
1052
1112
|
}
|
|
1053
|
-
|
|
1054
|
-
`
|
|
1055
|
-
`
|
|
1113
|
+
logConsolidateStep(
|
|
1114
|
+
`MCP server ${serverName}`,
|
|
1115
|
+
`Kept existing definition in ${consolidatedPath}`
|
|
1056
1116
|
);
|
|
1057
1117
|
return;
|
|
1058
1118
|
}
|
|
@@ -1060,7 +1120,10 @@ async function resolveMcpServerConflictAndMerge({
|
|
|
1060
1120
|
if (decision === "keep-incoming") {
|
|
1061
1121
|
const backup = await archiveExisting(consolidatedPath);
|
|
1062
1122
|
if (backup) {
|
|
1063
|
-
|
|
1123
|
+
logConsolidateWarn(
|
|
1124
|
+
"MCP registry",
|
|
1125
|
+
`Archived existing registry to ${backup}`
|
|
1126
|
+
);
|
|
1064
1127
|
}
|
|
1065
1128
|
consolidatedObj.updatedAt = nowIso();
|
|
1066
1129
|
consolidatedObj.mcpServers[serverName] = incomingCanonical;
|
|
@@ -1073,7 +1136,10 @@ async function resolveMcpServerConflictAndMerge({
|
|
|
1073
1136
|
consolidatedPath,
|
|
1074
1137
|
`${JSON.stringify(consolidatedObj, null, 2)}\n`
|
|
1075
1138
|
);
|
|
1076
|
-
|
|
1139
|
+
logConsolidateSuccess(
|
|
1140
|
+
`MCP server ${serverName}`,
|
|
1141
|
+
`Merged into ${consolidatedPath}`
|
|
1142
|
+
);
|
|
1077
1143
|
return;
|
|
1078
1144
|
}
|
|
1079
1145
|
|
|
@@ -1107,7 +1173,10 @@ async function resolveMcpServerConflictAndMerge({
|
|
|
1107
1173
|
consolidatedPath,
|
|
1108
1174
|
`${JSON.stringify(consolidatedObj, null, 2)}\n`
|
|
1109
1175
|
);
|
|
1110
|
-
|
|
1176
|
+
logConsolidateInfo(
|
|
1177
|
+
`MCP server ${serverName}`,
|
|
1178
|
+
`Kept both definitions as ${serverName} and ${newName}`
|
|
1179
|
+
);
|
|
1111
1180
|
}
|
|
1112
1181
|
|
|
1113
1182
|
async function handleSingleMcpServerLocation({
|
|
@@ -1127,7 +1196,9 @@ async function handleSingleMcpServerLocation({
|
|
|
1127
1196
|
}): Promise<void> {
|
|
1128
1197
|
if (!autoMode) {
|
|
1129
1198
|
const ok = await confirm({
|
|
1130
|
-
message: `Add MCP server "${serverName}" from
|
|
1199
|
+
message: `Add MCP server "${serverName}" from this config?`,
|
|
1200
|
+
active: "Add",
|
|
1201
|
+
inactive: "Skip",
|
|
1131
1202
|
});
|
|
1132
1203
|
if (isCancel(ok) || !ok) {
|
|
1133
1204
|
return;
|
|
@@ -1176,14 +1247,15 @@ async function handleMultipleMcpServerLocations({
|
|
|
1176
1247
|
|
|
1177
1248
|
while (true) {
|
|
1178
1249
|
const selection = await select({
|
|
1179
|
-
message: `Choose source for MCP server "${serverName}"`,
|
|
1250
|
+
message: `Choose a source for MCP server "${serverName}"`,
|
|
1180
1251
|
options: [
|
|
1181
1252
|
...locs.map((loc) => ({
|
|
1182
1253
|
value: mcpChoiceValue(loc),
|
|
1183
1254
|
label: locationLabel(loc),
|
|
1255
|
+
hint: locationHint(loc),
|
|
1184
1256
|
})),
|
|
1185
|
-
{ value: "view", label: "
|
|
1186
|
-
{ value: "skip", label: "Skip" },
|
|
1257
|
+
{ value: "view", label: "Preview MCP JSON", hint: "Open source text" },
|
|
1258
|
+
{ value: "skip", label: "Skip", hint: "Leave this server alone" },
|
|
1187
1259
|
],
|
|
1188
1260
|
});
|
|
1189
1261
|
if (isCancel(selection) || selection === "skip") {
|
|
@@ -1232,12 +1304,15 @@ async function resolveMcpConfigConflictAndCopy({
|
|
|
1232
1304
|
target: dest,
|
|
1233
1305
|
consolidatedAt: new Date().toISOString(),
|
|
1234
1306
|
};
|
|
1235
|
-
|
|
1307
|
+
logConsolidateSuccess(
|
|
1308
|
+
`MCP config ${basename(config.configPath)}`,
|
|
1309
|
+
`Copied to ${dest}`
|
|
1310
|
+
);
|
|
1236
1311
|
} catch (e: unknown) {
|
|
1237
1312
|
const err = e as { message?: string } | null;
|
|
1238
|
-
|
|
1239
|
-
`
|
|
1240
|
-
`
|
|
1313
|
+
logConsolidateError(
|
|
1314
|
+
`MCP config ${basename(config.configPath)}`,
|
|
1315
|
+
`Copy failed: ${String(err?.message ?? e)}`
|
|
1241
1316
|
);
|
|
1242
1317
|
}
|
|
1243
1318
|
return;
|
|
@@ -1272,9 +1347,9 @@ async function resolveMcpConfigConflictAndCopy({
|
|
|
1272
1347
|
target: dest,
|
|
1273
1348
|
consolidatedAt: new Date().toISOString(),
|
|
1274
1349
|
};
|
|
1275
|
-
|
|
1276
|
-
`
|
|
1277
|
-
`
|
|
1350
|
+
logConsolidateStep(
|
|
1351
|
+
`MCP config ${basename(dest)}`,
|
|
1352
|
+
`Kept existing copy at ${dest}`
|
|
1278
1353
|
);
|
|
1279
1354
|
return;
|
|
1280
1355
|
}
|
|
@@ -1282,7 +1357,7 @@ async function resolveMcpConfigConflictAndCopy({
|
|
|
1282
1357
|
if (decision === "keep-incoming") {
|
|
1283
1358
|
const backup = await archiveExisting(dest);
|
|
1284
1359
|
if (backup) {
|
|
1285
|
-
|
|
1360
|
+
logConsolidateWarn("MCP config", `Archived existing config to ${backup}`);
|
|
1286
1361
|
}
|
|
1287
1362
|
try {
|
|
1288
1363
|
await Bun.write(dest, Bun.file(config.configPath));
|
|
@@ -1291,12 +1366,15 @@ async function resolveMcpConfigConflictAndCopy({
|
|
|
1291
1366
|
target: dest,
|
|
1292
1367
|
consolidatedAt: new Date().toISOString(),
|
|
1293
1368
|
};
|
|
1294
|
-
|
|
1369
|
+
logConsolidateSuccess(
|
|
1370
|
+
`MCP config ${basename(config.configPath)}`,
|
|
1371
|
+
`Copied to ${dest}`
|
|
1372
|
+
);
|
|
1295
1373
|
} catch (e: unknown) {
|
|
1296
1374
|
const err = e as { message?: string } | null;
|
|
1297
|
-
|
|
1298
|
-
`
|
|
1299
|
-
`
|
|
1375
|
+
logConsolidateError(
|
|
1376
|
+
`MCP config ${basename(config.configPath)}`,
|
|
1377
|
+
`Copy failed: ${String(err?.message ?? e)}`
|
|
1300
1378
|
);
|
|
1301
1379
|
}
|
|
1302
1380
|
return;
|
|
@@ -1319,12 +1397,12 @@ async function resolveMcpConfigConflictAndCopy({
|
|
|
1319
1397
|
target: newDest,
|
|
1320
1398
|
consolidatedAt: new Date().toISOString(),
|
|
1321
1399
|
};
|
|
1322
|
-
|
|
1400
|
+
logConsolidateSuccess(`MCP config ${newName}`, `Copied to ${newDest}`);
|
|
1323
1401
|
} catch (e: unknown) {
|
|
1324
1402
|
const err = e as { message?: string } | null;
|
|
1325
|
-
|
|
1326
|
-
`
|
|
1327
|
-
`
|
|
1403
|
+
logConsolidateError(
|
|
1404
|
+
`MCP config ${basename(config.configPath)}`,
|
|
1405
|
+
`Copy failed: ${String(err?.message ?? e)}`
|
|
1328
1406
|
);
|
|
1329
1407
|
}
|
|
1330
1408
|
}
|
|
@@ -1342,16 +1420,18 @@ async function consolidateMcpConfigFiles(
|
|
|
1342
1420
|
for (const config of sorted) {
|
|
1343
1421
|
const key = config.configPath;
|
|
1344
1422
|
if (state.mcpConfigs[key] && !force) {
|
|
1345
|
-
|
|
1346
|
-
`
|
|
1347
|
-
`
|
|
1423
|
+
logConsolidateStep(
|
|
1424
|
+
`MCP config ${basename(config.configPath)}`,
|
|
1425
|
+
`Already consolidated from ${state.mcpConfigs[key].source}`
|
|
1348
1426
|
);
|
|
1349
1427
|
continue;
|
|
1350
1428
|
}
|
|
1351
1429
|
const dest = join(mcpDir, basename(config.configPath));
|
|
1352
1430
|
if (!autoMode) {
|
|
1353
1431
|
const ok = await confirm({
|
|
1354
|
-
message: `
|
|
1432
|
+
message: `Add MCP config "${basename(config.configPath)}" from this source?`,
|
|
1433
|
+
active: "Add",
|
|
1434
|
+
inactive: "Skip",
|
|
1355
1435
|
});
|
|
1356
1436
|
if (isCancel(ok) || !ok) {
|
|
1357
1437
|
continue;
|
|
@@ -1382,9 +1462,9 @@ async function consolidateMcpServers(
|
|
|
1382
1462
|
|
|
1383
1463
|
for (const serverName of serverNames) {
|
|
1384
1464
|
if (state.mcpServers[serverName] && !force) {
|
|
1385
|
-
|
|
1386
|
-
`
|
|
1387
|
-
`
|
|
1465
|
+
logConsolidateStep(
|
|
1466
|
+
`MCP server ${serverName}`,
|
|
1467
|
+
`Already consolidated from ${state.mcpServers[serverName].source}`
|
|
1388
1468
|
);
|
|
1389
1469
|
continue;
|
|
1390
1470
|
}
|
|
@@ -1598,12 +1678,30 @@ Options:
|
|
|
1598
1678
|
const home = ctx.homeDir ?? homedir();
|
|
1599
1679
|
const rootDir = ctx.rootDir ?? facultRootDir(home);
|
|
1600
1680
|
intro("fclt consolidate");
|
|
1681
|
+
log.step(
|
|
1682
|
+
"Bring discovered skills and MCP configs into the canonical store."
|
|
1683
|
+
);
|
|
1684
|
+
if (autoMode) {
|
|
1685
|
+
log.info(
|
|
1686
|
+
`Auto mode: ${autoMode}. Conflicts will be resolved without prompts.`
|
|
1687
|
+
);
|
|
1688
|
+
} else {
|
|
1689
|
+
log.info(
|
|
1690
|
+
"Interactive mode: review each source and resolve conflicts as they appear."
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
if (scanOptions.from.length > 0) {
|
|
1694
|
+
log.info(`Extra scan roots: ${scanOptions.from.join(", ")}`);
|
|
1695
|
+
}
|
|
1601
1696
|
|
|
1697
|
+
const scanSpinner = spinner();
|
|
1698
|
+
scanSpinner.start("Scanning candidate sources...");
|
|
1602
1699
|
const res = await scan([], {
|
|
1603
1700
|
...scanOptions,
|
|
1604
1701
|
homeDir: home,
|
|
1605
1702
|
cwd: ctx.cwd,
|
|
1606
1703
|
});
|
|
1704
|
+
scanSpinner.stop(`Found ${pluralize(res.sources.length, "source")}.`);
|
|
1607
1705
|
const state = await loadState(home);
|
|
1608
1706
|
|
|
1609
1707
|
const targets = {
|
|
@@ -1632,9 +1730,7 @@ Options:
|
|
|
1632
1730
|
);
|
|
1633
1731
|
|
|
1634
1732
|
await saveState(home, state);
|
|
1635
|
-
outro(
|
|
1636
|
-
`Consolidation complete. State saved to ${join(facultStateDir(home), "consolidated.json")}`
|
|
1637
|
-
);
|
|
1733
|
+
outro(`State saved to ${join(facultStateDir(home), "consolidated.json")}`);
|
|
1638
1734
|
} catch (err) {
|
|
1639
1735
|
console.error(err instanceof Error ? err.message : String(err));
|
|
1640
1736
|
process.exitCode = 1;
|