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.
@@ -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 { confirm, intro, isCancel, note, outro, select } from "@clack/prompts";
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
- const p = loc.entryDir ?? loc.configPath ?? "";
332
- return `${p} (${loc.sourceId}, modified ${formatDate(loc.modified)})`;
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: "View SKILL.md from which location?",
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: "View MCP JSON from which config?",
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
- note(`Copied to ${dest}`, `Skill: ${name}`);
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
- note(`Copy failed: ${String(err?.message ?? e)}`, `Skill: ${name}`);
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
- note(`Kept existing skill at ${dest}`, `Skill: ${name}`);
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
- note(`Archived existing skill to ${backup}`, `Skill: ${name}`);
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
- note(`Kept both skills: ${name} + ${newName}`, `Skill: ${name}`);
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: `Copy ${name} from ${loc.entryDir} (modified ${formatDate(loc.modified)})?`,
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: "View SKILL.md" },
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
- note("No skills found to consolidate.", "Skills");
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
- note(
727
- `Already consolidated from ${state.skills[name].source} → ${state.skills[name].target}`,
728
- `Skill: ${name}`
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
- note(`Merged into ${consolidatedPath}`, `MCP server: ${serverName}`);
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
- note(
1054
- `Kept existing MCP server in ${consolidatedPath}`,
1055
- `MCP server: ${serverName}`
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
- note(`Archived existing MCP registry to ${backup}`, "MCP registry");
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
- note(`Merged into ${consolidatedPath}`, `MCP server: ${serverName}`);
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
- note(`Kept both servers: ${serverName} + ${newName}`, "MCP server");
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 ${loc.configPath} (modified ${formatDate(loc.modified)})?`,
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: "View MCP JSON" },
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
- note(`Copied to ${dest}`, `MCP config: ${basename(config.configPath)}`);
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
- note(
1239
- `Copy failed: ${String(err?.message ?? e)}`,
1240
- `MCP config: ${config.configPath}`
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
- note(
1276
- `Kept existing MCP config at ${dest}`,
1277
- `MCP config: ${basename(dest)}`
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
- note(`Archived existing MCP config to ${backup}`, "MCP config");
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
- note(`Copied to ${dest}`, `MCP config: ${basename(config.configPath)}`);
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
- note(
1298
- `Copy failed: ${String(err?.message ?? e)}`,
1299
- `MCP config: ${config.configPath}`
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
- note(`Copied to ${newDest}`, `MCP config: ${newName}`);
1400
+ logConsolidateSuccess(`MCP config ${newName}`, `Copied to ${newDest}`);
1323
1401
  } catch (e: unknown) {
1324
1402
  const err = e as { message?: string } | null;
1325
- note(
1326
- `Copy failed: ${String(err?.message ?? e)}`,
1327
- `MCP config: ${config.configPath}`
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
- note(
1346
- `Already consolidated from ${state.mcpConfigs[key].source}`,
1347
- `MCP config: ${key}`
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: `Copy MCP config ${config.configPath} (modified ${formatDate(config.modified)})?`,
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
- note(
1386
- `Already consolidated from ${state.mcpServers[serverName].source}`,
1387
- `MCP server: ${serverName}`
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;