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.
- package/LICENSE +21 -0
- package/README.md +383 -0
- package/bin/facult.cjs +302 -0
- package/package.json +78 -0
- package/src/adapters/claude-cli.ts +18 -0
- package/src/adapters/claude-desktop.ts +15 -0
- package/src/adapters/clawdbot.ts +18 -0
- package/src/adapters/codex.ts +19 -0
- package/src/adapters/cursor.ts +18 -0
- package/src/adapters/index.ts +69 -0
- package/src/adapters/mcp.ts +270 -0
- package/src/adapters/reference.ts +9 -0
- package/src/adapters/skills.ts +47 -0
- package/src/adapters/types.ts +42 -0
- package/src/adapters/version.ts +18 -0
- package/src/audit/agent.ts +1071 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/static.ts +1130 -0
- package/src/audit/tui.ts +704 -0
- package/src/audit/types.ts +68 -0
- package/src/audit/update-index.ts +115 -0
- package/src/conflicts.ts +135 -0
- package/src/consolidate-conflict-action.ts +57 -0
- package/src/consolidate.ts +1637 -0
- package/src/enable-disable.ts +349 -0
- package/src/index-builder.ts +562 -0
- package/src/index.ts +589 -0
- package/src/manage.ts +894 -0
- package/src/migrate.ts +272 -0
- package/src/paths.ts +238 -0
- package/src/quarantine.ts +217 -0
- package/src/query.ts +186 -0
- package/src/remote-manifest-integrity.ts +367 -0
- package/src/remote-providers.ts +905 -0
- package/src/remote-source-policy.ts +237 -0
- package/src/remote-sources.ts +162 -0
- package/src/remote-types.ts +136 -0
- package/src/remote.ts +1970 -0
- package/src/scan.ts +2427 -0
- package/src/schema.ts +39 -0
- package/src/self-update.ts +408 -0
- package/src/snippets-cli.ts +293 -0
- package/src/snippets.ts +706 -0
- package/src/source-trust.ts +203 -0
- package/src/trust-list.ts +232 -0
- package/src/trust.ts +170 -0
- package/src/tui.ts +118 -0
- package/src/util/codex-toml.ts +126 -0
- package/src/util/json.ts +32 -0
- package/src/util/skills.ts +55 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
ensureSnippetFile,
|
|
4
|
+
findSnippet,
|
|
5
|
+
listSnippets,
|
|
6
|
+
syncAll,
|
|
7
|
+
validateSnippetMarkerName,
|
|
8
|
+
} from "./snippets";
|
|
9
|
+
|
|
10
|
+
const EDITOR_SPLIT_RE = /\s+/;
|
|
11
|
+
|
|
12
|
+
function printSnippetsHelp() {
|
|
13
|
+
console.log(`facult snippets — sync reusable blocks across config files
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
facult snippets list [--json]
|
|
17
|
+
facult snippets show <name> [--json]
|
|
18
|
+
facult snippets create <name>
|
|
19
|
+
facult snippets edit <name>
|
|
20
|
+
facult snippets sync [--dry-run] [file...]
|
|
21
|
+
|
|
22
|
+
Notes:
|
|
23
|
+
- <name> is the snippet marker name (e.g. codingstyle, global/codingstyle, myproject/context)
|
|
24
|
+
- Unscoped names (e.g. codingstyle) resolve to project snippet first (if in a git repo), then global.
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isSafePathString(p: string): boolean {
|
|
29
|
+
return !p.includes("\0");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function detectProjectForCwd(): Promise<string | null> {
|
|
33
|
+
const cwd = process.cwd();
|
|
34
|
+
let dir = resolve(cwd);
|
|
35
|
+
for (let i = 0; i < 50; i += 1) {
|
|
36
|
+
const git = join(dir, ".git");
|
|
37
|
+
try {
|
|
38
|
+
const st = await Bun.file(git).stat();
|
|
39
|
+
if (st.isDirectory() || st.isFile()) {
|
|
40
|
+
return basename(dir);
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// ignore
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parent = dirname(dir);
|
|
47
|
+
if (parent === dir) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
dir = parent;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function openEditorBestEffort(path: string) {
|
|
56
|
+
const editor = process.env.EDITOR?.trim();
|
|
57
|
+
if (!editor) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Best-effort: split on whitespace. This won't handle complex quoting, but avoids
|
|
61
|
+
// shelling out unnecessarily.
|
|
62
|
+
const parts = editor.split(EDITOR_SPLIT_RE).filter(Boolean);
|
|
63
|
+
const cmd = [...parts, path];
|
|
64
|
+
try {
|
|
65
|
+
Bun.spawnSync({
|
|
66
|
+
cmd,
|
|
67
|
+
stdin: "inherit",
|
|
68
|
+
stdout: "inherit",
|
|
69
|
+
stderr: "inherit",
|
|
70
|
+
});
|
|
71
|
+
} catch {
|
|
72
|
+
// ignore editor failures; user can open the file manually.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseSubcommandArgs(
|
|
77
|
+
args: string[],
|
|
78
|
+
allowedLongFlags: string[]
|
|
79
|
+
): { positionals: string[]; flags: Set<string>; error?: string } {
|
|
80
|
+
const allowed = new Set(allowedLongFlags);
|
|
81
|
+
const flags = new Set<string>();
|
|
82
|
+
const positionals: string[] = [];
|
|
83
|
+
let parseOptions = true;
|
|
84
|
+
|
|
85
|
+
for (const arg of args) {
|
|
86
|
+
if (!arg) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (parseOptions && arg === "--") {
|
|
90
|
+
parseOptions = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (parseOptions && arg.startsWith("--")) {
|
|
94
|
+
if (!allowed.has(arg)) {
|
|
95
|
+
return { positionals, flags, error: `Unknown option: ${arg}` };
|
|
96
|
+
}
|
|
97
|
+
flags.add(arg);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
positionals.push(arg);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { positionals, flags };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function snippetsCommand(argv: string[]) {
|
|
107
|
+
const [sub, ...rest] = argv;
|
|
108
|
+
if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
|
|
109
|
+
printSnippetsHelp();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (sub === "list") {
|
|
114
|
+
const json = rest.includes("--json");
|
|
115
|
+
const snippets = await listSnippets();
|
|
116
|
+
if (json) {
|
|
117
|
+
console.log(JSON.stringify(snippets, null, 2));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!snippets.length) {
|
|
121
|
+
console.log("(no snippets found)");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const s of snippets) {
|
|
125
|
+
console.log(s.marker);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (sub === "show") {
|
|
131
|
+
const parsed = parseSubcommandArgs(rest, ["--json"]);
|
|
132
|
+
if (parsed.error) {
|
|
133
|
+
console.error(parsed.error);
|
|
134
|
+
process.exitCode = 2;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const json = parsed.flags.has("--json");
|
|
138
|
+
const name = parsed.positionals[0];
|
|
139
|
+
if (!name) {
|
|
140
|
+
console.error("snippets show requires a name");
|
|
141
|
+
process.exitCode = 2;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (parsed.positionals.length > 1) {
|
|
145
|
+
console.error("snippets show accepts a single name");
|
|
146
|
+
process.exitCode = 2;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const err = validateSnippetMarkerName(name);
|
|
150
|
+
if (err) {
|
|
151
|
+
console.error(`Invalid snippet name "${name}": ${err}`);
|
|
152
|
+
process.exitCode = 2;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const project = await detectProjectForCwd();
|
|
157
|
+
const snippet = await findSnippet({ marker: name, project });
|
|
158
|
+
if (!snippet) {
|
|
159
|
+
console.error(`Snippet not found: ${name}`);
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (json) {
|
|
165
|
+
console.log(JSON.stringify(snippet, null, 2));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
console.log(snippet.content);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (sub === "create") {
|
|
173
|
+
const parsed = parseSubcommandArgs(rest, []);
|
|
174
|
+
if (parsed.error) {
|
|
175
|
+
console.error(parsed.error);
|
|
176
|
+
process.exitCode = 2;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const name = parsed.positionals[0];
|
|
180
|
+
if (!name) {
|
|
181
|
+
console.error("snippets create requires a name");
|
|
182
|
+
process.exitCode = 2;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (parsed.positionals.length > 1) {
|
|
186
|
+
console.error("snippets create accepts a single name");
|
|
187
|
+
process.exitCode = 2;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const err = validateSnippetMarkerName(name);
|
|
191
|
+
if (err) {
|
|
192
|
+
console.error(`Invalid snippet name "${name}": ${err}`);
|
|
193
|
+
process.exitCode = 2;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { path, created } = await ensureSnippetFile({ marker: name });
|
|
198
|
+
console.log(`${created ? "Created" : "Exists"}: ${path}`);
|
|
199
|
+
openEditorBestEffort(path);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (sub === "edit") {
|
|
204
|
+
const parsed = parseSubcommandArgs(rest, []);
|
|
205
|
+
if (parsed.error) {
|
|
206
|
+
console.error(parsed.error);
|
|
207
|
+
process.exitCode = 2;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const name = parsed.positionals[0];
|
|
211
|
+
if (!name) {
|
|
212
|
+
console.error("snippets edit requires a name");
|
|
213
|
+
process.exitCode = 2;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (parsed.positionals.length > 1) {
|
|
217
|
+
console.error("snippets edit accepts a single name");
|
|
218
|
+
process.exitCode = 2;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const err = validateSnippetMarkerName(name);
|
|
222
|
+
if (err) {
|
|
223
|
+
console.error(`Invalid snippet name "${name}": ${err}`);
|
|
224
|
+
process.exitCode = 2;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const project = await detectProjectForCwd();
|
|
229
|
+
const snippet = await findSnippet({ marker: name, project });
|
|
230
|
+
if (!snippet) {
|
|
231
|
+
console.error(`Snippet not found: ${name}`);
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (!isSafePathString(snippet.path)) {
|
|
236
|
+
console.error(`Ignored unsafe path: ${snippet.path}`);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
console.log(`Editing: ${snippet.path}`);
|
|
241
|
+
openEditorBestEffort(snippet.path);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (sub === "sync") {
|
|
246
|
+
const parsed = parseSubcommandArgs(rest, ["--dry-run"]);
|
|
247
|
+
if (parsed.error) {
|
|
248
|
+
console.error(parsed.error);
|
|
249
|
+
process.exitCode = 2;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const dryRun = parsed.flags.has("--dry-run");
|
|
253
|
+
const files = parsed.positionals;
|
|
254
|
+
|
|
255
|
+
const results = await syncAll({
|
|
256
|
+
dryRun,
|
|
257
|
+
files: files.length ? files : undefined,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
let updatedFiles = 0;
|
|
261
|
+
for (const r of results) {
|
|
262
|
+
if (r.errors.length) {
|
|
263
|
+
console.log(`${r.filePath}: error`);
|
|
264
|
+
for (const e of r.errors) {
|
|
265
|
+
console.log(` - ${e}`);
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (!r.changed) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
updatedFiles += 1;
|
|
273
|
+
console.log(`${dryRun ? "Would update" : "Updated"} ${r.filePath}:`);
|
|
274
|
+
for (const c of r.changes) {
|
|
275
|
+
if (c.status === "updated") {
|
|
276
|
+
const lines =
|
|
277
|
+
typeof c.lines === "number" ? ` (${c.lines} lines)` : "";
|
|
278
|
+
console.log(` ${c.marker} — updated${lines}`);
|
|
279
|
+
} else if (c.status === "not-found") {
|
|
280
|
+
console.log(` ${c.marker} — not found (skipped)`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const suffix = dryRun ? "would be updated" : "updated";
|
|
286
|
+
console.log(`${updatedFiles} files ${suffix}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.error(`Unknown snippets command: ${sub}`);
|
|
291
|
+
printSnippetsHelp();
|
|
292
|
+
process.exitCode = 2;
|
|
293
|
+
}
|