facult 2.5.2 → 2.7.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/src/trust.ts CHANGED
@@ -4,6 +4,13 @@ import type { FacultIndex } from "./index-builder";
4
4
  import { facultAiIndexPath, facultRootDir } from "./paths";
5
5
 
6
6
  type TrustMode = "trust" | "untrust";
7
+ type TrustTargetKind = "skills" | "mcp";
8
+
9
+ interface TrustArgs {
10
+ all: boolean;
11
+ kind?: TrustTargetKind;
12
+ names: string[];
13
+ }
7
14
 
8
15
  function isPlainObject(v: unknown): v is Record<string, unknown> {
9
16
  return !!v && typeof v === "object" && !Array.isArray(v);
@@ -28,6 +35,34 @@ function parseEntryName(raw: string): { kind: "skill" | "mcp"; name: string } {
28
35
  return { kind: "skill", name: raw };
29
36
  }
30
37
 
38
+ function collectTargetNames(args: {
39
+ index: FacultIndex;
40
+ all: boolean;
41
+ kind?: TrustTargetKind;
42
+ names: string[];
43
+ }): string[] {
44
+ if (!args.all) {
45
+ return args.names;
46
+ }
47
+
48
+ if (args.kind === "skills") {
49
+ return Object.keys(args.index.skills).sort();
50
+ }
51
+
52
+ if (args.kind === "mcp") {
53
+ return Object.keys(args.index.mcp?.servers ?? {})
54
+ .sort()
55
+ .map((name) => `mcp:${name}`);
56
+ }
57
+
58
+ return [
59
+ ...Object.keys(args.index.skills).sort(),
60
+ ...Object.keys(args.index.mcp?.servers ?? {})
61
+ .sort()
62
+ .map((name) => `mcp:${name}`),
63
+ ];
64
+ }
65
+
31
66
  async function loadIndex(homeDir: string): Promise<FacultIndex> {
32
67
  const { path: indexPath } = await ensureAiIndexPath({
33
68
  homeDir,
@@ -71,23 +106,33 @@ function setTrustFields(
71
106
 
72
107
  export async function applyTrust({
73
108
  names,
109
+ all,
110
+ kind,
74
111
  mode,
75
112
  homeDir,
76
113
  }: {
77
114
  names: string[];
115
+ all?: boolean;
116
+ kind?: TrustTargetKind;
78
117
  mode: TrustMode;
79
118
  homeDir?: string;
80
119
  }) {
81
- if (!names.length) {
82
- throw new Error("At least one name is required.");
83
- }
84
120
  const home = homeDir ?? homedir();
85
121
 
86
122
  const index = ensureIndexStructure(await loadIndex(home));
123
+ const targetNames = collectTargetNames({
124
+ index,
125
+ all: all === true,
126
+ kind,
127
+ names,
128
+ });
129
+ if (!targetNames.length) {
130
+ throw new Error("No matching entries found.");
131
+ }
87
132
  const now = new Date().toISOString();
88
133
 
89
134
  const missing: string[] = [];
90
- for (const raw of names) {
135
+ for (const raw of targetNames) {
91
136
  const { kind, name } = parseEntryName(raw);
92
137
  if (kind === "skill") {
93
138
  const entry = index.skills[name] as unknown;
@@ -115,17 +160,44 @@ export async function applyTrust({
115
160
  }
116
161
 
117
162
  function parseNamesFromArgv(argv: string[]): string[] {
163
+ return parseTrustArgs(argv).names;
164
+ }
165
+
166
+ function parseTrustArgs(argv: string[]): TrustArgs {
118
167
  const names: string[] = [];
168
+ let all = false;
169
+
119
170
  for (const arg of argv) {
120
171
  if (!arg) {
121
172
  continue;
122
173
  }
174
+ if (arg === "--all") {
175
+ all = true;
176
+ continue;
177
+ }
123
178
  if (arg.startsWith("-")) {
124
179
  throw new Error(`Unknown option: ${arg}`);
125
180
  }
126
181
  names.push(arg);
127
182
  }
128
- return names;
183
+
184
+ if (all) {
185
+ if (names.length === 0) {
186
+ return { all: true, names: [] };
187
+ }
188
+ if (names.length === 1 && (names[0] === "skills" || names[0] === "mcp")) {
189
+ return { all: true, kind: names[0], names: [] };
190
+ }
191
+ throw new Error(
192
+ 'When using --all, optionally pass only "skills" or "mcp".'
193
+ );
194
+ }
195
+
196
+ if (!names.length) {
197
+ throw new Error("At least one name is required.");
198
+ }
199
+
200
+ return { all: false, names };
129
201
  }
130
202
 
131
203
  export async function trustCommand(argv: string[]) {
@@ -135,13 +207,31 @@ export async function trustCommand(argv: string[]) {
135
207
  Usage:
136
208
  fclt trust <name> [moreNames...]
137
209
  fclt trust mcp:<name> [moreNames...]
210
+ fclt trust --all
211
+ fclt trust skills --all
212
+ fclt trust mcp --all
138
213
  `);
139
214
  return;
140
215
  }
141
216
  try {
142
- const names = parseNamesFromArgv(argv);
143
- await applyTrust({ names, mode: "trust" });
144
- console.log(`Marked as trusted: ${names.join(", ")}`);
217
+ const parsed = parseTrustArgs(argv);
218
+ await applyTrust({
219
+ names: parsed.names,
220
+ all: parsed.all,
221
+ kind: parsed.kind,
222
+ mode: "trust",
223
+ });
224
+ if (parsed.all) {
225
+ const targetLabel =
226
+ parsed.kind === "skills"
227
+ ? "all skills"
228
+ : parsed.kind === "mcp"
229
+ ? "all MCP servers"
230
+ : "all skills and MCP servers";
231
+ console.log(`Marked as trusted: ${targetLabel}`);
232
+ } else {
233
+ console.log(`Marked as trusted: ${parsed.names.join(", ")}`);
234
+ }
145
235
  console.log(
146
236
  'Note: Trust is an annotation. Run "fclt audit" for security review.'
147
237
  );
@@ -158,13 +248,31 @@ export async function untrustCommand(argv: string[]) {
158
248
  Usage:
159
249
  fclt untrust <name> [moreNames...]
160
250
  fclt untrust mcp:<name> [moreNames...]
251
+ fclt untrust --all
252
+ fclt untrust skills --all
253
+ fclt untrust mcp --all
161
254
  `);
162
255
  return;
163
256
  }
164
257
  try {
165
- const names = parseNamesFromArgv(argv);
166
- await applyTrust({ names, mode: "untrust" });
167
- console.log(`Marked as untrusted: ${names.join(", ")}`);
258
+ const parsed = parseTrustArgs(argv);
259
+ await applyTrust({
260
+ names: parsed.names,
261
+ all: parsed.all,
262
+ kind: parsed.kind,
263
+ mode: "untrust",
264
+ });
265
+ if (parsed.all) {
266
+ const targetLabel =
267
+ parsed.kind === "skills"
268
+ ? "all skills"
269
+ : parsed.kind === "mcp"
270
+ ? "all MCP servers"
271
+ : "all skills and MCP servers";
272
+ console.log(`Marked as untrusted: ${targetLabel}`);
273
+ } else {
274
+ console.log(`Marked as untrusted: ${parsed.names.join(", ")}`);
275
+ }
168
276
  } catch (err) {
169
277
  console.error(err instanceof Error ? err.message : String(err));
170
278
  process.exitCode = 1;
@@ -0,0 +1,95 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { dirname, relative } from "node:path";
3
+
4
+ export type GitPathExposure =
5
+ | {
6
+ insideRepo: false;
7
+ repoRoot: null;
8
+ state: "outside-repo";
9
+ }
10
+ | {
11
+ insideRepo: true;
12
+ repoRoot: string;
13
+ state: "tracked" | "ignored" | "untracked";
14
+ };
15
+
16
+ async function runGit(
17
+ args: string[],
18
+ cwd: string
19
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
20
+ const gitBinary = Bun.which("git") ?? "/usr/bin/git";
21
+ const proc = Bun.spawn({
22
+ cmd: [gitBinary, ...args],
23
+ cwd,
24
+ stdout: "pipe",
25
+ stderr: "pipe",
26
+ });
27
+ const [stdout, stderr, exitCode] = await Promise.all([
28
+ new Response(proc.stdout).text(),
29
+ new Response(proc.stderr).text(),
30
+ proc.exited,
31
+ ]);
32
+ return { exitCode, stdout, stderr };
33
+ }
34
+
35
+ async function resolveGitRepoRoot(pathValue: string): Promise<string | null> {
36
+ const cwd = dirname(pathValue);
37
+ const result = await runGit(["rev-parse", "--show-toplevel"], cwd);
38
+ if (result.exitCode !== 0) {
39
+ return null;
40
+ }
41
+ const repoRoot = result.stdout.trim();
42
+ if (!repoRoot) {
43
+ return null;
44
+ }
45
+ return await realpath(repoRoot).catch(() => repoRoot);
46
+ }
47
+
48
+ export async function getGitPathExposure(
49
+ pathValue: string
50
+ ): Promise<GitPathExposure> {
51
+ const repoRoot = await resolveGitRepoRoot(pathValue);
52
+ if (!repoRoot) {
53
+ return {
54
+ insideRepo: false,
55
+ repoRoot: null,
56
+ state: "outside-repo",
57
+ };
58
+ }
59
+
60
+ const resolvedPath = await realpath(pathValue).catch(() => pathValue);
61
+ const repoRelativePath = relative(repoRoot, resolvedPath);
62
+ if (
63
+ !repoRelativePath ||
64
+ repoRelativePath === "" ||
65
+ repoRelativePath.startsWith("../")
66
+ ) {
67
+ return {
68
+ insideRepo: false,
69
+ repoRoot: null,
70
+ state: "outside-repo",
71
+ };
72
+ }
73
+
74
+ const tracked = await runGit(
75
+ ["ls-files", "--error-unmatch", "--", repoRelativePath],
76
+ repoRoot
77
+ );
78
+ if (tracked.exitCode === 0) {
79
+ return {
80
+ insideRepo: true,
81
+ repoRoot,
82
+ state: "tracked",
83
+ };
84
+ }
85
+
86
+ const ignored = await runGit(
87
+ ["check-ignore", "-q", "--", repoRelativePath],
88
+ repoRoot
89
+ );
90
+ return {
91
+ insideRepo: true,
92
+ repoRoot,
93
+ state: ignored.exitCode === 0 ? "ignored" : "untracked",
94
+ };
95
+ }