facult 1.0.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -0
  3. package/bin/facult.cjs +302 -0
  4. package/package.json +78 -0
  5. package/src/adapters/claude-cli.ts +18 -0
  6. package/src/adapters/claude-desktop.ts +15 -0
  7. package/src/adapters/clawdbot.ts +18 -0
  8. package/src/adapters/codex.ts +19 -0
  9. package/src/adapters/cursor.ts +18 -0
  10. package/src/adapters/index.ts +69 -0
  11. package/src/adapters/mcp.ts +270 -0
  12. package/src/adapters/reference.ts +9 -0
  13. package/src/adapters/skills.ts +47 -0
  14. package/src/adapters/types.ts +42 -0
  15. package/src/adapters/version.ts +18 -0
  16. package/src/audit/agent.ts +1071 -0
  17. package/src/audit/index.ts +74 -0
  18. package/src/audit/static.ts +1130 -0
  19. package/src/audit/tui.ts +704 -0
  20. package/src/audit/types.ts +68 -0
  21. package/src/audit/update-index.ts +115 -0
  22. package/src/conflicts.ts +135 -0
  23. package/src/consolidate-conflict-action.ts +57 -0
  24. package/src/consolidate.ts +1637 -0
  25. package/src/enable-disable.ts +349 -0
  26. package/src/index-builder.ts +562 -0
  27. package/src/index.ts +589 -0
  28. package/src/manage.ts +894 -0
  29. package/src/migrate.ts +272 -0
  30. package/src/paths.ts +238 -0
  31. package/src/quarantine.ts +217 -0
  32. package/src/query.ts +186 -0
  33. package/src/remote-manifest-integrity.ts +367 -0
  34. package/src/remote-providers.ts +905 -0
  35. package/src/remote-source-policy.ts +237 -0
  36. package/src/remote-sources.ts +162 -0
  37. package/src/remote-types.ts +136 -0
  38. package/src/remote.ts +1970 -0
  39. package/src/scan.ts +2427 -0
  40. package/src/schema.ts +39 -0
  41. package/src/self-update.ts +408 -0
  42. package/src/snippets-cli.ts +293 -0
  43. package/src/snippets.ts +706 -0
  44. package/src/source-trust.ts +203 -0
  45. package/src/trust-list.ts +232 -0
  46. package/src/trust.ts +170 -0
  47. package/src/tui.ts +118 -0
  48. package/src/util/codex-toml.ts +126 -0
  49. package/src/util/json.ts +32 -0
  50. package/src/util/skills.ts +55 -0
