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/src/index.ts CHANGED
@@ -1,18 +1,21 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { join } from "node:path";
4
- import { getAllAdapters } from "./adapters";
5
- import { aiCommand } from "./ai";
6
- import { auditCommand } from "./audit";
7
- import { autosyncCommand } from "./autosync";
8
4
  import {
9
5
  type CapabilityScopeMode,
10
6
  parseCliContextArgs,
11
7
  resolveCliContextRoot,
12
8
  } from "./cli-context";
13
- import { consolidateCommand } from "./consolidate";
14
- import { doctorCommand } from "./doctor";
15
- import { disableCommand, enableCommand } from "./enable-disable";
9
+ import {
10
+ renderBadge,
11
+ renderBullets,
12
+ renderCatalog,
13
+ renderCode,
14
+ renderJsonBlock,
15
+ renderKeyValue,
16
+ renderPage,
17
+ renderTable,
18
+ } from "./cli-ui";
16
19
  import type { AssetScope, AssetSourceKind } from "./graph";
17
20
  import {
18
21
  graphDependencies,
@@ -28,14 +31,6 @@ import type {
28
31
  SkillEntry,
29
32
  SnippetEntry,
30
33
  } from "./index-builder";
31
- import { indexCommand } from "./index-builder";
32
- import {
33
- manageCommand,
34
- managedCommand,
35
- syncCommand,
36
- unmanageCommand,
37
- } from "./manage";
38
- import { migrateCommand } from "./migrate";
39
34
  import type { QueryFilters } from "./query";
40
35
  import {
41
36
  filterAgents,
@@ -46,18 +41,6 @@ import {
46
41
  findCapabilities,
47
42
  loadIndex,
48
43
  } from "./query";
49
- import {
50
- installCommand,
51
- searchCommand,
52
- sourcesCommand,
53
- templatesCommand,
54
- updateCommand,
55
- verifySourceCommand,
56
- } from "./remote";
57
- import { scanCommand } from "./scan";
58
- import { selfUpdateCommand } from "./self-update";
59
- import { snippetsCommand } from "./snippets-cli";
60
- import { trustCommand, untrustCommand } from "./trust";
61
44
  import { parseJsonLenient } from "./util/json";
62
45
 
63
46
  type ListKind = "skills" | "mcp" | "agents" | "snippets" | "instructions";
@@ -96,182 +79,232 @@ interface GraphCommandOptions {
96
79
  }
97
80
 
98
81
  function printHelp() {
99
- console.log(`fclt — manage canonical AI capabilities, sync surfaces, and evolution state
100
-
101
- Usage:
102
- fclt scan [--json] [--show-duplicates] [--tui] [--from <path>]
103
- fclt audit [--from <path>]
104
- fclt audit --non-interactive [name|mcp:<name>] [--severity <level>] [--rules <path>] [--from <path>] [--json]
105
- fclt audit --non-interactive [name|mcp:<name>] --with <claude|codex> [--from <path>] [--max-items <n|all>] [--json]
106
- fclt migrate [--from <path>] [--dry-run] [--move] [--write-config]
107
- fclt doctor [--repair]
108
- fclt consolidate [--force] [--auto <mode>] [scan options]
109
- fclt index [--force]
110
- fclt list [skills|mcp|agents|snippets|instructions] [--enabled-for TOOL] [--untrusted] [--flagged] [--pending] [--json]
111
- fclt show <name>
112
- fclt show mcp:<name> [--show-secrets]
113
- fclt show instruction:<name>
114
- fclt find <query> [--json]
115
- fclt graph <show|deps|dependents> <asset> [--json]
116
- fclt ai <writeback|evolve> [args...]
117
- fclt adapters
118
- fclt trust <name> [moreNames...]
119
- fclt untrust <name> [moreNames...]
120
- fclt manage <tool>
121
- fclt unmanage <tool>
122
- fclt managed
123
- fclt enable <name> [moreNames...] [--for <tools>]
124
- fclt disable <name> [moreNames...] [--for <tools>]
125
- fclt sync [tool] [--dry-run]
126
- fclt autosync <cmd> [args...]
127
- fclt search <query> [--index <name>] [--limit <n>]
128
- fclt install <index:item> [--as <name>] [--dry-run] [--force] [--strict-source-trust]
129
- fclt update [--apply] [--strict-source-trust]
130
- fclt update --self [--version <x.y.z|latest>] [--dry-run]
131
- fclt self-update [--version <x.y.z|latest>] [--dry-run]
132
- fclt verify-source <name> [--json]
133
- fclt sources <cmd> [args...]
134
- fclt templates <cmd> [args...]
135
- fclt snippets <cmd> [args...]
136
- fclt --show-duplicates
137
-
138
- Commands:
139
- scan Scan common config locations (Cursor, Claude, Claude Desktop, etc.)
140
- audit Security audits (interactive by default; use --non-interactive for scripts)
141
- migrate Copy/move a legacy canonical store to the current canonical root
142
- doctor Inspect and repair local fclt state
143
- consolidate Deduplicate and copy skills + MCP configs (interactive or --auto)
144
- index Build a queryable index from the canonical store (see FACULT_ROOT_DIR)
145
- list List indexed skills, MCP servers, agents, snippets, or instructions
146
- show Show a single indexed entry, including file contents
147
- find Search local indexed capabilities across asset types
148
- graph Inspect explicit capability graph nodes and dependencies
149
- ai Record, group, and evolve AI writeback into reviewable proposals
150
- adapters List registered tool adapters
151
- trust Mark a skill or MCP server as trusted (annotation only)
152
- untrust Remove trusted annotation
153
- manage Back up tool config and enter managed mode
154
- unmanage Restore backups and exit managed mode
155
- managed List tools in managed mode
156
- enable Enable skills or MCP servers for tools
157
- disable Disable skills or MCP servers for tools
158
- sync Sync managed tools with canonical configs
159
- autosync Install/manage a background autosync service
160
- search Search remote indices (builtin + provider aliases + configured)
161
- install Install an item from a remote index
162
- update Check/apply updates for remotely installed items
163
- self-update Update fclt itself based on install method
164
- verify-source Verify source trust and manifest integrity/signature status
165
- sources Manage source trust policy for remote indices
166
- templates Scaffold DX-first templates (skills/instructions/MCP/snippets/automations)
167
- snippets Sync reusable snippet blocks into config files
168
-
169
- Options:
170
- --json Print full JSON (ScanResult or list output)
171
- --show-duplicates Print duplicates for skills, MCP servers, and hook assets
172
- --tui Render scan output in an interactive TUI (skills list)
173
- --from Add one or more additional scan roots (repeatable): --from ~/dev
174
- --from-ignore (scan) Ignore directories by basename under --from roots (repeatable)
175
- --from-no-default-ignore (scan) Disable the default ignore list for --from scans
176
- --from-max-visits (scan) Max directories visited per --from root before truncating
177
- --from-max-results (scan) Max discovered paths per --from root before truncating
178
- --non-interactive (audit) Run static/agent audit non-interactively (for scripts)
179
- --severity Minimum severity to include in audit output (low|medium|high|critical)
180
- --rules Path to an audit rules YAML file (default: ~/.ai/.facult/audit-rules.yaml)
181
- --with (audit) Agent tool: claude|codex
182
- --max-items (audit) Max items to send to the agent (n|all)
183
- --force Re-copy items already consolidated OR rebuild index from scratch
184
- --auto Auto-resolve consolidate conflicts: keep-newest, keep-current, keep-incoming
185
- --enabled-for Filter list to entries enabled for a specific tool
186
- --untrusted Filter list to entries that are not trusted
187
- --flagged Filter list to entries flagged by audit
188
- --pending Filter list to entries pending audit
189
- --for Comma-separated list of tools for enable/disable
190
- --dry-run Show what sync would change
191
- --as Install/scaffold target name override
192
- --limit Max results for search
193
- --apply Apply updates (update command)
194
- --self (update) run self-update flow instead of remote item updates
195
- --strict-source-trust Enforce trust-only remote install/update actions
196
- --show-secrets (show) Print raw secret values (unsafe)
197
- --root Select a canonical .ai root explicitly
198
- --global Force the global canonical root
199
- --project Force the nearest repo-local .ai root
200
- --scope Capability view scope: merged|global|project
201
- --source Filter to builtin|global|project asset provenance
202
- `);
82
+ console.log(
83
+ renderPage({
84
+ title: "fclt",
85
+ subtitle:
86
+ "Manage canonical AI capability, rendered tool surfaces, and evolution state.",
87
+ sections: [
88
+ {
89
+ title: "Usage",
90
+ lines: renderBullets([
91
+ `${renderCode("fclt list")} defaults to ${renderCode("skills")} when you do not specify a type.`,
92
+ `${renderCode("fclt graph <asset>")} is shorthand for ${renderCode("fclt graph show <asset>")}.`,
93
+ `${renderCode("fclt templates init ...")} is the main entry for scaffolding new canonical capability.`,
94
+ ]),
95
+ },
96
+ {
97
+ title: "Core Commands",
98
+ lines: renderTable({
99
+ headers: ["Command", "Purpose"],
100
+ rows: [
101
+ ["scan", "Scan local tool configs and discovered assets"],
102
+ [
103
+ "audit",
104
+ "Run security audits with interactive or scripted flows",
105
+ ],
106
+ [
107
+ "consolidate",
108
+ "Import existing skills and MCP configs into canonical state",
109
+ ],
110
+ ["index", "Rebuild the generated capability index"],
111
+ [
112
+ "list",
113
+ "List indexed skills, MCP, agents, snippets, or instructions",
114
+ ],
115
+ ["show", "Inspect one indexed asset and its source contents"],
116
+ ["find", "Search indexed capability across asset types"],
117
+ ["graph", "Inspect capability graph nodes, deps, and dependents"],
118
+ [
119
+ "templates",
120
+ "Scaffold skills, MCP, agents, snippets, and automations",
121
+ ],
122
+ ["search/install/update", "Work with remote capability indices"],
123
+ [
124
+ "manage/sync",
125
+ "Enter managed mode and render tool-native output",
126
+ ],
127
+ ["ai", "Capture writeback and evolve canonical assets"],
128
+ ],
129
+ }),
130
+ },
131
+ {
132
+ title: "Common Options",
133
+ lines: renderTable({
134
+ headers: ["Option", "Meaning"],
135
+ rows: [
136
+ [
137
+ "--json",
138
+ "Machine-readable output instead of formatted terminal UI",
139
+ ],
140
+ ["--dry-run", "Show intended writes without mutating files"],
141
+ [
142
+ "--root / --global / --project",
143
+ "Pick the canonical root explicitly",
144
+ ],
145
+ [
146
+ "--scope / --source",
147
+ "Narrow merged views by scope or provenance",
148
+ ],
149
+ [
150
+ "--non-interactive / --yes",
151
+ "Suppress prompts where the command supports inferred defaults",
152
+ ],
153
+ ],
154
+ }),
155
+ },
156
+ {
157
+ title: "Examples",
158
+ lines: renderBullets([
159
+ renderCode("fclt list"),
160
+ renderCode("fclt graph skills:capability-evolution"),
161
+ renderCode("fclt templates init skill review-checklist"),
162
+ renderCode("fclt templates init agent writeback-curator"),
163
+ ]),
164
+ },
165
+ ],
166
+ })
167
+ );
203
168
  }
204
169
 
205
170
  function printListHelp() {
206
- console.log(`fclt list — list indexed entries from the canonical store
207
-
208
- Usage:
209
- fclt list [skills|mcp|agents|snippets|instructions] [options]
210
-
211
- Options:
212
- --enabled-for TOOL Only include entries enabled for a tool
213
- --untrusted Only include entries that are not trusted
214
- --flagged Only include entries flagged by audit
215
- --pending Only include entries pending audit
216
- --root PATH Select a canonical .ai root explicitly
217
- --global Force the global canonical root
218
- --project Force the nearest repo-local .ai root
219
- --scope SCOPE merged|global|project (default: merged)
220
- --source KIND builtin|global|project
221
- --json Print JSON array
222
- `);
171
+ console.log(
172
+ renderPage({
173
+ title: "fclt list",
174
+ subtitle: "List indexed entries from the canonical store.",
175
+ sections: [
176
+ {
177
+ title: "Usage",
178
+ lines: renderBullets([
179
+ renderCode(
180
+ "fclt list [skills|mcp|agents|snippets|instructions] [options]"
181
+ ),
182
+ renderCode("fclt list"),
183
+ ]),
184
+ },
185
+ {
186
+ title: "Options",
187
+ lines: renderTable({
188
+ headers: ["Option", "Meaning"],
189
+ rows: [
190
+ [
191
+ "--enabled-for TOOL",
192
+ "Only include entries enabled for one tool",
193
+ ],
194
+ ["--untrusted", "Only include entries without trust approval"],
195
+ ["--flagged", "Only include entries flagged by audit"],
196
+ ["--pending", "Only include entries still pending audit"],
197
+ ["--root / --global / --project", "Choose the canonical root"],
198
+ ["--scope", "merged, global, or project"],
199
+ ["--source", "builtin, global, or project provenance"],
200
+ ["--json", "Print the raw JSON array"],
201
+ ],
202
+ }),
203
+ },
204
+ ],
205
+ })
206
+ );
223
207
  }
224
208
 
225
209
  function printShowHelp() {
226
- console.log(`fclt show — show a single indexed entry (and file contents)
227
-
228
- Usage:
229
- fclt show <name>
230
- fclt show mcp:<name> [--show-secrets]
231
- fclt show instruction:<name>
232
-
233
- Options:
234
- --show-secrets (mcp) Print raw secret values (unsafe)
235
- --root PATH Select a canonical .ai root explicitly
236
- --global Force the global canonical root
237
- --project Force the nearest repo-local .ai root
238
- --scope SCOPE merged|global|project (default: merged)
239
- --source KIND builtin|global|project
240
- `);
210
+ console.log(
211
+ renderPage({
212
+ title: "fclt show",
213
+ subtitle: "Inspect one indexed entry and the source file behind it.",
214
+ sections: [
215
+ {
216
+ title: "Usage",
217
+ lines: renderBullets([
218
+ renderCode("fclt show <name>"),
219
+ renderCode("fclt show mcp:<name> [--show-secrets]"),
220
+ renderCode("fclt show instruction:<name>"),
221
+ ]),
222
+ },
223
+ {
224
+ title: "Options",
225
+ lines: renderTable({
226
+ headers: ["Option", "Meaning"],
227
+ rows: [
228
+ [
229
+ "--show-secrets",
230
+ "For MCP configs, print raw secrets instead of redacting",
231
+ ],
232
+ ["--root / --global / --project", "Choose the canonical root"],
233
+ ["--scope", "merged, global, or project"],
234
+ ["--source", "builtin, global, or project provenance"],
235
+ ],
236
+ }),
237
+ },
238
+ ],
239
+ })
240
+ );
241
241
  }
242
242
 
243
243
  function printFindHelp() {
244
- console.log(`fclt find — search local indexed capabilities across asset types
245
-
246
- Usage:
247
- fclt find <query> [--json]
248
-
249
- Options:
250
- --root PATH Select a canonical .ai root explicitly
251
- --global Force the global canonical root
252
- --project Force the nearest repo-local .ai root
253
- --scope SCOPE merged|global|project (default: merged)
254
- --source KIND builtin|global|project
255
- --json Print JSON array
256
- `);
244
+ console.log(
245
+ renderPage({
246
+ title: "fclt find",
247
+ subtitle:
248
+ "Search indexed capability across skills, MCP, agents, snippets, and instructions.",
249
+ sections: [
250
+ {
251
+ title: "Usage",
252
+ lines: renderBullets([renderCode("fclt find <query> [--json]")]),
253
+ },
254
+ {
255
+ title: "Options",
256
+ lines: renderTable({
257
+ headers: ["Option", "Meaning"],
258
+ rows: [
259
+ ["--root / --global / --project", "Choose the canonical root"],
260
+ ["--scope", "merged, global, or project"],
261
+ ["--source", "builtin, global, or project provenance"],
262
+ ["--json", "Print the raw JSON array"],
263
+ ],
264
+ }),
265
+ },
266
+ ],
267
+ })
268
+ );
257
269
  }
258
270
 
259
271
  function printGraphHelp() {
260
- console.log(`fclt graph — inspect explicit capability graph nodes and relations
261
-
262
- Usage:
263
- fclt graph show <asset> [--json]
264
- fclt graph deps <asset> [--json]
265
- fclt graph dependents <asset> [--json]
266
-
267
- Options:
268
- --root PATH Select a canonical .ai root explicitly
269
- --global Force the global canonical root
270
- --project Force the nearest repo-local .ai root
271
- --scope SCOPE merged|global|project (default: merged)
272
- --source KIND builtin|global|project
273
- --json Print JSON
274
- `);
272
+ console.log(
273
+ renderPage({
274
+ title: "fclt graph",
275
+ subtitle: "Inspect explicit capability graph nodes and relations.",
276
+ sections: [
277
+ {
278
+ title: "Usage",
279
+ lines: renderBullets([
280
+ renderCode("fclt graph <asset> [--json]"),
281
+ renderCode("fclt graph show <asset> [--json]"),
282
+ renderCode("fclt graph deps <asset> [--json]"),
283
+ renderCode("fclt graph dependents <asset> [--json]"),
284
+ ]),
285
+ },
286
+ {
287
+ title: "Notes",
288
+ lines: renderBullets([
289
+ `${renderCode("fclt graph <asset>")} defaults to ${renderCode("show")}.`,
290
+ "Selectors can be canonical refs, names, or graph node ids.",
291
+ ]),
292
+ },
293
+ {
294
+ title: "Options",
295
+ lines: renderTable({
296
+ headers: ["Option", "Meaning"],
297
+ rows: [
298
+ ["--root / --global / --project", "Choose the canonical root"],
299
+ ["--scope", "merged, global, or project"],
300
+ ["--source", "builtin, global, or project provenance"],
301
+ ["--json", "Print raw graph JSON"],
302
+ ],
303
+ }),
304
+ },
305
+ ],
306
+ })
307
+ );
275
308
  }
276
309
 
277
310
  function parseListKind(argv: string[]): { kind: ListKind; startIndex: number } {
@@ -371,15 +404,16 @@ export function parseFindArgs(argv: string[]): FindCommandOptions {
371
404
  return { text, json };
372
405
  }
373
406
 
374
- function parseGraphArgs(argv: string[]): GraphCommandOptions {
375
- const [kind, ...rest] = argv;
376
- if (!(kind === "show" || kind === "deps" || kind === "dependents")) {
377
- throw new Error(`Unknown graph command: ${kind ?? "<missing>"}`);
378
- }
407
+ export function parseGraphArgs(argv: string[]): GraphCommandOptions {
408
+ const [first, ...rest] = argv;
409
+ const hasExplicitKind =
410
+ first === "show" || first === "deps" || first === "dependents";
411
+ const kind: GraphCommandKind = hasExplicitKind ? first : "show";
412
+ const args = hasExplicitKind ? rest : argv;
379
413
 
380
414
  let json = false;
381
415
  let target: string | null = null;
382
- for (const arg of rest) {
416
+ for (const arg of args) {
383
417
  if (!arg) {
384
418
  continue;
385
419
  }
@@ -409,6 +443,68 @@ function scopeFilterForMode(
409
443
  return scopeMode === "project" ? "project" : undefined;
410
444
  }
411
445
 
446
+ function sourceLabel(entry: { sourceKind?: string; scope?: string }): string {
447
+ const source = entry.sourceKind?.trim();
448
+ const scope = entry.scope?.trim();
449
+ if (source && scope) {
450
+ return `${source}/${scope}`;
451
+ }
452
+ return source ?? scope ?? "merged";
453
+ }
454
+
455
+ function describeFilters(filters: QueryFilters): string {
456
+ const parts: string[] = [];
457
+
458
+ if (filters.enabledFor) {
459
+ parts.push(`enabled for ${filters.enabledFor}`);
460
+ }
461
+ if (filters.untrusted) {
462
+ parts.push("untrusted only");
463
+ }
464
+ if (filters.flagged) {
465
+ parts.push("flagged only");
466
+ }
467
+ if (filters.pending) {
468
+ parts.push("pending only");
469
+ }
470
+ if (filters.sourceKind) {
471
+ parts.push(`source ${filters.sourceKind}`);
472
+ }
473
+ if (filters.scope) {
474
+ parts.push(`scope ${filters.scope}`);
475
+ }
476
+
477
+ return parts.join(" • ");
478
+ }
479
+
480
+ function trustBadge(trusted?: boolean): string {
481
+ return trusted
482
+ ? renderBadge("trusted", "success")
483
+ : renderBadge("untrusted", "warn");
484
+ }
485
+
486
+ function auditBadge(status?: string): string {
487
+ const normalized = (status ?? "pending").trim().toLowerCase();
488
+ if (normalized === "passed") {
489
+ return renderBadge("audit passed", "success");
490
+ }
491
+ if (normalized === "flagged") {
492
+ return renderBadge("audit flagged", "danger");
493
+ }
494
+ return renderBadge("audit pending", "warn");
495
+ }
496
+
497
+ function displayDescription(value?: string): string {
498
+ const normalized = value
499
+ ?.trim()
500
+ .replaceAll('\\"', '"')
501
+ .replace(INLINE_NAME_DESCRIPTION_RE, "");
502
+ if (!normalized || normalized === ">") {
503
+ return "No description.";
504
+ }
505
+ return normalized;
506
+ }
507
+
412
508
  function resolveContextualOptions(
413
509
  argv: string[],
414
510
  opts?: { allowSource?: boolean }
@@ -504,33 +600,70 @@ async function listCommand(argv: string[]) {
504
600
  return;
505
601
  }
506
602
 
507
- for (const entry of entries) {
603
+ if (entries.length === 0) {
604
+ console.log(
605
+ renderPage({
606
+ title: `fclt list ${opts.kind}`,
607
+ subtitle: "No matching entries.",
608
+ sections: [
609
+ {
610
+ title: "Next Steps",
611
+ lines: renderBullets([
612
+ renderCode("fclt index --force"),
613
+ renderCode("fclt templates list"),
614
+ ]),
615
+ },
616
+ ],
617
+ footer: describeFilters(opts.filters)
618
+ ? [describeFilters(opts.filters)]
619
+ : undefined,
620
+ })
621
+ );
622
+ return;
623
+ }
624
+
625
+ const items = entries.map((entry) => {
508
626
  if (opts.kind === "skills") {
509
627
  const skill = entry as SkillEntry;
510
- const desc = skill.description ? `\t${skill.description}` : "";
511
- const meta = skill as SkillEntry & {
512
- trusted?: boolean;
513
- auditStatus?: string;
628
+ return {
629
+ title: skill.name,
630
+ meta: sourceLabel(skill),
631
+ badges: [trustBadge(skill.trusted), auditBadge(skill.auditStatus)],
632
+ description: displayDescription(skill.description),
514
633
  };
515
- const trustedLabel = meta.trusted === true ? "trusted" : "untrusted";
516
- const auditLabel = (meta.auditStatus ?? "pending").trim().toLowerCase();
517
- console.log(
518
- `${skill.name}${desc}\t[${trustedLabel}; audit=${auditLabel}]${formatSourceMeta(skill)}`
519
- );
520
- } else if (opts.kind === "mcp") {
521
- const meta = entry as McpEntry & {
522
- trusted?: boolean;
523
- auditStatus?: string;
634
+ }
635
+
636
+ if (opts.kind === "mcp") {
637
+ const server = entry as McpEntry;
638
+ return {
639
+ title: server.name,
640
+ meta: sourceLabel(server),
641
+ badges: [trustBadge(server.trusted), auditBadge(server.auditStatus)],
642
+ description:
643
+ Array.isArray(server.enabledFor) && server.enabledFor.length > 0
644
+ ? `Enabled for ${server.enabledFor.join(", ")}.`
645
+ : "No enabled-for restrictions recorded.",
524
646
  };
525
- const trustedLabel = meta.trusted === true ? "trusted" : "untrusted";
526
- const auditLabel = (meta.auditStatus ?? "pending").trim().toLowerCase();
527
- console.log(
528
- `${entry.name}\t[${trustedLabel}; audit=${auditLabel}]${formatSourceMeta(entry)}`
529
- );
530
- } else {
531
- console.log(`${entry.name}${formatSourceMeta(entry)}`);
532
647
  }
533
- }
648
+
649
+ const detailEntry = entry as AgentEntry | SnippetEntry | InstructionEntry;
650
+ return {
651
+ title: entry.name,
652
+ meta: sourceLabel(entry),
653
+ description: displayDescription(detailEntry.description),
654
+ };
655
+ });
656
+
657
+ console.log(
658
+ renderPage({
659
+ title: `fclt list ${opts.kind}`,
660
+ subtitle: `${entries.length} matching entr${entries.length === 1 ? "y" : "ies"}`,
661
+ sections: [{ title: "Entries", lines: renderCatalog(items) }],
662
+ footer: describeFilters(opts.filters)
663
+ ? [describeFilters(opts.filters)]
664
+ : undefined,
665
+ })
666
+ );
534
667
  }
535
668
 
536
669
  async function readEntryContents(entryPath: string): Promise<string> {
@@ -589,35 +722,55 @@ async function findCommand(argv: string[]) {
589
722
  return;
590
723
  }
591
724
 
592
- for (const entry of matches) {
593
- const desc = entry.description ? `\t${entry.description}` : "";
594
- console.log(`${entry.kind}:${entry.name}${desc}${formatSourceMeta(entry)}`);
725
+ if (matches.length === 0) {
726
+ console.log(
727
+ renderPage({
728
+ title: "fclt find",
729
+ subtitle: `No matches for "${opts.text}".`,
730
+ sections: [
731
+ {
732
+ title: "Try",
733
+ lines: renderBullets([
734
+ renderCode("fclt list"),
735
+ renderCode("fclt index --force"),
736
+ ]),
737
+ },
738
+ ],
739
+ })
740
+ );
741
+ return;
595
742
  }
743
+
744
+ console.log(
745
+ renderPage({
746
+ title: "fclt find",
747
+ subtitle: `${matches.length} match${matches.length === 1 ? "" : "es"} for "${opts.text}"`,
748
+ sections: [
749
+ {
750
+ title: "Results",
751
+ lines: renderCatalog(
752
+ matches.map((entry) => ({
753
+ title: `${entry.kind}:${entry.name}`,
754
+ meta: sourceLabel(entry),
755
+ description: displayDescription(entry.description),
756
+ }))
757
+ ),
758
+ },
759
+ ],
760
+ })
761
+ );
596
762
  }
597
763
 
598
764
  const SECRET_KEY_RE = /(TOKEN|KEY|SECRET|PASSWORD|PASS|BEARER)/i;
599
765
  const SECRETY_STRING_RE =
600
766
  /\b(sk-[A-Za-z0-9]{10,}|ghp_[A-Za-z0-9]{10,}|github_pat_[A-Za-z0-9_]{10,})\b/g;
767
+ const INLINE_NAME_DESCRIPTION_RE = /^name:\s+\S+\s+description:\s*/i;
768
+ const TRAILING_NEWLINE_RE = /\n$/;
601
769
 
602
770
  function redactPossibleSecrets(value: string): string {
603
771
  return value.replace(SECRETY_STRING_RE, "<redacted>");
604
772
  }
605
773
 
606
- function formatSourceMeta(entry: {
607
- sourceKind?: string;
608
- scope?: string;
609
- }): string {
610
- const source = entry.sourceKind?.trim();
611
- const scope = entry.scope?.trim();
612
- if (!(source || scope)) {
613
- return "";
614
- }
615
- if (source && scope) {
616
- return `\t[${source}/${scope}]`;
617
- }
618
- return `\t[${source ?? scope}]`;
619
- }
620
-
621
774
  function sanitizeForDisplay(value: unknown): unknown {
622
775
  if (typeof value === "string") {
623
776
  return redactPossibleSecrets(value);
@@ -796,10 +949,28 @@ async function showCommand(argv: string[]) {
796
949
  }
797
950
  }
798
951
 
799
- console.log(`${kind}:${entry.name}`);
800
- console.log(JSON.stringify(displayEntry, null, 2));
801
- console.log("\n---\n");
802
- console.log(displayContents);
952
+ console.log(
953
+ renderPage({
954
+ title: `fclt show ${kind}:${entry.name}`,
955
+ subtitle: contentPath,
956
+ sections: [
957
+ {
958
+ title: "Metadata",
959
+ lines: renderJsonBlock(displayEntry),
960
+ },
961
+ {
962
+ title: "Contents",
963
+ lines: displayContents.replace(TRAILING_NEWLINE_RE, "").split("\n"),
964
+ },
965
+ ],
966
+ footer:
967
+ kind === "mcp" && !showSecrets
968
+ ? [
969
+ "Secrets are redacted. Re-run with --show-secrets only when you need raw values.",
970
+ ]
971
+ : undefined,
972
+ })
973
+ );
803
974
  }
804
975
 
805
976
  async function graphCommand(argv: string[]) {
@@ -858,51 +1029,126 @@ async function graphCommand(argv: string[]) {
858
1029
  }
859
1030
 
860
1031
  if (opts.kind === "show") {
861
- console.log(node.id);
862
- console.log(JSON.stringify(node, null, 2));
863
- console.log("\nDependencies:");
864
- for (const dep of deps) {
865
- console.log(
866
- `- ${dep.edge.kind}\t${dep.node.id}\t(${dep.edge.locator})`
867
- );
868
- }
869
- console.log("\nDependents:");
870
- for (const dependent of dependents) {
871
- console.log(
872
- `- ${dependent.edge.kind}\t${dependent.node.id}\t(${dependent.edge.locator})`
873
- );
874
- }
1032
+ console.log(
1033
+ renderPage({
1034
+ title: `fclt graph ${node.id}`,
1035
+ subtitle: `${node.kind} ${sourceLabel(node)}`,
1036
+ sections: [
1037
+ {
1038
+ title: "Node",
1039
+ lines: renderKeyValue([
1040
+ ["id", node.id],
1041
+ ["kind", node.kind],
1042
+ ["name", node.name],
1043
+ ["path", node.path ?? "—"],
1044
+ ["canonicalRef", node.canonicalRef ?? "—"],
1045
+ ]),
1046
+ },
1047
+ {
1048
+ title: "Dependencies",
1049
+ lines:
1050
+ deps.length > 0
1051
+ ? renderCatalog(
1052
+ deps.map((dep) => ({
1053
+ title: dep.node.id,
1054
+ meta: dep.edge.kind,
1055
+ details: [dep.edge.locator],
1056
+ }))
1057
+ )
1058
+ : ["No dependencies."],
1059
+ },
1060
+ {
1061
+ title: "Dependents",
1062
+ lines:
1063
+ dependents.length > 0
1064
+ ? renderCatalog(
1065
+ dependents.map((dependent) => ({
1066
+ title: dependent.node.id,
1067
+ meta: dependent.edge.kind,
1068
+ details: [dependent.edge.locator],
1069
+ }))
1070
+ )
1071
+ : ["No dependents."],
1072
+ },
1073
+ ],
1074
+ })
1075
+ );
875
1076
  return;
876
1077
  }
877
1078
 
878
1079
  const relations = opts.kind === "deps" ? deps : dependents;
879
- for (const relation of relations) {
880
- console.log(
881
- `${relation.edge.kind}\t${relation.node.id}\t(${relation.edge.locator})`
882
- );
883
- }
1080
+ console.log(
1081
+ renderPage({
1082
+ title: `fclt graph ${opts.kind}`,
1083
+ subtitle: `${relations.length} relation${relations.length === 1 ? "" : "s"} for ${node.id}`,
1084
+ sections: [
1085
+ {
1086
+ title: opts.kind === "deps" ? "Dependencies" : "Dependents",
1087
+ lines:
1088
+ relations.length > 0
1089
+ ? renderCatalog(
1090
+ relations.map((relation) => ({
1091
+ title: relation.node.id,
1092
+ meta: relation.edge.kind,
1093
+ details: [relation.edge.locator],
1094
+ }))
1095
+ )
1096
+ : ["No relations found."],
1097
+ },
1098
+ ],
1099
+ })
1100
+ );
884
1101
  } catch (err) {
885
1102
  console.error(err instanceof Error ? err.message : String(err));
886
1103
  process.exitCode = 1;
887
1104
  }
888
1105
  }
889
1106
 
890
- function adaptersCommand(argv: string[]) {
1107
+ async function adaptersCommand(argv: string[]) {
891
1108
  if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
892
1109
  console.log(
893
- "fclt adapters — list registered tool adapters\n\nUsage:\n fclt adapters\n"
1110
+ renderPage({
1111
+ title: "fclt adapters",
1112
+ subtitle: "List registered tool adapters.",
1113
+ sections: [
1114
+ {
1115
+ title: "Usage",
1116
+ lines: renderBullets([renderCode("fclt adapters")]),
1117
+ },
1118
+ ],
1119
+ })
894
1120
  );
895
1121
  return;
896
1122
  }
1123
+ const { getAllAdapters } = await import("./adapters");
897
1124
  const adapters = getAllAdapters();
898
1125
  if (!adapters.length) {
899
- console.log("No adapters registered.");
1126
+ console.log(
1127
+ renderPage({
1128
+ title: "fclt adapters",
1129
+ subtitle: "No adapters registered.",
1130
+ sections: [],
1131
+ })
1132
+ );
900
1133
  return;
901
1134
  }
902
- for (const adapter of adapters) {
903
- const versions = adapter.versions.join(", ");
904
- console.log(`${adapter.id}\t${versions}`);
905
- }
1135
+ console.log(
1136
+ renderPage({
1137
+ title: "fclt adapters",
1138
+ subtitle: `${adapters.length} registered adapter${adapters.length === 1 ? "" : "s"}`,
1139
+ sections: [
1140
+ {
1141
+ title: "Adapters",
1142
+ lines: renderCatalog(
1143
+ adapters.map((adapter) => ({
1144
+ title: adapter.id,
1145
+ description: `Versions: ${adapter.versions.join(", ")}`,
1146
+ }))
1147
+ ),
1148
+ },
1149
+ ],
1150
+ })
1151
+ );
906
1152
  }
907
1153
 
908
1154
  async function main(argv: string[]) {
@@ -914,28 +1160,35 @@ async function main(argv: string[]) {
914
1160
 
915
1161
  // Convenience: allow `fclt --show-duplicates` as shorthand for `fclt scan --show-duplicates`.
916
1162
  if (cmd === "--show-duplicates") {
1163
+ const { scanCommand } = await import("./scan");
917
1164
  await scanCommand([cmd, ...rest]);
918
1165
  return;
919
1166
  }
920
1167
 
921
1168
  switch (cmd) {
922
1169
  case "scan":
923
- await scanCommand(rest);
1170
+ await import("./scan").then(({ scanCommand }) => scanCommand(rest));
924
1171
  return;
925
1172
  case "audit":
926
- await auditCommand(rest);
1173
+ await import("./audit").then(({ auditCommand }) => auditCommand(rest));
927
1174
  return;
928
1175
  case "migrate":
929
- await migrateCommand(rest);
1176
+ await import("./migrate").then(({ migrateCommand }) =>
1177
+ migrateCommand(rest)
1178
+ );
930
1179
  return;
931
1180
  case "doctor":
932
- await doctorCommand(rest);
1181
+ await import("./doctor").then(({ doctorCommand }) => doctorCommand(rest));
933
1182
  return;
934
1183
  case "consolidate":
935
- await consolidateCommand(rest);
1184
+ await import("./consolidate").then(({ consolidateCommand }) =>
1185
+ consolidateCommand(rest)
1186
+ );
936
1187
  return;
937
1188
  case "index":
938
- await indexCommand(rest);
1189
+ await import("./index-builder").then(({ indexCommand }) =>
1190
+ indexCommand(rest)
1191
+ );
939
1192
  return;
940
1193
  case "list":
941
1194
  await listCommand(rest);
@@ -950,65 +1203,91 @@ async function main(argv: string[]) {
950
1203
  await graphCommand(rest);
951
1204
  return;
952
1205
  case "ai":
953
- await aiCommand(rest);
1206
+ await import("./ai").then(({ aiCommand }) => aiCommand(rest));
954
1207
  return;
955
1208
  case "adapters":
956
1209
  await adaptersCommand(rest);
957
1210
  return;
958
1211
  case "trust":
959
- await trustCommand(rest);
1212
+ await import("./trust").then(({ trustCommand }) => trustCommand(rest));
960
1213
  return;
961
1214
  case "untrust":
962
- await untrustCommand(rest);
1215
+ await import("./trust").then(({ untrustCommand }) =>
1216
+ untrustCommand(rest)
1217
+ );
963
1218
  return;
964
1219
  case "manage":
965
- await manageCommand(rest);
1220
+ await import("./manage").then(({ manageCommand }) => manageCommand(rest));
966
1221
  return;
967
1222
  case "unmanage":
968
- await unmanageCommand(rest);
1223
+ await import("./manage").then(({ unmanageCommand }) =>
1224
+ unmanageCommand(rest)
1225
+ );
969
1226
  return;
970
1227
  case "managed":
971
- await managedCommand(rest);
1228
+ await import("./manage").then(({ managedCommand }) =>
1229
+ managedCommand(rest)
1230
+ );
972
1231
  return;
973
1232
  case "enable":
974
- await enableCommand(rest);
1233
+ await import("./enable-disable").then(({ enableCommand }) =>
1234
+ enableCommand(rest)
1235
+ );
975
1236
  return;
976
1237
  case "disable":
977
- await disableCommand(rest);
1238
+ await import("./enable-disable").then(({ disableCommand }) =>
1239
+ disableCommand(rest)
1240
+ );
978
1241
  return;
979
1242
  case "sync":
980
- await syncCommand(rest);
1243
+ await import("./manage").then(({ syncCommand }) => syncCommand(rest));
981
1244
  return;
982
1245
  case "autosync":
983
- await autosyncCommand(rest);
1246
+ await import("./autosync").then(({ autosyncCommand }) =>
1247
+ autosyncCommand(rest)
1248
+ );
984
1249
  return;
985
1250
  case "search":
986
- await searchCommand(rest);
1251
+ await import("./remote").then(({ searchCommand }) => searchCommand(rest));
987
1252
  return;
988
1253
  case "install":
989
- await installCommand(rest);
1254
+ await import("./remote").then(({ installCommand }) =>
1255
+ installCommand(rest)
1256
+ );
990
1257
  return;
991
1258
  case "update":
992
1259
  if (rest.includes("--self")) {
993
- await selfUpdateCommand(rest.filter((arg) => arg !== "--self"));
1260
+ await import("./self-update").then(({ selfUpdateCommand }) =>
1261
+ selfUpdateCommand(rest.filter((arg) => arg !== "--self"))
1262
+ );
994
1263
  return;
995
1264
  }
996
- await updateCommand(rest);
1265
+ await import("./remote").then(({ updateCommand }) => updateCommand(rest));
997
1266
  return;
998
1267
  case "self-update":
999
- await selfUpdateCommand(rest);
1268
+ await import("./self-update").then(({ selfUpdateCommand }) =>
1269
+ selfUpdateCommand(rest)
1270
+ );
1000
1271
  return;
1001
1272
  case "verify-source":
1002
- await verifySourceCommand(rest);
1273
+ await import("./remote").then(({ verifySourceCommand }) =>
1274
+ verifySourceCommand(rest)
1275
+ );
1003
1276
  return;
1004
1277
  case "templates":
1005
- await templatesCommand(rest);
1278
+ await import("./remote").then(({ templatesCommand }) =>
1279
+ templatesCommand(rest)
1280
+ );
1006
1281
  return;
1007
1282
  case "sources":
1008
- await sourcesCommand(rest);
1283
+ await import("./remote").then(({ sourcesCommand }) =>
1284
+ sourcesCommand(rest)
1285
+ );
1009
1286
  return;
1010
1287
  case "snippets":
1011
- await snippetsCommand(rest);
1288
+ await import("./snippets-cli").then(({ snippetsCommand }) =>
1289
+ snippetsCommand(rest)
1290
+ );
1012
1291
  return;
1013
1292
  default:
1014
1293
  console.error(`Unknown command: ${cmd}`);