facult 1.1.0 → 1.2.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/README.md +312 -26
- package/package.json +1 -1
- package/src/agents.ts +26 -1
- package/src/ai-state.ts +27 -2
- package/src/ai.ts +1763 -0
- package/src/audit/update-index.ts +1 -0
- package/src/autosync.ts +96 -27
- package/src/builtin.ts +61 -0
- package/src/cli-context.ts +198 -0
- package/src/enable-disable.ts +1 -0
- package/src/global-docs.ts +50 -6
- package/src/graph-query.ts +175 -0
- package/src/graph.ts +119 -0
- package/src/index-builder.ts +1099 -41
- package/src/index.ts +445 -23
- package/src/manage.ts +1904 -187
- package/src/paths.ts +137 -5
- package/src/query.ts +135 -4
- package/src/remote.ts +140 -9
- package/src/trust-list.ts +1 -0
- package/src/trust.ts +1 -0
package/src/paths.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { parseJsonLenient } from "./util/json";
|
|
5
5
|
|
|
6
6
|
export interface FacultConfig {
|
|
@@ -94,6 +94,7 @@ function looksLikeFacultRoot(root: string): boolean {
|
|
|
94
94
|
// Heuristic: treat as a facult store if it contains something we'd create.
|
|
95
95
|
return (
|
|
96
96
|
dirExists(join(root, "rules")) ||
|
|
97
|
+
dirExists(join(root, "instructions")) ||
|
|
97
98
|
dirExists(join(root, "agents")) ||
|
|
98
99
|
dirExists(join(root, "skills")) ||
|
|
99
100
|
dirExists(join(root, "mcp")) ||
|
|
@@ -135,12 +136,108 @@ export function facultStateDir(home: string = defaultHomeDir()): string {
|
|
|
135
136
|
return join(home, ".facult");
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
export function
|
|
139
|
-
|
|
139
|
+
export function projectRootFromAiRoot(
|
|
140
|
+
rootDir: string,
|
|
141
|
+
home: string = defaultHomeDir()
|
|
142
|
+
): string | null {
|
|
143
|
+
const resolved = resolve(rootDir);
|
|
144
|
+
if (resolved === resolve(join(home, ".ai"))) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (resolved === resolve(legacyPreferredRoot(home))) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return resolved.endsWith("/.ai") ? dirname(resolved) : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function projectSlugFromAiRoot(
|
|
154
|
+
rootDir: string,
|
|
155
|
+
home: string = defaultHomeDir()
|
|
156
|
+
): string | null {
|
|
157
|
+
const projectRoot = projectRootFromAiRoot(rootDir, home);
|
|
158
|
+
if (!projectRoot) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const base = basename(projectRoot).trim().toLowerCase();
|
|
162
|
+
const slug = base.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
163
|
+
return slug || "project";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function facultGeneratedStateDir(args?: {
|
|
167
|
+
home?: string;
|
|
168
|
+
rootDir?: string;
|
|
169
|
+
}): string {
|
|
170
|
+
const home = args?.home ?? defaultHomeDir();
|
|
171
|
+
const rootDir = args?.rootDir;
|
|
172
|
+
const projectRoot = rootDir ? projectRootFromAiRoot(rootDir, home) : null;
|
|
173
|
+
return projectRoot ? join(projectRoot, ".facult") : facultStateDir(home);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function facultAiStateDir(
|
|
177
|
+
home: string = defaultHomeDir(),
|
|
178
|
+
rootDir?: string
|
|
179
|
+
): string {
|
|
180
|
+
return join(facultGeneratedStateDir({ home, rootDir }), "ai");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function facultAiIndexPath(
|
|
184
|
+
home: string = defaultHomeDir(),
|
|
185
|
+
rootDir?: string
|
|
186
|
+
): string {
|
|
187
|
+
return join(facultAiStateDir(home, rootDir), "index.json");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function facultAiGraphPath(
|
|
191
|
+
home: string = defaultHomeDir(),
|
|
192
|
+
rootDir?: string
|
|
193
|
+
): string {
|
|
194
|
+
return join(facultAiStateDir(home, rootDir), "graph.json");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function facultAiRuntimeScopeDir(
|
|
198
|
+
home: string = defaultHomeDir(),
|
|
199
|
+
rootDir?: string
|
|
200
|
+
): string {
|
|
201
|
+
const slug = rootDir ? projectSlugFromAiRoot(rootDir, home) : null;
|
|
202
|
+
return slug
|
|
203
|
+
? join(facultStateDir(home), "ai", "projects", slug)
|
|
204
|
+
: join(facultStateDir(home), "ai", "global");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function facultAiJournalPath(
|
|
208
|
+
home: string = defaultHomeDir(),
|
|
209
|
+
rootDir?: string
|
|
210
|
+
): string {
|
|
211
|
+
return join(
|
|
212
|
+
facultAiRuntimeScopeDir(home, rootDir),
|
|
213
|
+
"journal",
|
|
214
|
+
"events.jsonl"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function facultAiWritebackQueuePath(
|
|
219
|
+
home: string = defaultHomeDir(),
|
|
220
|
+
rootDir?: string
|
|
221
|
+
): string {
|
|
222
|
+
return join(
|
|
223
|
+
facultAiRuntimeScopeDir(home, rootDir),
|
|
224
|
+
"writeback",
|
|
225
|
+
"queue.jsonl"
|
|
226
|
+
);
|
|
140
227
|
}
|
|
141
228
|
|
|
142
|
-
export function
|
|
143
|
-
|
|
229
|
+
export function facultAiProposalDir(
|
|
230
|
+
home: string = defaultHomeDir(),
|
|
231
|
+
rootDir?: string
|
|
232
|
+
): string {
|
|
233
|
+
return join(facultAiRuntimeScopeDir(home, rootDir), "evolution", "proposals");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function facultAiDraftDir(
|
|
237
|
+
home: string = defaultHomeDir(),
|
|
238
|
+
rootDir?: string
|
|
239
|
+
): string {
|
|
240
|
+
return join(facultAiRuntimeScopeDir(home, rootDir), "evolution", "drafts");
|
|
144
241
|
}
|
|
145
242
|
|
|
146
243
|
export function facultConfigPath(home: string = defaultHomeDir()): string {
|
|
@@ -268,3 +365,38 @@ export function facultRootDir(home: string = defaultHomeDir()): string {
|
|
|
268
365
|
}
|
|
269
366
|
return preferred;
|
|
270
367
|
}
|
|
368
|
+
|
|
369
|
+
export function findNearestProjectAiRoot(start: string): string | null {
|
|
370
|
+
let current = resolve(start);
|
|
371
|
+
while (true) {
|
|
372
|
+
const candidate = join(current, ".ai");
|
|
373
|
+
if (looksLikeFacultRoot(candidate)) {
|
|
374
|
+
return candidate;
|
|
375
|
+
}
|
|
376
|
+
const parent = dirname(current);
|
|
377
|
+
if (parent === current) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
current = parent;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function facultContextRootDir(args?: {
|
|
385
|
+
home?: string;
|
|
386
|
+
cwd?: string;
|
|
387
|
+
}): string {
|
|
388
|
+
const home = args?.home ?? defaultHomeDir();
|
|
389
|
+
const envRoot = process.env.FACULT_ROOT_DIR?.trim();
|
|
390
|
+
if (envRoot) {
|
|
391
|
+
const resolved = resolvePath(envRoot, home);
|
|
392
|
+
return isSafePathString(resolved) ? resolved : join(home, ".ai");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const cwd = args?.cwd?.trim() || process.cwd();
|
|
396
|
+
const projectRoot = findNearestProjectAiRoot(cwd);
|
|
397
|
+
if (projectRoot) {
|
|
398
|
+
return projectRoot;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return facultRootDir(home);
|
|
402
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { ensureAiIndexPath } from "./ai-state";
|
|
3
|
+
import type { AssetScope, AssetSourceKind } from "./graph";
|
|
3
4
|
import type {
|
|
4
5
|
AgentEntry,
|
|
5
6
|
FacultIndex,
|
|
7
|
+
InstructionEntry,
|
|
6
8
|
McpEntry,
|
|
7
9
|
SkillEntry,
|
|
8
10
|
SnippetEntry,
|
|
9
11
|
} from "./index-builder";
|
|
10
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
facultAiIndexPath,
|
|
14
|
+
facultContextRootDir,
|
|
15
|
+
facultRootDir,
|
|
16
|
+
} from "./paths";
|
|
11
17
|
import { applyOrgTrustList } from "./trust-list";
|
|
12
18
|
|
|
13
19
|
export interface QueryFilters {
|
|
@@ -23,6 +29,10 @@ export interface QueryFilters {
|
|
|
23
29
|
tags?: string[];
|
|
24
30
|
/** Full-text search query (case-insensitive). */
|
|
25
31
|
text?: string;
|
|
32
|
+
/** Only include entries from a specific source layer. */
|
|
33
|
+
sourceKind?: AssetSourceKind;
|
|
34
|
+
/** Only include entries from a specific asset scope. */
|
|
35
|
+
scope?: AssetScope;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
interface IndexEntry {
|
|
@@ -32,6 +42,18 @@ interface IndexEntry {
|
|
|
32
42
|
enabledFor?: string[];
|
|
33
43
|
trusted?: boolean;
|
|
34
44
|
auditStatus?: string;
|
|
45
|
+
sourceKind?: AssetSourceKind;
|
|
46
|
+
scope?: AssetScope;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CapabilityMatch {
|
|
50
|
+
kind: "skills" | "mcp" | "agents" | "snippets" | "instructions";
|
|
51
|
+
name: string;
|
|
52
|
+
path: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
tags?: string[];
|
|
55
|
+
sourceKind?: string;
|
|
56
|
+
scope?: string;
|
|
35
57
|
}
|
|
36
58
|
|
|
37
59
|
const WHITESPACE_RE = /\s+/;
|
|
@@ -46,7 +68,7 @@ function matchesEnabledFor(entry: IndexEntry, tool?: string): boolean {
|
|
|
46
68
|
}
|
|
47
69
|
const enabledFor = entry.enabledFor;
|
|
48
70
|
if (!Array.isArray(enabledFor)) {
|
|
49
|
-
return
|
|
71
|
+
return true;
|
|
50
72
|
}
|
|
51
73
|
const target = normalizeText(tool);
|
|
52
74
|
return enabledFor.some((t) => normalizeText(t) === target);
|
|
@@ -99,16 +121,40 @@ function matchesText(entry: IndexEntry, text?: string): boolean {
|
|
|
99
121
|
return terms.every((term) => haystack.includes(term.toLowerCase()));
|
|
100
122
|
}
|
|
101
123
|
|
|
124
|
+
function matchesSourceKind(
|
|
125
|
+
entry: IndexEntry,
|
|
126
|
+
sourceKind?: AssetSourceKind
|
|
127
|
+
): boolean {
|
|
128
|
+
if (!sourceKind) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return entry.sourceKind === sourceKind;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function matchesScope(entry: IndexEntry, scope?: AssetScope): boolean {
|
|
135
|
+
if (!scope) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return entry.scope === scope;
|
|
139
|
+
}
|
|
140
|
+
|
|
102
141
|
/** Return the canonical facult root directory. */
|
|
103
142
|
export function facultRootDirPath(home: string = homedir()): string {
|
|
104
143
|
return facultRootDir(home);
|
|
105
144
|
}
|
|
106
145
|
|
|
146
|
+
export function facultContextRootDirPath(home: string = homedir()): string {
|
|
147
|
+
return facultContextRootDir({ home, cwd: process.cwd() });
|
|
148
|
+
}
|
|
149
|
+
|
|
107
150
|
/**
|
|
108
151
|
* Return the path to the facult index.json file.
|
|
109
152
|
*/
|
|
110
153
|
export function facultIndexPath(home: string = homedir()): string {
|
|
111
|
-
return facultAiIndexPath(
|
|
154
|
+
return facultAiIndexPath(
|
|
155
|
+
home,
|
|
156
|
+
facultContextRootDir({ home, cwd: process.cwd() })
|
|
157
|
+
);
|
|
112
158
|
}
|
|
113
159
|
|
|
114
160
|
/**
|
|
@@ -125,7 +171,9 @@ export async function loadIndex(opts?: {
|
|
|
125
171
|
if (!resolvedHome) {
|
|
126
172
|
throw new Error("HOME is not set.");
|
|
127
173
|
}
|
|
128
|
-
const rootDir =
|
|
174
|
+
const rootDir =
|
|
175
|
+
opts?.rootDir ??
|
|
176
|
+
facultContextRootDir({ home: resolvedHome, cwd: process.cwd() });
|
|
129
177
|
const { path: indexPath } = await ensureAiIndexPath({
|
|
130
178
|
homeDir: resolvedHome,
|
|
131
179
|
rootDir,
|
|
@@ -180,6 +228,87 @@ export function filterSnippets(
|
|
|
180
228
|
return filterEntries(entries, filters);
|
|
181
229
|
}
|
|
182
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Filter instruction entries using query filters.
|
|
233
|
+
*/
|
|
234
|
+
export function filterInstructions(
|
|
235
|
+
entries: Record<string, InstructionEntry>,
|
|
236
|
+
filters?: QueryFilters
|
|
237
|
+
): InstructionEntry[] {
|
|
238
|
+
return filterEntries(entries, filters);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function findCapabilities(
|
|
242
|
+
index: FacultIndex,
|
|
243
|
+
filters: Pick<QueryFilters, "text" | "sourceKind" | "scope">
|
|
244
|
+
): CapabilityMatch[] {
|
|
245
|
+
const results: CapabilityMatch[] = [];
|
|
246
|
+
|
|
247
|
+
for (const entry of filterSkills(index.skills, filters)) {
|
|
248
|
+
results.push({
|
|
249
|
+
kind: "skills",
|
|
250
|
+
name: entry.name,
|
|
251
|
+
path: entry.path,
|
|
252
|
+
description: entry.description,
|
|
253
|
+
tags: entry.tags,
|
|
254
|
+
sourceKind: entry.sourceKind,
|
|
255
|
+
scope: entry.scope,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const entry of filterMcp(index.mcp?.servers ?? {}, filters)) {
|
|
260
|
+
results.push({
|
|
261
|
+
kind: "mcp",
|
|
262
|
+
name: entry.name,
|
|
263
|
+
path: entry.path,
|
|
264
|
+
sourceKind: entry.sourceKind,
|
|
265
|
+
scope: entry.scope,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const entry of filterAgents(index.agents ?? {}, filters)) {
|
|
270
|
+
results.push({
|
|
271
|
+
kind: "agents",
|
|
272
|
+
name: entry.name,
|
|
273
|
+
path: entry.path,
|
|
274
|
+
description: entry.description,
|
|
275
|
+
sourceKind: entry.sourceKind,
|
|
276
|
+
scope: entry.scope,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const entry of filterSnippets(index.snippets ?? {}, filters)) {
|
|
281
|
+
results.push({
|
|
282
|
+
kind: "snippets",
|
|
283
|
+
name: entry.name,
|
|
284
|
+
path: entry.path,
|
|
285
|
+
description: entry.description,
|
|
286
|
+
tags: entry.tags,
|
|
287
|
+
sourceKind: entry.sourceKind,
|
|
288
|
+
scope: entry.scope,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (const entry of filterInstructions(index.instructions ?? {}, filters)) {
|
|
293
|
+
results.push({
|
|
294
|
+
kind: "instructions",
|
|
295
|
+
name: entry.name,
|
|
296
|
+
path: entry.path,
|
|
297
|
+
description: entry.description,
|
|
298
|
+
tags: entry.tags,
|
|
299
|
+
sourceKind: entry.sourceKind,
|
|
300
|
+
scope: entry.scope,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return results.sort((a, b) => {
|
|
305
|
+
if (a.kind !== b.kind) {
|
|
306
|
+
return a.kind.localeCompare(b.kind);
|
|
307
|
+
}
|
|
308
|
+
return a.name.localeCompare(b.name);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
183
312
|
function filterEntries<T extends IndexEntry>(
|
|
184
313
|
entries: Record<string, T>,
|
|
185
314
|
filters?: QueryFilters
|
|
@@ -189,6 +318,8 @@ function filterEntries<T extends IndexEntry>(
|
|
|
189
318
|
.filter((entry) => matchesUntrusted(entry, filters?.untrusted))
|
|
190
319
|
.filter((entry) => matchesFlagged(entry, filters?.flagged))
|
|
191
320
|
.filter((entry) => matchesPending(entry, filters?.pending))
|
|
321
|
+
.filter((entry) => matchesSourceKind(entry, filters?.sourceKind))
|
|
322
|
+
.filter((entry) => matchesScope(entry, filters?.scope))
|
|
192
323
|
.filter((entry) => matchesTags(entry, filters?.tags))
|
|
193
324
|
.filter((entry) => matchesText(entry, filters?.text))
|
|
194
325
|
.sort((a, b) => a.name.localeCompare(b.name));
|
package/src/remote.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, rm } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { buildIndex } from "./index-builder";
|
|
5
6
|
import { facultRootDir } from "./paths";
|
|
6
7
|
import {
|
|
@@ -320,6 +321,100 @@ function renderTemplate(text: string, values: Record<string, string>): string {
|
|
|
320
321
|
return out;
|
|
321
322
|
}
|
|
322
323
|
|
|
324
|
+
function builtinPackRoot(packName: string): string {
|
|
325
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
326
|
+
return join(here, "..", "assets", "packs", packName);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function pathExists(pathValue: string): Promise<boolean> {
|
|
330
|
+
try {
|
|
331
|
+
await Bun.file(pathValue).stat();
|
|
332
|
+
return true;
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function listFilesRecursive(rootDir: string): Promise<string[]> {
|
|
339
|
+
const out: string[] = [];
|
|
340
|
+
const stack = [rootDir];
|
|
341
|
+
while (stack.length) {
|
|
342
|
+
const current = stack.pop();
|
|
343
|
+
if (!current) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const entries = await readdir(current, { withFileTypes: true }).catch(
|
|
347
|
+
() => [] as import("node:fs").Dirent[]
|
|
348
|
+
);
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
const fullPath = join(current, entry.name);
|
|
351
|
+
if (entry.isDirectory()) {
|
|
352
|
+
stack.push(fullPath);
|
|
353
|
+
} else if (entry.isFile()) {
|
|
354
|
+
out.push(fullPath);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return out.sort();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function scaffoldBuiltinProjectAiPack(args: {
|
|
362
|
+
cwd?: string;
|
|
363
|
+
homeDir?: string;
|
|
364
|
+
dryRun?: boolean;
|
|
365
|
+
force?: boolean;
|
|
366
|
+
}): Promise<InstallResult> {
|
|
367
|
+
const cwd = resolve(args.cwd ?? process.cwd());
|
|
368
|
+
const rootDir = join(cwd, ".ai");
|
|
369
|
+
const packRoot = builtinPackRoot("facult-operating-model");
|
|
370
|
+
const files = await listFilesRecursive(packRoot);
|
|
371
|
+
const changedPaths: string[] = [];
|
|
372
|
+
|
|
373
|
+
for (const sourcePath of files) {
|
|
374
|
+
const relPath = relative(packRoot, sourcePath);
|
|
375
|
+
if (!relPath || relPath.startsWith("..")) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const targetPath = join(rootDir, relPath);
|
|
379
|
+
const exists = await pathExists(targetPath);
|
|
380
|
+
if (exists && !args.force) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
changedPaths.push(targetPath);
|
|
384
|
+
if (!args.dryRun) {
|
|
385
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
386
|
+
await Bun.write(targetPath, await Bun.file(sourcePath).text());
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const configPath = join(rootDir, "config.toml");
|
|
391
|
+
if (!(await pathExists(configPath)) || args.force) {
|
|
392
|
+
changedPaths.push(configPath);
|
|
393
|
+
if (!args.dryRun) {
|
|
394
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
395
|
+
await Bun.write(configPath, "version = 1\n");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!args.dryRun) {
|
|
400
|
+
await buildIndex({
|
|
401
|
+
homeDir: args.homeDir,
|
|
402
|
+
rootDir,
|
|
403
|
+
force: false,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
ref: `${BUILTIN_INDEX_NAME}:facult-operating-model`,
|
|
409
|
+
type: "skill",
|
|
410
|
+
installedAs: "project-ai",
|
|
411
|
+
path: rootDir,
|
|
412
|
+
sourceTrustLevel: "trusted",
|
|
413
|
+
dryRun: Boolean(args.dryRun),
|
|
414
|
+
changedPaths: uniqueSorted(changedPaths),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
323
418
|
function compareVersions(a: string, b: string): number {
|
|
324
419
|
const aTokens = (a.match(VERSION_TOKEN_RE) ?? []).map((t) => t.toLowerCase());
|
|
325
420
|
const bTokens = (b.match(VERSION_TOKEN_RE) ?? []).map((t) => t.toLowerCase());
|
|
@@ -1589,6 +1684,7 @@ Usage:
|
|
|
1589
1684
|
facult templates init snippet <marker> [--force] [--dry-run]
|
|
1590
1685
|
facult templates init agents [--force] [--dry-run]
|
|
1591
1686
|
facult templates init claude [--force] [--dry-run]
|
|
1687
|
+
facult templates init project-ai [--force] [--dry-run]
|
|
1592
1688
|
|
|
1593
1689
|
Notes:
|
|
1594
1690
|
- Templates are powered by the builtin remote index (${BUILTIN_INDEX_NAME}).
|
|
@@ -1872,13 +1968,23 @@ export async function templatesCommand(
|
|
|
1872
1968
|
}
|
|
1873
1969
|
if (sub === "list") {
|
|
1874
1970
|
const json = rest.includes("--json");
|
|
1875
|
-
const rows =
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1971
|
+
const rows = [
|
|
1972
|
+
...BUILTIN_MANIFEST.items.map((item) => ({
|
|
1973
|
+
id: item.id,
|
|
1974
|
+
type: item.type,
|
|
1975
|
+
title: item.title ?? "",
|
|
1976
|
+
description: item.description ?? "",
|
|
1977
|
+
version: item.version ?? "",
|
|
1978
|
+
})),
|
|
1979
|
+
{
|
|
1980
|
+
id: "project-ai",
|
|
1981
|
+
type: "pack",
|
|
1982
|
+
title: "Project AI Pack",
|
|
1983
|
+
description:
|
|
1984
|
+
"Seed a repo-local .ai with the built-in Facult operating-model pack.",
|
|
1985
|
+
version: "1.0.0",
|
|
1986
|
+
},
|
|
1987
|
+
];
|
|
1882
1988
|
if (json) {
|
|
1883
1989
|
console.log(JSON.stringify(rows, null, 2));
|
|
1884
1990
|
return;
|
|
@@ -1897,7 +2003,7 @@ export async function templatesCommand(
|
|
|
1897
2003
|
const [kind, ...args] = rest;
|
|
1898
2004
|
if (!kind) {
|
|
1899
2005
|
console.error(
|
|
1900
|
-
"templates init requires a kind (skill|mcp|snippet|agents|claude)"
|
|
2006
|
+
"templates init requires a kind (skill|mcp|snippet|agents|claude|project-ai)"
|
|
1901
2007
|
);
|
|
1902
2008
|
process.exitCode = 2;
|
|
1903
2009
|
return;
|
|
@@ -1907,6 +2013,31 @@ export async function templatesCommand(
|
|
|
1907
2013
|
const json = args.includes("--json");
|
|
1908
2014
|
const positional = args.filter((a) => a && !a.startsWith("-"));
|
|
1909
2015
|
|
|
2016
|
+
if (kind === "project-ai") {
|
|
2017
|
+
try {
|
|
2018
|
+
const result = await scaffoldBuiltinProjectAiPack({
|
|
2019
|
+
cwd: ctx.cwd,
|
|
2020
|
+
homeDir: ctx.homeDir,
|
|
2021
|
+
dryRun,
|
|
2022
|
+
force,
|
|
2023
|
+
});
|
|
2024
|
+
if (json) {
|
|
2025
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
const action = dryRun ? "Would scaffold" : "Scaffolded";
|
|
2029
|
+
console.log(`${action} ${kind} template as ${result.installedAs}`);
|
|
2030
|
+
for (const path of result.changedPaths) {
|
|
2031
|
+
console.log(` - ${path}`);
|
|
2032
|
+
}
|
|
2033
|
+
return;
|
|
2034
|
+
} catch (err) {
|
|
2035
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
2036
|
+
process.exitCode = 1;
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
1910
2041
|
let ref = "";
|
|
1911
2042
|
let as: string | undefined;
|
|
1912
2043
|
if (kind === "skill") {
|
package/src/trust-list.ts
CHANGED