package/src/schema.ts ADDED
@@ -0,0 +1,39 @@
1
+ export interface Provenance {
2
+ /** Tool/source id (e.g. "cursor", "claude-desktop"). */
3
+ sourceId: string;
4
+ /** Path to the config file this item came from (if applicable). */
5
+ sourcePath: string;
6
+ /** ISO timestamp when facult imported/consolidated this item. */
7
+ importedAt: string;
8
+ /** Optional source file mtime at import time (ISO). */
9
+ sourceModifiedAt?: string;
10
+ }
11
+
12
+ export type McpTransport = "stdio" | "http" | "sse";
13
+
14
+ export interface CanonicalMcpServer {
15
+ name: string;
16
+
17
+ // Known/common fields (cross-tool)
18
+ transport?: McpTransport;
19
+ command?: string;
20
+ args?: string[];
21
+ url?: string;
22
+ env?: Record<string, string>;
23
+
24
+ /** Unknown/tool-specific fields preserved losslessly for round-tripping. */
25
+ vendorExtensions?: Record<string, unknown>;
26
+
27
+ /** Where this server definition came from. */
28
+ provenance?: Provenance;
29
+ }
30
+
31
+ export interface CanonicalMcpRegistry {
32
+ /** Schema version for the canonical MCP registry format. */
33
+ version: 1;
34
+ updatedAt: string;
35
+ mcpServers: Record<string, CanonicalMcpServer>;
36
+
37
+ /** Unknown top-level fields preserved losslessly for round-tripping. */
38
+ vendorExtensions?: Record<string, unknown>;
39
+ }
@@ -0,0 +1,408 @@
1
+ import { mkdir, rename } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join, resolve, sep } from "node:path";
4
+
5
+ const REPO_OWNER = "hack-dance";
6
+ const REPO_NAME = "facult";
7
+ const DOWNLOAD_RETRIES = 12;
8
+ const DOWNLOAD_RETRY_DELAY_MS = 5000;
9
+
10
+ type InstallMethod =
11
+ | "script-dev"
12
+ | "script-bin"
13
+ | "release-script"
14
+ | "npm-binary-cache"
15
+ | "unknown";
16
+
17
+ interface InstallState {
18
+ version: number;
19
+ method?: string;
20
+ packageVersion?: string;
21
+ binaryPath?: string;
22
+ packageManager?: string;
23
+ source?: string;
24
+ installedAt?: string;
25
+ }
26
+
27
+ interface ParsedArgs {
28
+ dryRun: boolean;
29
+ requestedVersion?: string;
30
+ }
31
+
32
+ interface DetectInstallMethodContext {
33
+ envInstallMethod?: string;
34
+ executablePath?: string;
35
+ homeDir?: string;
36
+ }
37
+
38
+ function printHelp() {
39
+ console.log(`facult self-update — update facult itself based on install method
40
+
41
+ Usage:
42
+ facult self-update [--version <x.y.z|latest>] [--dry-run]
43
+ facult update --self [--version <x.y.z|latest>] [--dry-run]
44
+
45
+ Options:
46
+ --version Target version (defaults to latest)
47
+ --dry-run Print update actions without changing anything
48
+ `);
49
+ }
50
+
51
+ export function parseSelfUpdateArgs(argv: string[]): ParsedArgs {
52
+ let dryRun = false;
53
+ let requestedVersion: string | undefined;
54
+
55
+ for (let i = 0; i < argv.length; i += 1) {
56
+ const arg = argv[i];
57
+ if (!arg) {
58
+ continue;
59
+ }
60
+ if (arg === "--dry-run") {
61
+ dryRun = true;
62
+ continue;
63
+ }
64
+ if (arg === "--version") {
65
+ const next = argv[i + 1];
66
+ if (!next) {
67
+ throw new Error("--version requires a value.");
68
+ }
69
+ requestedVersion = next.trim();
70
+ i += 1;
71
+ continue;
72
+ }
73
+ if (arg.startsWith("--version=")) {
74
+ requestedVersion = arg.slice("--version=".length).trim();
75
+ continue;
76
+ }
77
+ throw new Error(`Unknown option: ${arg}`);
78
+ }
79
+
80
+ return { dryRun, requestedVersion };
81
+ }
82
+
83
+ async function loadInstallState(home: string): Promise<InstallState | null> {
84
+ const path = join(home, ".facult", "install.json");
85
+ try {
86
+ const txt = await Bun.file(path).text();
87
+ return JSON.parse(txt) as InstallState;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ export function detectInstallMethod(
94
+ state: InstallState | null,
95
+ context: DetectInstallMethodContext = {}
96
+ ): InstallMethod {
97
+ const envMethod =
98
+ context.envInstallMethod ?? process.env.FACULT_INSTALL_METHOD?.trim();
99
+ if (envMethod === "script-dev" || envMethod === "script-bin") {
100
+ return envMethod;
101
+ }
102
+ if (envMethod === "npm-binary-cache") {
103
+ return "npm-binary-cache";
104
+ }
105
+ const raw = state?.method?.trim();
106
+ if (
107
+ raw === "script-dev" ||
108
+ raw === "script-bin" ||
109
+ raw === "release-script" ||
110
+ raw === "npm-binary-cache"
111
+ ) {
112
+ return raw;
113
+ }
114
+
115
+ const exec = context.executablePath ?? process.execPath;
116
+ const home = context.homeDir ?? homedir();
117
+ const facultBin = join(home, ".facult", "bin");
118
+ if (exec.startsWith(facultBin + sep) && basename(exec).startsWith("facult")) {
119
+ return "release-script";
120
+ }
121
+
122
+ return "unknown";
123
+ }
124
+
125
+ function resolvePlatformTarget(): {
126
+ platform: string;
127
+ arch: string;
128
+ ext: string;
129
+ } {
130
+ const platform = process.platform;
131
+ const arch = process.arch;
132
+
133
+ if (platform === "darwin" && (arch === "arm64" || arch === "x64")) {
134
+ return { platform: "darwin", arch, ext: "" };
135
+ }
136
+ if (platform === "linux" && arch === "x64") {
137
+ return { platform: "linux", arch, ext: "" };
138
+ }
139
+ if (platform === "win32" && arch === "x64") {
140
+ return { platform: "windows", arch, ext: ".exe" };
141
+ }
142
+ throw new Error(
143
+ [
144
+ `Unsupported platform/arch: ${platform}/${arch}`,
145
+ "Prebuilt binaries are currently available for:",
146
+ " - darwin/x64",
147
+ " - darwin/arm64",
148
+ " - linux/x64",
149
+ " - windows/x64",
150
+ ].join("\n")
151
+ );
152
+ }
153
+
154
+ async function resolveLatestTag(): Promise<string> {
155
+ const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`;
156
+ const res = await fetch(url, {
157
+ headers: {
158
+ "user-agent": "facult-self-update",
159
+ accept: "application/vnd.github+json",
160
+ },
161
+ });
162
+ if (!res.ok) {
163
+ throw new Error(`Failed to resolve latest release tag: HTTP ${res.status}`);
164
+ }
165
+ const json = (await res.json()) as Record<string, unknown>;
166
+ const tag = typeof json.tag_name === "string" ? json.tag_name.trim() : "";
167
+ if (!tag) {
168
+ throw new Error("Latest release did not include a tag.");
169
+ }
170
+ return tag;
171
+ }
172
+
173
+ export function normalizeVersionTag(requested?: string): string | null {
174
+ if (!requested || requested === "latest") {
175
+ return null;
176
+ }
177
+ return requested.startsWith("v") ? requested : `v${requested}`;
178
+ }
179
+
180
+ export function stripTagPrefix(tag: string): string {
181
+ return tag.startsWith("v") ? tag.slice(1) : tag;
182
+ }
183
+
184
+ async function writeInstallState(args: {
185
+ home: string;
186
+ method: InstallMethod;
187
+ packageVersion?: string;
188
+ binaryPath?: string;
189
+ }) {
190
+ const dir = join(args.home, ".facult");
191
+ await mkdir(dir, { recursive: true });
192
+ const payload: InstallState = {
193
+ version: 1,
194
+ method: args.method,
195
+ packageVersion: args.packageVersion,
196
+ binaryPath: args.binaryPath,
197
+ source: args.method === "npm-binary-cache" ? "npm" : "direct",
198
+ installedAt: new Date().toISOString(),
199
+ };
200
+ await Bun.write(
201
+ join(dir, "install.json"),
202
+ `${JSON.stringify(payload, null, 2)}\n`
203
+ );
204
+ }
205
+
206
+ async function selfUpdateBinary(args: {
207
+ home: string;
208
+ state: InstallState | null;
209
+ method: InstallMethod;
210
+ requestedVersion?: string;
211
+ dryRun: boolean;
212
+ }) {
213
+ const target = resolvePlatformTarget();
214
+ const explicitTag = normalizeVersionTag(args.requestedVersion);
215
+ const tag = explicitTag ?? (await resolveLatestTag());
216
+ const version = stripTagPrefix(tag);
217
+ const assetName = `facult-${version}-${target.platform}-${target.arch}${target.ext}`;
218
+ const url = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${tag}/${assetName}`;
219
+
220
+ const defaultBinaryName =
221
+ target.platform === "windows" ? "facult.exe" : "facult";
222
+ const fallbackPath = join(args.home, ".facult", "bin", defaultBinaryName);
223
+ const currentExec = process.execPath;
224
+ const preferredPath =
225
+ args.state?.binaryPath ||
226
+ (basename(currentExec).startsWith("facult") ? currentExec : fallbackPath);
227
+ const binaryPath = resolve(preferredPath);
228
+
229
+ if (args.dryRun) {
230
+ console.log(`[dry-run] Would download ${url}`);
231
+ console.log(`[dry-run] Would replace ${binaryPath}`);
232
+ return;
233
+ }
234
+
235
+ await mkdir(dirname(binaryPath), { recursive: true });
236
+ const bytes = await fetchReleaseBinaryWithRetry(url);
237
+ const tmpPath = `${binaryPath}.tmp-${Date.now()}`;
238
+ await Bun.write(tmpPath, Buffer.from(bytes));
239
+ if (target.platform !== "windows") {
240
+ await Bun.$`chmod +x ${tmpPath}`.quiet();
241
+ }
242
+ await rename(tmpPath, binaryPath);
243
+ await writeInstallState({
244
+ home: args.home,
245
+ method: args.method === "unknown" ? "release-script" : args.method,
246
+ packageVersion: version,
247
+ binaryPath,
248
+ });
249
+ console.log(`Updated facult binary to ${version}`);
250
+ console.log(`Path: ${binaryPath}`);
251
+ }
252
+
253
+ type PackageManager = "npm" | "bun";
254
+
255
+ function chooseGlobalPackageManager(preferred?: string): PackageManager {
256
+ const forced = process.env.FACULT_INSTALL_PM?.trim();
257
+ if (forced === "npm" || forced === "bun") {
258
+ return forced;
259
+ }
260
+
261
+ if (preferred === "npm" && Bun.which("npm")) {
262
+ return "npm";
263
+ }
264
+ if (preferred === "bun" && Bun.which("bun")) {
265
+ return "bun";
266
+ }
267
+
268
+ if (Bun.which("npm")) {
269
+ return "npm";
270
+ }
271
+ if (Bun.which("bun")) {
272
+ return "bun";
273
+ }
274
+ return "npm";
275
+ }
276
+
277
+ async function selfUpdateViaPackageManager(args: {
278
+ requestedVersion?: string;
279
+ dryRun: boolean;
280
+ preferredPackageManager?: string;
281
+ }) {
282
+ const pm = chooseGlobalPackageManager(args.preferredPackageManager);
283
+ const targetVersion =
284
+ args.requestedVersion && args.requestedVersion !== "latest"
285
+ ? stripTagPrefix(args.requestedVersion)
286
+ : "latest";
287
+
288
+ const installSpec = `facult@${targetVersion}`;
289
+ const cmd =
290
+ pm === "npm"
291
+ ? ["npm", "install", "-g", installSpec]
292
+ : ["bun", "add", "-g", installSpec];
293
+
294
+ if (args.dryRun) {
295
+ console.log(`[dry-run] Would run: ${cmd.join(" ")}`);
296
+ return;
297
+ }
298
+
299
+ const proc = Bun.spawn({
300
+ cmd,
301
+ stdin: "inherit",
302
+ stdout: "inherit",
303
+ stderr: "inherit",
304
+ env: process.env,
305
+ });
306
+ const code = await proc.exited;
307
+ if (code !== 0) {
308
+ throw new Error(`Self-update failed via ${pm} (exit ${code}).`);
309
+ }
310
+ console.log(`Updated facult via ${pm}: ${installSpec}`);
311
+ }
312
+
313
+ async function fetchReleaseBinaryWithRetry(url: string): Promise<ArrayBuffer> {
314
+ let lastStatus: number | null = null;
315
+ let lastError: unknown;
316
+
317
+ for (let attempt = 1; attempt <= DOWNLOAD_RETRIES; attempt += 1) {
318
+ try {
319
+ const response = await fetch(url, {
320
+ headers: {
321
+ "user-agent": "facult-self-update",
322
+ accept: "application/octet-stream",
323
+ },
324
+ });
325
+ if (response.ok) {
326
+ return await response.arrayBuffer();
327
+ }
328
+
329
+ lastStatus = response.status;
330
+ if (response.status >= 500 && attempt < DOWNLOAD_RETRIES) {
331
+ await sleep(DOWNLOAD_RETRY_DELAY_MS);
332
+ continue;
333
+ }
334
+ if (response.status === 404 && attempt < DOWNLOAD_RETRIES) {
335
+ await sleep(DOWNLOAD_RETRY_DELAY_MS);
336
+ continue;
337
+ }
338
+
339
+ throw new Error(`Failed to download ${url}: HTTP ${response.status}`);
340
+ } catch (error) {
341
+ lastError = error;
342
+ if (attempt >= DOWNLOAD_RETRIES) {
343
+ break;
344
+ }
345
+ await sleep(DOWNLOAD_RETRY_DELAY_MS);
346
+ }
347
+ }
348
+
349
+ if (lastError instanceof Error) {
350
+ throw lastError;
351
+ }
352
+ const statusDetail = lastStatus ? ` HTTP ${lastStatus}` : "";
353
+ throw new Error(`Failed to download ${url}.${statusDetail}`);
354
+ }
355
+
356
+ function sleep(ms: number): Promise<void> {
357
+ return new Promise((resolve) => {
358
+ setTimeout(resolve, ms);
359
+ });
360
+ }
361
+
362
+ export async function selfUpdateCommand(argv: string[]) {
363
+ if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
364
+ printHelp();
365
+ return;
366
+ }
367
+
368
+ let parsed: ParsedArgs;
369
+ try {
370
+ parsed = parseSelfUpdateArgs(argv);
371
+ } catch (err) {
372
+ console.error(err instanceof Error ? err.message : String(err));
373
+ process.exitCode = 1;
374
+ return;
375
+ }
376
+
377
+ const home = homedir();
378
+ const state = await loadInstallState(home);
379
+ const method = detectInstallMethod(state);
380
+
381
+ try {
382
+ if (method === "script-dev") {
383
+ console.log("Detected dev-wrapper install.");
384
+ console.log(
385
+ "Self-update is not automated for dev mode. Update your repo and rerun install:dev."
386
+ );
387
+ return;
388
+ }
389
+ if (method === "npm-binary-cache") {
390
+ await selfUpdateViaPackageManager({
391
+ ...parsed,
392
+ preferredPackageManager:
393
+ process.env.FACULT_INSTALL_PM?.trim() || state?.packageManager,
394
+ });
395
+ return;
396
+ }
397
+ await selfUpdateBinary({
398
+ home,
399
+ state,
400
+ method,
401
+ requestedVersion: parsed.requestedVersion,
402
+ dryRun: parsed.dryRun,
403
+ });
404
+ } catch (err) {
405
+ console.error(err instanceof Error ? err.message : String(err));
406
+ process.exitCode = 1;
407
+ }
408
+ }