@contextforge/core 0.1.2 → 0.1.5
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/dist/index.d.ts +297 -131
- package/dist/index.js +976 -422
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -144,6 +144,7 @@ async function detectProject(root) {
|
|
|
144
144
|
const vitest = vitestConfig || hasPackage(packageJson, "vitest");
|
|
145
145
|
const jest = jestConfig || hasPackage(packageJson, "jest");
|
|
146
146
|
const playwright = playwrightConfig || hasPackage(packageJson, "@playwright/test") || hasPackage(packageJson, "playwright");
|
|
147
|
+
const supabase = hasPackage(packageJson, "@supabase/supabase-js") || hasPackage(packageJson, "@supabase/ssr") || hasPackage(packageJson, "@supabase/auth-helpers-nextjs");
|
|
147
148
|
return {
|
|
148
149
|
root: resolvedRoot,
|
|
149
150
|
packageManager,
|
|
@@ -154,6 +155,9 @@ async function detectProject(root) {
|
|
|
154
155
|
shadcn
|
|
155
156
|
},
|
|
156
157
|
database,
|
|
158
|
+
services: {
|
|
159
|
+
supabase
|
|
160
|
+
},
|
|
157
161
|
testing: {
|
|
158
162
|
vitest,
|
|
159
163
|
jest,
|
|
@@ -165,43 +169,71 @@ async function detectProject(root) {
|
|
|
165
169
|
|
|
166
170
|
// src/registry/registrySchema.ts
|
|
167
171
|
import { z } from "zod";
|
|
168
|
-
var
|
|
172
|
+
var PackFileTypeSchema = z.enum([
|
|
173
|
+
"rules",
|
|
174
|
+
"agents",
|
|
175
|
+
"claude",
|
|
176
|
+
"skill",
|
|
177
|
+
"cursor",
|
|
178
|
+
"copilot"
|
|
179
|
+
]);
|
|
180
|
+
var RegistryPackSourceSchema = z.object({
|
|
181
|
+
provider: z.string().optional(),
|
|
182
|
+
license: z.string().optional()
|
|
183
|
+
}).catchall(z.unknown()).optional();
|
|
184
|
+
var RegistryPackSummarySchema = z.object({
|
|
169
185
|
name: z.string().min(1),
|
|
186
|
+
title: z.string().min(1),
|
|
187
|
+
topic: z.string().min(1),
|
|
188
|
+
description: z.string().default(""),
|
|
189
|
+
path: z.string().min(1),
|
|
190
|
+
source: RegistryPackSourceSchema
|
|
191
|
+
});
|
|
192
|
+
var RegistryIndexSchema = z.object({
|
|
193
|
+
name: z.string().optional(),
|
|
170
194
|
version: z.string().optional(),
|
|
195
|
+
schemaVersion: z.string().optional(),
|
|
196
|
+
description: z.string().optional(),
|
|
197
|
+
topics: z.array(z.string()).default([]),
|
|
198
|
+
packs: z.array(RegistryPackSummarySchema)
|
|
199
|
+
});
|
|
200
|
+
var PackFileSchema = z.object({
|
|
201
|
+
type: PackFileTypeSchema,
|
|
202
|
+
path: z.string().min(1),
|
|
203
|
+
output: z.string().optional(),
|
|
204
|
+
mode: z.string().optional()
|
|
205
|
+
});
|
|
206
|
+
var PackOutputsSchema = z.object({
|
|
207
|
+
globalRules: z.boolean().optional(),
|
|
208
|
+
agentsInstruction: z.boolean().optional(),
|
|
209
|
+
claudeInstruction: z.boolean().optional(),
|
|
210
|
+
skill: z.boolean().optional(),
|
|
211
|
+
cursorRule: z.boolean().optional(),
|
|
212
|
+
copilotInstruction: z.boolean().optional()
|
|
213
|
+
}).default({}).transform((outputs) => ({
|
|
214
|
+
globalRules: outputs.globalRules ?? true,
|
|
215
|
+
agentsInstruction: outputs.agentsInstruction ?? true,
|
|
216
|
+
claudeInstruction: outputs.claudeInstruction ?? true,
|
|
217
|
+
skill: outputs.skill ?? true,
|
|
218
|
+
cursorRule: outputs.cursorRule ?? true,
|
|
219
|
+
copilotInstruction: outputs.copilotInstruction ?? true
|
|
220
|
+
}));
|
|
221
|
+
var PackManifestSchema = z.object({
|
|
222
|
+
name: z.string().min(1),
|
|
171
223
|
title: z.string().min(1),
|
|
172
|
-
|
|
173
|
-
|
|
224
|
+
version: z.string().default("0.0.0"),
|
|
225
|
+
topic: z.string().min(1),
|
|
226
|
+
description: z.string().default(""),
|
|
227
|
+
classification: z.enum(["always-referenced", "task-triggered", "permission-gated"]).default("task-triggered"),
|
|
228
|
+
source: RegistryPackSourceSchema,
|
|
174
229
|
detect: z.object({
|
|
175
230
|
files: z.array(z.string()).optional(),
|
|
176
231
|
packages: z.array(z.string()).optional()
|
|
177
232
|
}).optional(),
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
skill: z.boolean().default(false),
|
|
181
|
-
cursorRule: z.boolean().default(true),
|
|
182
|
-
copilotInstruction: z.boolean().default(true)
|
|
183
|
-
})
|
|
184
|
-
});
|
|
185
|
-
var RemotePackFilesSchema = z.object({
|
|
186
|
-
pack: z.string().optional(),
|
|
187
|
-
rules: z.string().optional(),
|
|
188
|
-
skill: z.string().optional(),
|
|
189
|
-
cursor: z.string().optional(),
|
|
190
|
-
copilot: z.string().optional()
|
|
191
|
-
});
|
|
192
|
-
var RemotePackEntrySchema = z.object({
|
|
193
|
-
name: z.string().min(1),
|
|
194
|
-
version: z.string().optional(),
|
|
195
|
-
baseUrl: z.string().optional(),
|
|
196
|
-
pack: PackSchema.optional(),
|
|
197
|
-
files: RemotePackFilesSchema.optional()
|
|
198
|
-
}).refine((entry) => Boolean(entry.baseUrl || entry.pack || entry.files?.pack), {
|
|
199
|
-
message: "Remote pack entry must include baseUrl, inline pack metadata, or files.pack"
|
|
200
|
-
});
|
|
201
|
-
var RemoteRegistryIndexSchema = z.object({
|
|
202
|
-
version: z.string().default("1"),
|
|
203
|
-
packs: z.array(RemotePackEntrySchema)
|
|
233
|
+
files: z.array(PackFileSchema).default([]),
|
|
234
|
+
outputs: PackOutputsSchema
|
|
204
235
|
});
|
|
236
|
+
var PackSchema = PackManifestSchema;
|
|
205
237
|
|
|
206
238
|
// src/registry/loadRegistry.ts
|
|
207
239
|
import path8 from "path";
|
|
@@ -209,117 +241,103 @@ import path8 from "path";
|
|
|
209
241
|
// src/registry/localRegistry.ts
|
|
210
242
|
import path7 from "path";
|
|
211
243
|
import fs7 from "fs-extra";
|
|
212
|
-
async function
|
|
213
|
-
|
|
244
|
+
async function readOptional(filePath) {
|
|
245
|
+
return await fs7.pathExists(filePath) ? fs7.readFile(filePath, "utf8") : void 0;
|
|
246
|
+
}
|
|
247
|
+
function normalizePackFilePath(file) {
|
|
248
|
+
return file.path.split(/[\\/]/u).join(path7.sep);
|
|
249
|
+
}
|
|
250
|
+
async function loadCachedPack(packRoot2) {
|
|
251
|
+
const manifestPath = path7.join(packRoot2, "pack.json");
|
|
252
|
+
if (!await fs7.pathExists(manifestPath)) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const manifest = PackManifestSchema.parse(await fs7.readJson(manifestPath));
|
|
256
|
+
const files = {};
|
|
257
|
+
for (const file of manifest.files) {
|
|
258
|
+
const content = await readOptional(path7.join(packRoot2, normalizePackFilePath(file)));
|
|
259
|
+
if (content !== void 0) {
|
|
260
|
+
files[file.type] = content;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
manifest,
|
|
265
|
+
files
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
async function loadCachedPacks(packsRoot) {
|
|
269
|
+
if (!await fs7.pathExists(packsRoot)) {
|
|
214
270
|
return [];
|
|
215
271
|
}
|
|
216
|
-
const entries = await fs7.readdir(
|
|
272
|
+
const entries = await fs7.readdir(packsRoot, { withFileTypes: true });
|
|
217
273
|
const packs = [];
|
|
218
274
|
for (const entry of entries) {
|
|
219
275
|
if (!entry.isDirectory()) {
|
|
220
276
|
continue;
|
|
221
277
|
}
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
const parsed = PackSchema.parse(await fs7.readJson(packPath));
|
|
228
|
-
const readOptional = async (fileName) => {
|
|
229
|
-
const filePath = path7.join(directory, fileName);
|
|
230
|
-
return await fs7.pathExists(filePath) ? fs7.readFile(filePath, "utf8") : void 0;
|
|
231
|
-
};
|
|
232
|
-
const rules = await readOptional("rules.md");
|
|
233
|
-
if (!rules) {
|
|
234
|
-
throw new Error(`Pack "${parsed.name}" is missing rules.md`);
|
|
278
|
+
const pack = await loadCachedPack(path7.join(packsRoot, entry.name));
|
|
279
|
+
if (pack) {
|
|
280
|
+
packs.push(pack);
|
|
235
281
|
}
|
|
236
|
-
packs.push({
|
|
237
|
-
...parsed,
|
|
238
|
-
directory,
|
|
239
|
-
source,
|
|
240
|
-
files: {
|
|
241
|
-
rules,
|
|
242
|
-
skill: await readOptional("skill.md"),
|
|
243
|
-
cursor: await readOptional("cursor.mdc"),
|
|
244
|
-
copilot: await readOptional("copilot.md")
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
282
|
}
|
|
248
|
-
return packs;
|
|
283
|
+
return packs.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
|
249
284
|
}
|
|
250
285
|
|
|
251
286
|
// src/registry/remoteRegistry.ts
|
|
252
287
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
253
|
-
async function fetchText(url,
|
|
288
|
+
async function fetchText(url, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
254
289
|
const controller = new AbortController();
|
|
255
290
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
256
291
|
try {
|
|
257
292
|
const response = await fetch(url, { signal: controller.signal });
|
|
258
293
|
if (!response.ok) {
|
|
259
|
-
if (!required && response.status === 404) {
|
|
260
|
-
return void 0;
|
|
261
|
-
}
|
|
262
294
|
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
263
295
|
}
|
|
264
296
|
return response.text();
|
|
265
|
-
} catch (error) {
|
|
266
|
-
if (!required) {
|
|
267
|
-
return void 0;
|
|
268
|
-
}
|
|
269
|
-
throw error;
|
|
270
297
|
} finally {
|
|
271
298
|
clearTimeout(timeout);
|
|
272
299
|
}
|
|
273
300
|
}
|
|
274
|
-
function
|
|
275
|
-
return
|
|
301
|
+
function resolvePackUrl(registryUrl, packPath) {
|
|
302
|
+
return new URL(packPath, registryUrl).toString();
|
|
276
303
|
}
|
|
277
|
-
function
|
|
278
|
-
return
|
|
304
|
+
function resolvePackFileUrl(packUrl, filePath) {
|
|
305
|
+
return new URL(filePath, packUrl).toString();
|
|
279
306
|
}
|
|
280
|
-
async function
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
resolveUrl(entry.files?.cursor, registryUrl) ?? defaultPackFile(baseUrl, "cursor.mdc") ?? "",
|
|
311
|
-
false,
|
|
312
|
-
timeoutMs
|
|
313
|
-
),
|
|
314
|
-
copilot: await fetchText(
|
|
315
|
-
resolveUrl(entry.files?.copilot, registryUrl) ?? defaultPackFile(baseUrl, "copilot.md") ?? "",
|
|
316
|
-
false,
|
|
317
|
-
timeoutMs
|
|
318
|
-
)
|
|
319
|
-
}
|
|
320
|
-
});
|
|
307
|
+
async function fetchRegistry(registryUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
308
|
+
const content = await fetchText(registryUrl, timeoutMs);
|
|
309
|
+
return RegistryIndexSchema.parse(JSON.parse(content));
|
|
310
|
+
}
|
|
311
|
+
async function fetchPackManifest(packUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
312
|
+
const content = await fetchText(packUrl, timeoutMs);
|
|
313
|
+
return PackManifestSchema.parse(JSON.parse(content));
|
|
314
|
+
}
|
|
315
|
+
function findPackSummary(registry, packName) {
|
|
316
|
+
return registry.packs.find((pack) => pack.name === packName);
|
|
317
|
+
}
|
|
318
|
+
async function fetchPackFile(packUrl, file, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
319
|
+
return fetchText(resolvePackFileUrl(packUrl, file.path), timeoutMs);
|
|
320
|
+
}
|
|
321
|
+
function sortRegistryPacks(registry) {
|
|
322
|
+
return [...registry.packs].sort((a, b) => a.topic.localeCompare(b.topic) || a.name.localeCompare(b.name));
|
|
323
|
+
}
|
|
324
|
+
function listRegistryPacks(input) {
|
|
325
|
+
if (typeof input === "string") {
|
|
326
|
+
return fetchRegistry(input).then(sortRegistryPacks);
|
|
327
|
+
}
|
|
328
|
+
return sortRegistryPacks(input);
|
|
329
|
+
}
|
|
330
|
+
function searchRegistryPacks(input, query) {
|
|
331
|
+
if (typeof input === "string") {
|
|
332
|
+
return fetchRegistry(input).then((registry) => searchRegistryPacks(registry, query));
|
|
333
|
+
}
|
|
334
|
+
const normalized = query.trim().toLowerCase();
|
|
335
|
+
if (!normalized) {
|
|
336
|
+
return sortRegistryPacks(input);
|
|
321
337
|
}
|
|
322
|
-
return
|
|
338
|
+
return sortRegistryPacks(input).filter(
|
|
339
|
+
(pack) => [pack.name, pack.title, pack.description, pack.topic].join(" ").toLowerCase().includes(normalized)
|
|
340
|
+
);
|
|
323
341
|
}
|
|
324
342
|
|
|
325
343
|
// src/registry/loadRegistry.ts
|
|
@@ -327,56 +345,99 @@ var OFFICIAL_REGISTRY_SOURCE = "official";
|
|
|
327
345
|
var OFFICIAL_REGISTRY_URL = "https://registry.contextforge.org/index.json";
|
|
328
346
|
var DEFAULT_REGISTRY_SOURCES = [OFFICIAL_REGISTRY_SOURCE];
|
|
329
347
|
var PROJECT_PACK_CACHE = ".contextforge/packs";
|
|
330
|
-
function
|
|
331
|
-
|
|
332
|
-
for (const pack of packs) {
|
|
333
|
-
if (!byName.has(pack.name)) {
|
|
334
|
-
byName.set(pack.name, pack);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
338
|
-
}
|
|
339
|
-
async function loadRegistrySource(source, timeoutMs) {
|
|
340
|
-
if (source === OFFICIAL_REGISTRY_SOURCE) {
|
|
341
|
-
try {
|
|
342
|
-
return await loadRemoteRegistry(OFFICIAL_REGISTRY_URL, timeoutMs ?? 1500);
|
|
343
|
-
} catch {
|
|
344
|
-
return [];
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
return loadRemoteRegistry(source, timeoutMs);
|
|
348
|
+
function sourceToUrl(source) {
|
|
349
|
+
return source === OFFICIAL_REGISTRY_SOURCE ? OFFICIAL_REGISTRY_URL : source;
|
|
348
350
|
}
|
|
349
351
|
async function loadRegistry(input = {}) {
|
|
350
352
|
if (typeof input === "string") {
|
|
351
|
-
return
|
|
352
|
-
}
|
|
353
|
-
const sources = input.sources ?? DEFAULT_REGISTRY_SOURCES;
|
|
354
|
-
const packs = [];
|
|
355
|
-
if (input.root) {
|
|
356
|
-
packs.push(...await loadLocalRegistry(path8.join(input.root, PROJECT_PACK_CACHE), "project-cache"));
|
|
353
|
+
return fetchRegistry(input);
|
|
357
354
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
355
|
+
const source = input.sources?.[0] ?? OFFICIAL_REGISTRY_SOURCE;
|
|
356
|
+
return fetchRegistry(sourceToUrl(source), input.timeoutMs);
|
|
357
|
+
}
|
|
358
|
+
function registrySourceToUrl(source) {
|
|
359
|
+
return sourceToUrl(source ?? OFFICIAL_REGISTRY_SOURCE);
|
|
360
|
+
}
|
|
361
|
+
async function loadRemotePack(registryUrl, summary, timeoutMs) {
|
|
362
|
+
const packUrl = resolvePackUrl(registryUrl, summary.path);
|
|
363
|
+
const manifest = await fetchPackManifest(packUrl, timeoutMs);
|
|
364
|
+
return {
|
|
365
|
+
manifest,
|
|
366
|
+
summary,
|
|
367
|
+
packUrl,
|
|
368
|
+
files: {}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
async function loadProjectPacks(root) {
|
|
372
|
+
return loadCachedPacks(path8.join(root, PROJECT_PACK_CACHE));
|
|
362
373
|
}
|
|
363
374
|
|
|
364
375
|
// src/registry/resolvePack.ts
|
|
365
376
|
import path9 from "path";
|
|
366
377
|
import fg5 from "fast-glob";
|
|
367
378
|
import fs8 from "fs-extra";
|
|
379
|
+
|
|
380
|
+
// src/config/configSchema.ts
|
|
381
|
+
import { z as z2 } from "zod";
|
|
382
|
+
var ToolSchema = z2.enum(["codex", "claude", "cursor", "copilot"]);
|
|
383
|
+
var DEFAULT_TOOLS = ["codex", "claude", "cursor", "copilot"];
|
|
384
|
+
var DEFAULT_CORE_PACKS = [
|
|
385
|
+
"verification-before-completion",
|
|
386
|
+
"systematic-debugging",
|
|
387
|
+
"code-review",
|
|
388
|
+
"git-workflow",
|
|
389
|
+
"dependency-management",
|
|
390
|
+
"diataxis-docs"
|
|
391
|
+
];
|
|
392
|
+
var CurrentConfigSchema = z2.object({
|
|
393
|
+
version: z2.string().default("0.1.0"),
|
|
394
|
+
registry: z2.string().default(OFFICIAL_REGISTRY_URL),
|
|
395
|
+
tools: z2.array(ToolSchema).default(DEFAULT_TOOLS),
|
|
396
|
+
installedPacks: z2.array(z2.string()).default([]),
|
|
397
|
+
defaultCorePacks: z2.array(z2.string()).default([...DEFAULT_CORE_PACKS]),
|
|
398
|
+
generatedFiles: z2.array(z2.string()).default([])
|
|
399
|
+
});
|
|
400
|
+
var LegacyConfigSchema = z2.object({
|
|
401
|
+
version: z2.string().optional(),
|
|
402
|
+
registries: z2.array(z2.string()).optional(),
|
|
403
|
+
tools: z2.array(ToolSchema).optional(),
|
|
404
|
+
packs: z2.array(z2.string()).optional(),
|
|
405
|
+
packageManager: z2.string().optional(),
|
|
406
|
+
generatedFiles: z2.array(z2.string()).optional()
|
|
407
|
+
});
|
|
408
|
+
function normalizeConfig(raw) {
|
|
409
|
+
const current = CurrentConfigSchema.safeParse(raw);
|
|
410
|
+
if (current.success && "installedPacks" in raw) {
|
|
411
|
+
return current.data;
|
|
412
|
+
}
|
|
413
|
+
const legacy = LegacyConfigSchema.safeParse(raw);
|
|
414
|
+
if (legacy.success) {
|
|
415
|
+
return CurrentConfigSchema.parse({
|
|
416
|
+
version: legacy.data.version ?? "0.1.0",
|
|
417
|
+
registry: legacy.data.registries?.find((source) => source !== "official") ?? OFFICIAL_REGISTRY_URL,
|
|
418
|
+
tools: legacy.data.tools ?? DEFAULT_TOOLS,
|
|
419
|
+
installedPacks: legacy.data.packs ?? [],
|
|
420
|
+
defaultCorePacks: [...DEFAULT_CORE_PACKS],
|
|
421
|
+
generatedFiles: legacy.data.generatedFiles ?? []
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return CurrentConfigSchema.parse(raw);
|
|
425
|
+
}
|
|
426
|
+
var ConfigSchema = {
|
|
427
|
+
parse: normalizeConfig
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/registry/resolvePack.ts
|
|
368
431
|
function findPack(registry, packName) {
|
|
369
|
-
return registry.find((pack) => pack.name === packName);
|
|
432
|
+
return registry.packs.find((pack) => pack.name === packName);
|
|
370
433
|
}
|
|
371
|
-
function
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return pack;
|
|
379
|
-
});
|
|
434
|
+
function mandatoryCorePacks(registry) {
|
|
435
|
+
return DEFAULT_CORE_PACKS.map((name) => findPack(registry, name)).filter(
|
|
436
|
+
(pack) => Boolean(pack)
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
function missingMandatoryCorePacks(registry) {
|
|
440
|
+
return DEFAULT_CORE_PACKS.filter((name) => !findPack(registry, name));
|
|
380
441
|
}
|
|
381
442
|
async function hasDetectFile(root, filePattern) {
|
|
382
443
|
if (filePattern.includes("*")) {
|
|
@@ -389,31 +450,40 @@ async function hasDetectFile(root, filePattern) {
|
|
|
389
450
|
}
|
|
390
451
|
return fs8.pathExists(path9.join(root, filePattern));
|
|
391
452
|
}
|
|
392
|
-
function
|
|
393
|
-
return Boolean(pack.detect?.files?.length || pack.detect?.packages?.length);
|
|
394
|
-
}
|
|
395
|
-
async function recommendPacks(analysis, registry) {
|
|
396
|
-
const packageJson = await readPackageJson(analysis.root);
|
|
397
|
-
const recommended = [];
|
|
398
|
-
for (const pack of registry) {
|
|
399
|
-
if (pack.name === "env-secrets") {
|
|
400
|
-
recommended.push(pack);
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
if (hasDetectHints(pack) && await packMatchesProject(pack, analysis.root, packageJson)) {
|
|
404
|
-
recommended.push(pack);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
return recommended;
|
|
408
|
-
}
|
|
409
|
-
async function packMatchesProject(pack, root, packageJson) {
|
|
453
|
+
async function manifestMatchesProject(manifest, root, packageJson) {
|
|
410
454
|
const fileChecks = await Promise.all(
|
|
411
|
-
|
|
455
|
+
manifest.detect?.files?.map((filePattern) => hasDetectFile(root, filePattern)) ?? []
|
|
412
456
|
);
|
|
413
|
-
const packageChecks =
|
|
457
|
+
const packageChecks = manifest.detect?.packages?.map((packageName) => hasPackage(packageJson, packageName)) ?? [];
|
|
414
458
|
const checks = [...fileChecks, ...packageChecks];
|
|
415
459
|
return checks.length === 0 || checks.some(Boolean);
|
|
416
460
|
}
|
|
461
|
+
function recommendPackNames(analysis) {
|
|
462
|
+
const names = /* @__PURE__ */ new Set();
|
|
463
|
+
if (analysis.framework === "next-app-router") {
|
|
464
|
+
names.add("nextjs-best-practices");
|
|
465
|
+
names.add("react-performance");
|
|
466
|
+
names.add("react-composition");
|
|
467
|
+
}
|
|
468
|
+
if (analysis.styling.tailwind || analysis.styling.shadcn) {
|
|
469
|
+
names.add("shadcn-ui");
|
|
470
|
+
names.add("tailwind-v4");
|
|
471
|
+
names.add("ui-ux-design");
|
|
472
|
+
names.add("frontend-aesthetics");
|
|
473
|
+
}
|
|
474
|
+
if (analysis.language === "typescript") {
|
|
475
|
+
names.add("typescript-advanced-types");
|
|
476
|
+
}
|
|
477
|
+
if (analysis.services.supabase) {
|
|
478
|
+
names.add("supabase");
|
|
479
|
+
names.add("security-baseline");
|
|
480
|
+
}
|
|
481
|
+
return [...names];
|
|
482
|
+
}
|
|
483
|
+
async function recommendPacks(analysis, registry) {
|
|
484
|
+
const recommendedNames = recommendPackNames(analysis);
|
|
485
|
+
return recommendedNames.map((name) => findPack(registry, name)).filter((pack) => Boolean(pack));
|
|
486
|
+
}
|
|
417
487
|
function packageManagerLabel(packageManager) {
|
|
418
488
|
const labels = {
|
|
419
489
|
pnpm: "pnpm",
|
|
@@ -424,170 +494,296 @@ function packageManagerLabel(packageManager) {
|
|
|
424
494
|
};
|
|
425
495
|
return labels[packageManager];
|
|
426
496
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
497
|
+
async function packMatchesProject(pack, root, packageJson) {
|
|
498
|
+
return manifestMatchesProject(pack.manifest, root, packageJson ?? await readPackageJson(root));
|
|
499
|
+
}
|
|
500
|
+
function resolvePacks(packNames, packs) {
|
|
501
|
+
return packNames.map((packName) => {
|
|
502
|
+
const pack = packs.find((item) => item.manifest.name === packName);
|
|
503
|
+
if (!pack) {
|
|
504
|
+
throw new Error(`Unknown ContextForge pack: ${packName}`);
|
|
505
|
+
}
|
|
506
|
+
return pack;
|
|
507
|
+
});
|
|
508
|
+
}
|
|
439
509
|
|
|
440
510
|
// src/config/defaultConfig.ts
|
|
441
|
-
|
|
442
|
-
function createConfig(analysis, packs, tools = DEFAULT_TOOLS, registries = DEFAULT_REGISTRY_SOURCES) {
|
|
511
|
+
function createConfig(_analysis, packs, tools = DEFAULT_TOOLS, registry = OFFICIAL_REGISTRY_URL) {
|
|
443
512
|
return {
|
|
444
513
|
version: "0.1.0",
|
|
445
|
-
|
|
514
|
+
registry,
|
|
446
515
|
tools,
|
|
447
|
-
|
|
448
|
-
|
|
516
|
+
installedPacks: [...new Set(packs.map((pack) => pack.name))],
|
|
517
|
+
defaultCorePacks: [...DEFAULT_CORE_PACKS],
|
|
449
518
|
generatedFiles: []
|
|
450
519
|
};
|
|
451
520
|
}
|
|
452
521
|
|
|
453
|
-
// src/config/
|
|
522
|
+
// src/config/lockFile.ts
|
|
454
523
|
import path10 from "path";
|
|
455
524
|
import fs9 from "fs-extra";
|
|
525
|
+
import { z as z3 } from "zod";
|
|
526
|
+
var LOCK_PATH = ".contextforge/lock.json";
|
|
527
|
+
var LockSchema = z3.object({
|
|
528
|
+
registry: z3.string(),
|
|
529
|
+
resolvedAt: z3.string(),
|
|
530
|
+
packs: z3.record(
|
|
531
|
+
z3.string(),
|
|
532
|
+
z3.object({
|
|
533
|
+
version: z3.string(),
|
|
534
|
+
path: z3.string(),
|
|
535
|
+
source: z3.string()
|
|
536
|
+
})
|
|
537
|
+
)
|
|
538
|
+
});
|
|
539
|
+
async function loadLock(root) {
|
|
540
|
+
const lockPath = path10.join(root, LOCK_PATH);
|
|
541
|
+
if (!await fs9.pathExists(lockPath)) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
return LockSchema.parse(await fs9.readJson(lockPath));
|
|
545
|
+
}
|
|
546
|
+
async function saveLock(root, lock) {
|
|
547
|
+
const lockPath = path10.join(root, LOCK_PATH);
|
|
548
|
+
await fs9.ensureDir(path10.dirname(lockPath));
|
|
549
|
+
await fs9.writeFile(lockPath, `${JSON.stringify(lock, null, 2)}
|
|
550
|
+
`);
|
|
551
|
+
}
|
|
552
|
+
async function updateContextForgeLock(root, registry, installed) {
|
|
553
|
+
const lock = {
|
|
554
|
+
registry,
|
|
555
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
556
|
+
packs: {}
|
|
557
|
+
};
|
|
558
|
+
for (const pack of installed) {
|
|
559
|
+
lock.packs[pack.manifest.name] = {
|
|
560
|
+
version: pack.manifest.version,
|
|
561
|
+
path: pack.summary?.path ?? `packs/${pack.manifest.name}/pack.json`,
|
|
562
|
+
source: pack.packUrl ?? ""
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
await saveLock(root, lock);
|
|
566
|
+
return lock;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/config/loadConfig.ts
|
|
570
|
+
import path11 from "path";
|
|
571
|
+
import fs10 from "fs-extra";
|
|
456
572
|
var CONFIG_PATH = ".contextforge/config.json";
|
|
457
573
|
async function loadConfig(root) {
|
|
458
|
-
const configPath =
|
|
459
|
-
if (!await
|
|
574
|
+
const configPath = path11.join(root, CONFIG_PATH);
|
|
575
|
+
if (!await fs10.pathExists(configPath)) {
|
|
460
576
|
throw new Error("ContextForge config not found. Run `npx @contextforge/cli init` first.");
|
|
461
577
|
}
|
|
462
|
-
return ConfigSchema.parse(await
|
|
578
|
+
return ConfigSchema.parse(await fs10.readJson(configPath));
|
|
463
579
|
}
|
|
464
580
|
|
|
465
581
|
// src/config/saveConfig.ts
|
|
466
|
-
import
|
|
467
|
-
import
|
|
582
|
+
import path12 from "path";
|
|
583
|
+
import fs11 from "fs-extra";
|
|
468
584
|
async function saveConfig(root, config) {
|
|
469
|
-
const configPath =
|
|
470
|
-
await
|
|
471
|
-
await
|
|
585
|
+
const configPath = path12.join(root, CONFIG_PATH);
|
|
586
|
+
await fs11.ensureDir(path12.dirname(configPath));
|
|
587
|
+
await fs11.writeFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
472
588
|
`);
|
|
473
589
|
}
|
|
474
590
|
function addPackToConfig(config, packName) {
|
|
475
591
|
return {
|
|
476
592
|
...config,
|
|
477
|
-
|
|
593
|
+
installedPacks: [.../* @__PURE__ */ new Set([...config.installedPacks, packName])]
|
|
478
594
|
};
|
|
479
595
|
}
|
|
480
596
|
|
|
481
597
|
// src/registry/installPacks.ts
|
|
482
|
-
import
|
|
483
|
-
import
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
title: pack.title,
|
|
490
|
-
description: pack.description,
|
|
491
|
-
category: pack.category,
|
|
492
|
-
detect: pack.detect,
|
|
493
|
-
outputs: pack.outputs
|
|
494
|
-
};
|
|
598
|
+
import path13 from "path";
|
|
599
|
+
import fs12 from "fs-extra";
|
|
600
|
+
function normalizeRelativePath(relativePath) {
|
|
601
|
+
return relativePath.split(/[\\/]/u).join(path13.sep);
|
|
602
|
+
}
|
|
603
|
+
function packRoot(projectRoot, packName) {
|
|
604
|
+
return path13.join(projectRoot, PROJECT_PACK_CACHE, packName);
|
|
495
605
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
606
|
+
function packFilePath(projectRoot, packName, file) {
|
|
607
|
+
return path13.join(packRoot(projectRoot, packName), normalizeRelativePath(file.path));
|
|
608
|
+
}
|
|
609
|
+
async function downloadPackToContextForge(projectRoot, packName, packManifest, packUrl, timeoutMs) {
|
|
610
|
+
const root = packRoot(projectRoot, packName);
|
|
611
|
+
const files = {};
|
|
612
|
+
await fs12.ensureDir(root);
|
|
613
|
+
await fs12.writeFile(path13.join(root, "pack.json"), `${JSON.stringify(packManifest, null, 2)}
|
|
500
614
|
`);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
for (const [fileName, content] of optionalFiles) {
|
|
508
|
-
if (content) {
|
|
509
|
-
await fs11.writeFile(path12.join(packRoot, fileName), content);
|
|
510
|
-
}
|
|
615
|
+
for (const file of packManifest.files) {
|
|
616
|
+
const content = await fetchPackFile(packUrl, file, timeoutMs);
|
|
617
|
+
const targetPath = packFilePath(projectRoot, packName, file);
|
|
618
|
+
await fs12.ensureDir(path13.dirname(targetPath));
|
|
619
|
+
await fs12.writeFile(targetPath, content);
|
|
620
|
+
files[file.type] = content;
|
|
511
621
|
}
|
|
622
|
+
return {
|
|
623
|
+
manifest: packManifest,
|
|
624
|
+
packUrl,
|
|
625
|
+
files
|
|
626
|
+
};
|
|
512
627
|
}
|
|
513
|
-
async function
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
628
|
+
async function installPack(projectRoot, registryUrl, packName, options = {}) {
|
|
629
|
+
const registry = await fetchRegistry(registryUrl, options.timeoutMs);
|
|
630
|
+
const summary = findPackSummary(registry, packName);
|
|
631
|
+
if (!summary) {
|
|
632
|
+
throw new Error(`Unknown ContextForge pack: ${packName}`);
|
|
518
633
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
634
|
+
const alreadyInstalled = await fs12.pathExists(path13.join(packRoot(projectRoot, packName), "pack.json"));
|
|
635
|
+
const packUrl = resolvePackUrl(registryUrl, summary.path);
|
|
636
|
+
const manifest = await fetchPackManifest(packUrl, options.timeoutMs);
|
|
637
|
+
if (alreadyInstalled && !options.force) {
|
|
638
|
+
return {
|
|
639
|
+
installed: false,
|
|
640
|
+
alreadyInstalled: true,
|
|
641
|
+
packName,
|
|
642
|
+
manifest,
|
|
643
|
+
summary,
|
|
644
|
+
packUrl
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
if (!options.dryRun) {
|
|
648
|
+
await downloadPackToContextForge(projectRoot, packName, manifest, packUrl, options.timeoutMs);
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
installed: !options.dryRun,
|
|
652
|
+
alreadyInstalled,
|
|
653
|
+
packName,
|
|
654
|
+
manifest,
|
|
655
|
+
summary,
|
|
656
|
+
packUrl
|
|
530
657
|
};
|
|
531
|
-
await fs11.ensureDir(path12.dirname(metadataPath));
|
|
532
|
-
await fs11.writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}
|
|
533
|
-
`);
|
|
534
658
|
}
|
|
535
659
|
|
|
536
|
-
// src/compiler/
|
|
537
|
-
function
|
|
660
|
+
// src/compiler/compileOutputs.ts
|
|
661
|
+
function normalizeOutputPath(outputPath) {
|
|
662
|
+
return outputPath.split(/[\\/]/u).join("/");
|
|
663
|
+
}
|
|
664
|
+
function defaultOutput(packName, type) {
|
|
665
|
+
const defaults = {
|
|
666
|
+
rules: null,
|
|
667
|
+
agents: `.contextforge/instructions/agents/${packName}.md`,
|
|
668
|
+
claude: `.contextforge/instructions/claude/${packName}.md`,
|
|
669
|
+
skill: `.agents/skills/${packName}/SKILL.md`,
|
|
670
|
+
cursor: `.cursor/rules/${packName}.mdc`,
|
|
671
|
+
copilot: `.github/instructions/${packName}.instructions.md`
|
|
672
|
+
};
|
|
673
|
+
return defaults[type];
|
|
674
|
+
}
|
|
675
|
+
function shouldGenerateFile(type, tools) {
|
|
676
|
+
if (type === "agents") {
|
|
677
|
+
return tools.includes("codex");
|
|
678
|
+
}
|
|
679
|
+
if (type === "claude") {
|
|
680
|
+
return tools.includes("claude");
|
|
681
|
+
}
|
|
682
|
+
if (type === "skill") {
|
|
683
|
+
return tools.includes("codex") || tools.includes("claude");
|
|
684
|
+
}
|
|
685
|
+
if (type === "cursor") {
|
|
686
|
+
return tools.includes("cursor");
|
|
687
|
+
}
|
|
688
|
+
if (type === "copilot") {
|
|
689
|
+
return tools.includes("copilot");
|
|
690
|
+
}
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
function compileRootAgents(packs, analysis) {
|
|
694
|
+
const summaries = packs.map((pack) => pack.files.agents?.trim()).filter((summary) => Boolean(summary));
|
|
695
|
+
const gitSafetyWarning = packs.some((pack) => pack.manifest.name === "git-workflow") ? [
|
|
696
|
+
"## Git Safety",
|
|
697
|
+
"",
|
|
698
|
+
"Do not commit, push, merge, rebase, reset, delete branches, or rewrite history unless explicitly requested by the user.",
|
|
699
|
+
""
|
|
700
|
+
] : [];
|
|
538
701
|
return [
|
|
539
|
-
"#
|
|
702
|
+
"# Project Agent Instructions",
|
|
703
|
+
"",
|
|
704
|
+
"Generated by ContextForge from installed instruction packs.",
|
|
705
|
+
"",
|
|
706
|
+
"## Core Behavior",
|
|
707
|
+
"",
|
|
708
|
+
summaries.length > 0 ? summaries.join("\n\n") : "No pack-specific agent summaries are installed yet.",
|
|
540
709
|
"",
|
|
541
|
-
|
|
710
|
+
...gitSafetyWarning,
|
|
711
|
+
"## Installed Packs",
|
|
712
|
+
"",
|
|
713
|
+
...packs.length > 0 ? packs.map((pack) => `- ${pack.manifest.name}: ${pack.manifest.title}`) : ["- No packs installed"],
|
|
542
714
|
"",
|
|
543
715
|
"## Detected Project",
|
|
716
|
+
"",
|
|
544
717
|
`- Framework: ${analysis.framework}`,
|
|
545
718
|
`- Language: ${analysis.language}`,
|
|
546
|
-
`- Package manager: ${
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
].filter(Boolean).join(", ") || "not detected"}`,
|
|
554
|
-
"",
|
|
555
|
-
"## Active Packs",
|
|
556
|
-
...packs.flatMap((pack) => [
|
|
557
|
-
"",
|
|
558
|
-
`### ${pack.title}`,
|
|
559
|
-
"",
|
|
560
|
-
pack.description,
|
|
561
|
-
"",
|
|
562
|
-
pack.files.rules.trim()
|
|
563
|
-
])
|
|
719
|
+
`- Package manager: ${analysis.packageManager}`,
|
|
720
|
+
"",
|
|
721
|
+
"## Notes",
|
|
722
|
+
"",
|
|
723
|
+
"Detailed Codex skills live in `.agents/skills`.",
|
|
724
|
+
"Pack sources live in `.contextforge/packs`.",
|
|
725
|
+
"Pack-specific agent summaries live in `.contextforge/instructions/agents`."
|
|
564
726
|
].join("\n");
|
|
565
727
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
"
|
|
571
|
-
"
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
// src/compiler/compileCopilotInstructions.ts
|
|
576
|
-
function compileCopilotInstructions(analysis, packs) {
|
|
577
|
-
const enabledPacks = packs.filter((pack) => pack.outputs.copilotInstruction);
|
|
728
|
+
function compileRootClaude(packs) {
|
|
729
|
+
const summaries = packs.map((pack) => pack.files.claude?.trim()).filter((summary) => Boolean(summary));
|
|
730
|
+
const gitSafetyWarning = packs.some((pack) => pack.manifest.name === "git-workflow") ? [
|
|
731
|
+
"## Git Safety",
|
|
732
|
+
"",
|
|
733
|
+
"Do not commit, push, merge, rebase, reset, delete branches, or rewrite history unless explicitly requested by the user.",
|
|
734
|
+
""
|
|
735
|
+
] : [];
|
|
578
736
|
return [
|
|
579
|
-
"#
|
|
737
|
+
"# Claude Code Instructions",
|
|
738
|
+
"",
|
|
739
|
+
"Generated by ContextForge from installed instruction packs.",
|
|
740
|
+
"",
|
|
741
|
+
"## Core Behavior",
|
|
580
742
|
"",
|
|
581
|
-
|
|
743
|
+
summaries.length > 0 ? summaries.join("\n\n") : "No pack-specific Claude summaries are installed yet.",
|
|
582
744
|
"",
|
|
583
|
-
...
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
745
|
+
...gitSafetyWarning,
|
|
746
|
+
"## Installed Packs",
|
|
747
|
+
"",
|
|
748
|
+
...packs.length > 0 ? packs.map((pack) => `- ${pack.manifest.name}: ${pack.manifest.title}`) : ["- No packs installed"],
|
|
749
|
+
"",
|
|
750
|
+
"## Notes",
|
|
751
|
+
"",
|
|
752
|
+
"Keep this file concise.",
|
|
753
|
+
"Detailed pack sources live in `.contextforge/packs`.",
|
|
754
|
+
"Pack-specific Claude summaries live in `.contextforge/instructions/claude`."
|
|
589
755
|
].join("\n");
|
|
590
756
|
}
|
|
757
|
+
function compileOutputs(config, packs, analysis) {
|
|
758
|
+
const outputs = [];
|
|
759
|
+
for (const pack of packs) {
|
|
760
|
+
for (const file of pack.manifest.files) {
|
|
761
|
+
if (!shouldGenerateFile(file.type, config.tools)) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const content = pack.files[file.type];
|
|
765
|
+
const outputPath = file.output ?? defaultOutput(pack.manifest.name, file.type);
|
|
766
|
+
if (!content || !outputPath) {
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
outputs.push({
|
|
770
|
+
path: normalizeOutputPath(outputPath),
|
|
771
|
+
content
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (config.tools.includes("codex")) {
|
|
776
|
+
outputs.push({ path: "AGENTS.md", content: compileRootAgents(packs, analysis) });
|
|
777
|
+
}
|
|
778
|
+
if (config.tools.includes("claude")) {
|
|
779
|
+
outputs.push({ path: "CLAUDE.md", content: compileRootClaude(packs) });
|
|
780
|
+
}
|
|
781
|
+
return outputs;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/compiler/compileAgentsMd.ts
|
|
785
|
+
import path14 from "path";
|
|
786
|
+
import fs13 from "fs-extra";
|
|
591
787
|
|
|
592
788
|
// src/fs/updateGeneratedBlock.ts
|
|
593
789
|
var GENERATED_BLOCK_START = "<!-- contextforge:start -->";
|
|
@@ -602,6 +798,9 @@ function escapeRegExp(value) {
|
|
|
602
798
|
function getGeneratedBlock(content) {
|
|
603
799
|
return content.match(blockPattern)?.[0] ?? null;
|
|
604
800
|
}
|
|
801
|
+
function removeGeneratedBlock(content) {
|
|
802
|
+
return content.replace(blockPattern, "").replace(/\s+$/u, "");
|
|
803
|
+
}
|
|
605
804
|
function wrapGeneratedBlock(content) {
|
|
606
805
|
return `${GENERATED_BLOCK_START}
|
|
607
806
|
${content.trim()}
|
|
@@ -624,151 +823,408 @@ ${generatedBlock}
|
|
|
624
823
|
`;
|
|
625
824
|
}
|
|
626
825
|
|
|
627
|
-
// src/compiler/
|
|
628
|
-
|
|
826
|
+
// src/compiler/compileAgentsMd.ts
|
|
827
|
+
var AGENTS_INSTRUCTIONS_DIR = ".contextforge/instructions/agents";
|
|
828
|
+
async function readInstructionSummaries(root) {
|
|
829
|
+
const directory = path14.join(root, AGENTS_INSTRUCTIONS_DIR);
|
|
830
|
+
if (!await fs13.pathExists(directory)) {
|
|
831
|
+
return [];
|
|
832
|
+
}
|
|
833
|
+
const files = (await fs13.readdir(directory)).filter((file) => file.endsWith(".md")).sort((a, b) => a.localeCompare(b));
|
|
834
|
+
return Promise.all(files.map((file) => fs13.readFile(path14.join(directory, file), "utf8")));
|
|
835
|
+
}
|
|
836
|
+
async function compileAgentsMd(root, packs) {
|
|
837
|
+
const summaries = await readInstructionSummaries(root);
|
|
838
|
+
const gitSafetyWarning = packs.some((pack) => pack.manifest.name === "git-workflow") ? [
|
|
839
|
+
"## Git Safety",
|
|
840
|
+
"",
|
|
841
|
+
"Do not commit, push, merge, rebase, reset, delete branches, or rewrite history unless explicitly requested by the user.",
|
|
842
|
+
""
|
|
843
|
+
] : [];
|
|
629
844
|
return [
|
|
630
|
-
"
|
|
631
|
-
`description: ${description}`,
|
|
632
|
-
"alwaysApply: true",
|
|
633
|
-
"---",
|
|
845
|
+
"# Project Agent Instructions",
|
|
634
846
|
"",
|
|
635
847
|
GENERATED_BLOCK_START,
|
|
636
|
-
|
|
848
|
+
"Generated by ContextForge from installed instruction packs.",
|
|
849
|
+
"",
|
|
850
|
+
"## Core Behavior",
|
|
851
|
+
"",
|
|
852
|
+
summaries.length > 0 ? summaries.map((summary) => summary.trim()).join("\n\n") : "No pack-specific agent summaries are installed yet.",
|
|
853
|
+
"",
|
|
854
|
+
...gitSafetyWarning,
|
|
855
|
+
"## Installed Packs",
|
|
856
|
+
"",
|
|
857
|
+
...packs.length > 0 ? packs.map((pack) => `- ${pack.manifest.name}: ${pack.manifest.title}`) : ["- No packs installed"],
|
|
858
|
+
"",
|
|
859
|
+
"## Notes",
|
|
860
|
+
"",
|
|
861
|
+
"Detailed Codex skills live in `.agents/skills`.",
|
|
862
|
+
"Pack sources live in `.contextforge/packs`.",
|
|
863
|
+
"Pack-specific agent summaries live in `.contextforge/instructions/agents`.",
|
|
864
|
+
"",
|
|
637
865
|
GENERATED_BLOCK_END
|
|
638
866
|
].join("\n");
|
|
639
867
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
`- Framework: ${analysis.framework}`,
|
|
650
|
-
`- Language: ${analysis.language}`,
|
|
651
|
-
`- Package manager: ${analysis.packageManager}`,
|
|
652
|
-
`- Active packs: ${packs.map((pack) => pack.name).join(", ") || "none"}`
|
|
653
|
-
].join("\n")
|
|
654
|
-
)
|
|
655
|
-
}
|
|
656
|
-
];
|
|
657
|
-
for (const pack of packs.filter((item) => item.outputs.cursorRule)) {
|
|
658
|
-
files.push({
|
|
659
|
-
path: `.cursor/rules/${pack.name}.mdc`,
|
|
660
|
-
content: withCursorFrontmatter(
|
|
661
|
-
pack.description.replace(/:/g, "-"),
|
|
662
|
-
(pack.files.cursor ?? pack.files.rules).trim()
|
|
663
|
-
)
|
|
664
|
-
});
|
|
868
|
+
|
|
869
|
+
// src/compiler/compileClaudeMd.ts
|
|
870
|
+
import path15 from "path";
|
|
871
|
+
import fs14 from "fs-extra";
|
|
872
|
+
var CLAUDE_INSTRUCTIONS_DIR = ".contextforge/instructions/claude";
|
|
873
|
+
async function readInstructionSummaries2(root) {
|
|
874
|
+
const directory = path15.join(root, CLAUDE_INSTRUCTIONS_DIR);
|
|
875
|
+
if (!await fs14.pathExists(directory)) {
|
|
876
|
+
return [];
|
|
665
877
|
}
|
|
666
|
-
|
|
878
|
+
const files = (await fs14.readdir(directory)).filter((file) => file.endsWith(".md")).sort((a, b) => a.localeCompare(b));
|
|
879
|
+
return Promise.all(files.map((file) => fs14.readFile(path15.join(directory, file), "utf8")));
|
|
667
880
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
881
|
+
async function compileClaudeMd(root, packs) {
|
|
882
|
+
const summaries = await readInstructionSummaries2(root);
|
|
883
|
+
const gitSafetyWarning = packs.some((pack) => pack.manifest.name === "git-workflow") ? [
|
|
884
|
+
"## Git Safety",
|
|
885
|
+
"",
|
|
886
|
+
"Do not commit, push, merge, rebase, reset, delete branches, or rewrite history unless explicitly requested by the user.",
|
|
887
|
+
""
|
|
888
|
+
] : [];
|
|
889
|
+
return [
|
|
890
|
+
"# Claude Code Instructions",
|
|
891
|
+
"",
|
|
892
|
+
GENERATED_BLOCK_START,
|
|
893
|
+
"Generated by ContextForge from installed instruction packs.",
|
|
894
|
+
"",
|
|
895
|
+
"## Core Behavior",
|
|
896
|
+
"",
|
|
897
|
+
summaries.length > 0 ? summaries.map((summary) => summary.trim()).join("\n\n") : "No pack-specific Claude summaries are installed yet.",
|
|
898
|
+
"",
|
|
899
|
+
...gitSafetyWarning,
|
|
900
|
+
"## Installed Packs",
|
|
901
|
+
"",
|
|
902
|
+
...packs.length > 0 ? packs.map((pack) => `- ${pack.manifest.name}: ${pack.manifest.title}`) : ["- No packs installed"],
|
|
903
|
+
"",
|
|
904
|
+
"## Notes",
|
|
905
|
+
"",
|
|
906
|
+
"Keep this file concise.",
|
|
907
|
+
"Detailed pack sources live in `.contextforge/packs`.",
|
|
908
|
+
"Pack-specific Claude summaries live in `.contextforge/instructions/claude`.",
|
|
909
|
+
"",
|
|
910
|
+
GENERATED_BLOCK_END
|
|
911
|
+
].join("\n");
|
|
672
912
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}));
|
|
913
|
+
|
|
914
|
+
// src/compiler/generateToolOutputs.ts
|
|
915
|
+
import path18 from "path";
|
|
916
|
+
import fs17 from "fs-extra";
|
|
917
|
+
|
|
918
|
+
// src/fs/safeWriteFile.ts
|
|
919
|
+
import path16 from "path";
|
|
920
|
+
import fs15 from "fs-extra";
|
|
921
|
+
async function safeWriteFile(filePath, generatedContent) {
|
|
922
|
+
const existingContent = await fs15.pathExists(filePath) ? await fs15.readFile(filePath, "utf8") : null;
|
|
923
|
+
const nextContent = updateGeneratedBlock(existingContent, generatedContent);
|
|
924
|
+
await fs15.ensureDir(path16.dirname(filePath));
|
|
925
|
+
await fs15.writeFile(filePath, nextContent);
|
|
687
926
|
}
|
|
688
927
|
|
|
689
|
-
// src/compiler/
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
928
|
+
// src/compiler/cleanupGeneratedFiles.ts
|
|
929
|
+
import path17 from "path";
|
|
930
|
+
import fs16 from "fs-extra";
|
|
931
|
+
function isGeneratedOnlyPath(relativePath) {
|
|
932
|
+
return relativePath.startsWith(".agents/skills/") || relativePath.startsWith(".cursor/rules/");
|
|
933
|
+
}
|
|
934
|
+
async function cleanupFile(root, relativePath) {
|
|
935
|
+
const filePath = path17.join(root, relativePath);
|
|
936
|
+
if (!await fs16.pathExists(filePath)) {
|
|
937
|
+
return;
|
|
695
938
|
}
|
|
696
|
-
if (
|
|
697
|
-
|
|
939
|
+
if (isGeneratedOnlyPath(relativePath)) {
|
|
940
|
+
await fs16.remove(filePath);
|
|
941
|
+
return;
|
|
698
942
|
}
|
|
699
|
-
|
|
700
|
-
|
|
943
|
+
const content = await fs16.readFile(filePath, "utf8");
|
|
944
|
+
if (!getGeneratedBlock(content)) {
|
|
945
|
+
return;
|
|
701
946
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
947
|
+
const nextContent = removeGeneratedBlock(content);
|
|
948
|
+
if (nextContent.trim().length === 0) {
|
|
949
|
+
await fs16.remove(filePath);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
await fs16.writeFile(filePath, `${nextContent}
|
|
953
|
+
`);
|
|
954
|
+
}
|
|
955
|
+
async function cleanupStaleGeneratedFiles(root, previousFiles, currentFiles) {
|
|
956
|
+
const current = new Set(currentFiles);
|
|
957
|
+
const staleFiles = [...new Set(previousFiles)].filter((file) => !current.has(file));
|
|
958
|
+
for (const staleFile of staleFiles) {
|
|
959
|
+
await cleanupFile(root, staleFile);
|
|
707
960
|
}
|
|
708
|
-
return outputs;
|
|
709
961
|
}
|
|
710
962
|
|
|
711
|
-
// src/compiler/
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
963
|
+
// src/compiler/generateToolOutputs.ts
|
|
964
|
+
var GENERATED_ONLY_PREFIXES = [
|
|
965
|
+
".agents/skills/",
|
|
966
|
+
".cursor/rules/",
|
|
967
|
+
".github/instructions/",
|
|
968
|
+
".contextforge/instructions/"
|
|
969
|
+
];
|
|
970
|
+
function normalizeOutputPath2(outputPath) {
|
|
971
|
+
return outputPath.split(/[\\/]/u).join("/");
|
|
972
|
+
}
|
|
973
|
+
function defaultOutput2(packName, type) {
|
|
974
|
+
const defaults = {
|
|
975
|
+
rules: null,
|
|
976
|
+
agents: `.contextforge/instructions/agents/${packName}.md`,
|
|
977
|
+
claude: `.contextforge/instructions/claude/${packName}.md`,
|
|
978
|
+
skill: `.agents/skills/${packName}/SKILL.md`,
|
|
979
|
+
cursor: `.cursor/rules/${packName}.mdc`,
|
|
980
|
+
copilot: `.github/instructions/${packName}.instructions.md`
|
|
981
|
+
};
|
|
982
|
+
return defaults[type];
|
|
983
|
+
}
|
|
984
|
+
function shouldGenerateFile2(type, tools) {
|
|
985
|
+
if (type === "agents") {
|
|
986
|
+
return tools.includes("codex");
|
|
987
|
+
}
|
|
988
|
+
if (type === "claude") {
|
|
989
|
+
return tools.includes("claude");
|
|
990
|
+
}
|
|
991
|
+
if (type === "skill") {
|
|
992
|
+
return tools.includes("codex") || tools.includes("claude");
|
|
993
|
+
}
|
|
994
|
+
if (type === "cursor") {
|
|
995
|
+
return tools.includes("cursor");
|
|
996
|
+
}
|
|
997
|
+
if (type === "copilot") {
|
|
998
|
+
return tools.includes("copilot");
|
|
999
|
+
}
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
function generatedFileFromPack(pack, file) {
|
|
1003
|
+
const content = pack.files[file.type];
|
|
1004
|
+
const outputPath = file.output ?? defaultOutput2(pack.manifest.name, file.type);
|
|
1005
|
+
if (!content || !outputPath) {
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
return {
|
|
1009
|
+
path: normalizeOutputPath2(outputPath),
|
|
1010
|
+
content
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
async function writeGeneratedOnlyFile(root, output) {
|
|
1014
|
+
const filePath = path18.join(root, output.path);
|
|
1015
|
+
await fs17.ensureDir(path18.dirname(filePath));
|
|
1016
|
+
await fs17.writeFile(filePath, output.content.endsWith("\n") ? output.content : `${output.content}
|
|
1017
|
+
`);
|
|
1018
|
+
}
|
|
1019
|
+
function packOutputs(pack, tools) {
|
|
1020
|
+
return pack.manifest.files.filter((file) => shouldGenerateFile2(file.type, tools)).map((file) => generatedFileFromPack(pack, file)).filter((file) => Boolean(file));
|
|
1021
|
+
}
|
|
1022
|
+
async function generateToolOutputs(root, packs, config, previousFiles = []) {
|
|
1023
|
+
const packGeneratedOutputs = packs.flatMap((pack) => packOutputs(pack, config.tools));
|
|
1024
|
+
for (const output of packGeneratedOutputs) {
|
|
1025
|
+
const isGeneratedOnly = GENERATED_ONLY_PREFIXES.some((prefix) => output.path.startsWith(prefix));
|
|
1026
|
+
if (isGeneratedOnly) {
|
|
1027
|
+
await writeGeneratedOnlyFile(root, output);
|
|
1028
|
+
} else {
|
|
1029
|
+
await safeWriteFile(path18.join(root, output.path), output.content);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
const rootOutputs = [];
|
|
1033
|
+
if (config.tools.includes("codex")) {
|
|
1034
|
+
rootOutputs.push({ path: "AGENTS.md", content: await compileAgentsMd(root, packs) });
|
|
1035
|
+
}
|
|
1036
|
+
if (config.tools.includes("claude")) {
|
|
1037
|
+
rootOutputs.push({ path: "CLAUDE.md", content: await compileClaudeMd(root, packs) });
|
|
1038
|
+
}
|
|
1039
|
+
for (const output of rootOutputs) {
|
|
1040
|
+
await safeWriteFile(path18.join(root, output.path), output.content);
|
|
1041
|
+
}
|
|
1042
|
+
const currentFiles = [...packGeneratedOutputs, ...rootOutputs].map((output) => output.path);
|
|
1043
|
+
await cleanupStaleGeneratedFiles(root, previousFiles, currentFiles);
|
|
1044
|
+
return currentFiles;
|
|
722
1045
|
}
|
|
723
1046
|
|
|
724
1047
|
// src/compiler/writeGeneratedFiles.ts
|
|
725
|
-
|
|
1048
|
+
import path19 from "path";
|
|
1049
|
+
async function writeGeneratedFiles(root, outputs, previousFiles = []) {
|
|
1050
|
+
const currentFiles = outputs.map((output) => output.path);
|
|
726
1051
|
for (const output of outputs) {
|
|
727
|
-
await safeWriteFile(
|
|
1052
|
+
await safeWriteFile(path19.join(root, output.path), output.content);
|
|
728
1053
|
}
|
|
729
|
-
|
|
1054
|
+
await cleanupStaleGeneratedFiles(root, previousFiles, currentFiles);
|
|
1055
|
+
return currentFiles;
|
|
730
1056
|
}
|
|
731
1057
|
|
|
732
1058
|
// src/sync.ts
|
|
733
|
-
import
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
const config =
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1059
|
+
import path20 from "path";
|
|
1060
|
+
import fs18 from "fs-extra";
|
|
1061
|
+
async function updateContextForgeConfig(projectRoot, packName, registryUrl, generatedFiles) {
|
|
1062
|
+
const existing = await loadOptionalConfig(projectRoot);
|
|
1063
|
+
const config = addPackToConfig(
|
|
1064
|
+
existing ?? {
|
|
1065
|
+
version: "0.1.0",
|
|
1066
|
+
registry: registryUrl,
|
|
1067
|
+
tools: ["codex", "claude", "cursor", "copilot"],
|
|
1068
|
+
installedPacks: [],
|
|
1069
|
+
defaultCorePacks: [],
|
|
1070
|
+
generatedFiles: []
|
|
1071
|
+
},
|
|
1072
|
+
packName
|
|
1073
|
+
);
|
|
747
1074
|
const nextConfig = {
|
|
748
1075
|
...config,
|
|
749
|
-
|
|
1076
|
+
registry: registryUrl,
|
|
750
1077
|
generatedFiles
|
|
751
1078
|
};
|
|
752
|
-
await saveConfig(
|
|
1079
|
+
await saveConfig(projectRoot, nextConfig);
|
|
1080
|
+
return nextConfig;
|
|
1081
|
+
}
|
|
1082
|
+
async function loadOptionalConfig(root) {
|
|
1083
|
+
try {
|
|
1084
|
+
return await loadConfig(root);
|
|
1085
|
+
} catch {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
async function loadInstalledPackFromRegistry(projectRoot, registryUrl, summary, timeoutMs) {
|
|
1090
|
+
const packUrl = resolvePackUrl(registryUrl, summary.path);
|
|
1091
|
+
const manifest = await fetchPackManifest(packUrl, timeoutMs);
|
|
1092
|
+
return downloadPackToContextForge(projectRoot, manifest.name, manifest, packUrl, timeoutMs);
|
|
1093
|
+
}
|
|
1094
|
+
async function syncInstalledPacks(projectRoot, providedConfig) {
|
|
1095
|
+
const root = path20.resolve(projectRoot);
|
|
1096
|
+
const analysis = await detectProject(root);
|
|
1097
|
+
const previousConfig = await loadOptionalConfig(root);
|
|
1098
|
+
const config = providedConfig ?? await loadConfig(root);
|
|
1099
|
+
const registry = await fetchRegistry(config.registry);
|
|
1100
|
+
const installed = [];
|
|
1101
|
+
const missing = [];
|
|
1102
|
+
for (const packName of config.installedPacks) {
|
|
1103
|
+
const summary = findPackSummary(registry, packName);
|
|
1104
|
+
if (!summary) {
|
|
1105
|
+
missing.push(packName);
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
installed.push(await loadInstalledPackFromRegistry(root, config.registry, summary));
|
|
1109
|
+
}
|
|
1110
|
+
const generatedFiles = await generateToolOutputs(
|
|
1111
|
+
root,
|
|
1112
|
+
installed,
|
|
1113
|
+
config,
|
|
1114
|
+
previousConfig?.generatedFiles ?? []
|
|
1115
|
+
);
|
|
1116
|
+
const nextConfig = {
|
|
1117
|
+
...config,
|
|
1118
|
+
installedPacks: installed.map((pack) => pack.manifest.name),
|
|
1119
|
+
generatedFiles
|
|
1120
|
+
};
|
|
1121
|
+
await saveConfig(root, nextConfig);
|
|
1122
|
+
await updateContextForgeLock(
|
|
1123
|
+
root,
|
|
1124
|
+
config.registry,
|
|
1125
|
+
installed.map((pack) => ({
|
|
1126
|
+
manifest: pack.manifest,
|
|
1127
|
+
summary: registry.packs.find((summary) => summary.name === pack.manifest.name),
|
|
1128
|
+
packUrl: pack.packUrl
|
|
1129
|
+
}))
|
|
1130
|
+
);
|
|
1131
|
+
if (missing.length > 0) {
|
|
1132
|
+
throw new Error(`Installed packs missing from registry: ${missing.join(", ")}`);
|
|
1133
|
+
}
|
|
753
1134
|
return {
|
|
754
|
-
root
|
|
1135
|
+
root,
|
|
755
1136
|
analysis,
|
|
756
1137
|
generatedFiles,
|
|
757
|
-
outputs,
|
|
758
|
-
config: nextConfig
|
|
1138
|
+
outputs: generatedFiles.map((file) => ({ path: file, content: "" })),
|
|
1139
|
+
config: nextConfig,
|
|
1140
|
+
installedPacks: installed
|
|
759
1141
|
};
|
|
760
1142
|
}
|
|
1143
|
+
async function syncProject(root, providedConfig) {
|
|
1144
|
+
return syncInstalledPacks(root, providedConfig);
|
|
1145
|
+
}
|
|
1146
|
+
async function installPackAndSync(projectRoot, registryUrl, packName, options = {}) {
|
|
1147
|
+
const root = path20.resolve(projectRoot);
|
|
1148
|
+
const config = await loadOptionalConfig(root);
|
|
1149
|
+
const result = await installPack(root, registryUrl, packName, options);
|
|
1150
|
+
if (result.alreadyInstalled && !options.force && config?.installedPacks.includes(packName)) {
|
|
1151
|
+
return {
|
|
1152
|
+
...result,
|
|
1153
|
+
generatedFiles: [],
|
|
1154
|
+
config: config ?? void 0
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
if (options.dryRun) {
|
|
1158
|
+
return {
|
|
1159
|
+
...result,
|
|
1160
|
+
generatedFiles: [],
|
|
1161
|
+
config: config ?? void 0
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
const nextConfig = {
|
|
1165
|
+
version: "0.1.0",
|
|
1166
|
+
registry: registryUrl,
|
|
1167
|
+
tools: options.tools ?? config?.tools ?? ["codex", "claude", "cursor", "copilot"],
|
|
1168
|
+
installedPacks: [.../* @__PURE__ */ new Set([...config?.installedPacks ?? [], packName])],
|
|
1169
|
+
defaultCorePacks: config?.defaultCorePacks ?? [],
|
|
1170
|
+
generatedFiles: config?.generatedFiles ?? []
|
|
1171
|
+
};
|
|
1172
|
+
const syncResult = await syncInstalledPacks(root, nextConfig);
|
|
1173
|
+
return {
|
|
1174
|
+
...result,
|
|
1175
|
+
generatedFiles: syncResult.generatedFiles,
|
|
1176
|
+
config: syncResult.config
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
async function readInstalledPacks(projectRoot) {
|
|
1180
|
+
return loadProjectPacks(projectRoot);
|
|
1181
|
+
}
|
|
1182
|
+
async function pathExists(root, relativePath) {
|
|
1183
|
+
return fs18.pathExists(path20.join(root, relativePath));
|
|
1184
|
+
}
|
|
761
1185
|
|
|
762
1186
|
// src/doctor/doctorProject.ts
|
|
763
|
-
import
|
|
764
|
-
import
|
|
1187
|
+
import path21 from "path";
|
|
1188
|
+
import fs19 from "fs-extra";
|
|
765
1189
|
async function fileExists(root, relativePath) {
|
|
766
|
-
return
|
|
1190
|
+
return fs19.pathExists(path21.join(root, relativePath));
|
|
1191
|
+
}
|
|
1192
|
+
async function readIfExists(root, relativePath) {
|
|
1193
|
+
const filePath = path21.join(root, relativePath);
|
|
1194
|
+
if (!await fs19.pathExists(filePath)) {
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
return fs19.readFile(filePath, "utf8");
|
|
1198
|
+
}
|
|
1199
|
+
function hasContextForgeBlock(content) {
|
|
1200
|
+
return Boolean(content && getGeneratedBlock(content));
|
|
1201
|
+
}
|
|
1202
|
+
function tooLarge(content) {
|
|
1203
|
+
return Boolean(content && content.length > 2e4);
|
|
767
1204
|
}
|
|
768
|
-
|
|
1205
|
+
function expectedGeneratedOutputs(packName, tools) {
|
|
1206
|
+
const outputs = [];
|
|
1207
|
+
if (tools.includes("codex") || tools.includes("claude")) {
|
|
1208
|
+
outputs.push(`.agents/skills/${packName}/SKILL.md`);
|
|
1209
|
+
}
|
|
1210
|
+
if (tools.includes("cursor")) {
|
|
1211
|
+
outputs.push(`.cursor/rules/${packName}.mdc`);
|
|
1212
|
+
}
|
|
1213
|
+
if (tools.includes("copilot")) {
|
|
1214
|
+
outputs.push(`.github/instructions/${packName}.instructions.md`);
|
|
1215
|
+
}
|
|
1216
|
+
if (tools.includes("codex")) {
|
|
1217
|
+
outputs.push(`.contextforge/instructions/agents/${packName}.md`);
|
|
1218
|
+
}
|
|
1219
|
+
if (tools.includes("claude")) {
|
|
1220
|
+
outputs.push(`.contextforge/instructions/claude/${packName}.md`);
|
|
1221
|
+
}
|
|
1222
|
+
return outputs;
|
|
1223
|
+
}
|
|
1224
|
+
async function doctorProject(root, options = {}) {
|
|
769
1225
|
const checks = [];
|
|
770
1226
|
const issues = [];
|
|
771
|
-
const resolvedRoot =
|
|
1227
|
+
const resolvedRoot = path21.resolve(root);
|
|
772
1228
|
if (!await fileExists(resolvedRoot, CONFIG_PATH)) {
|
|
773
1229
|
return {
|
|
774
1230
|
checks,
|
|
@@ -782,97 +1238,195 @@ async function doctorProject(root) {
|
|
|
782
1238
|
}
|
|
783
1239
|
checks.push("Config found");
|
|
784
1240
|
const config = await loadConfig(resolvedRoot);
|
|
785
|
-
const
|
|
1241
|
+
const registryUrl = options.registry ?? config.registry;
|
|
1242
|
+
const [analysis, packageJson, lock] = await Promise.all([
|
|
786
1243
|
detectProject(resolvedRoot),
|
|
787
|
-
|
|
788
|
-
|
|
1244
|
+
readPackageJson(resolvedRoot),
|
|
1245
|
+
loadLock(resolvedRoot)
|
|
789
1246
|
]);
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1247
|
+
let registryPacks = /* @__PURE__ */ new Set();
|
|
1248
|
+
try {
|
|
1249
|
+
const registry = await fetchRegistry(registryUrl);
|
|
1250
|
+
registryPacks = new Set(registry.packs.map((pack) => pack.name));
|
|
1251
|
+
checks.push("Registry reachable");
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
issues.push({
|
|
1254
|
+
level: "error",
|
|
1255
|
+
message: `Registry ${registryUrl} is not reachable: ${error instanceof Error ? error.message : String(error)}`
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
if (lock) {
|
|
1259
|
+
checks.push("Lock file found");
|
|
1260
|
+
} else {
|
|
1261
|
+
issues.push({
|
|
1262
|
+
level: "warning",
|
|
1263
|
+
message: `${LOCK_PATH} is missing. Run \`npx @contextforge/cli sync\`.`
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
const cachedPacks = await loadProjectPacks(resolvedRoot);
|
|
1267
|
+
const cachedPackNames = new Set(cachedPacks.map((pack) => pack.manifest.name));
|
|
1268
|
+
for (const packName of config.installedPacks) {
|
|
1269
|
+
if (registryPacks.size > 0 && !registryPacks.has(packName)) {
|
|
1270
|
+
issues.push({
|
|
1271
|
+
level: "error",
|
|
1272
|
+
message: `${packName} is installed in config, but no longer exists in the registry.`
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
if (!cachedPackNames.has(packName)) {
|
|
1276
|
+
issues.push({
|
|
1277
|
+
level: "error",
|
|
1278
|
+
message: `.contextforge/packs/${packName}/pack.json is missing. Run \`npx @contextforge/cli sync\`.`
|
|
1279
|
+
});
|
|
799
1280
|
continue;
|
|
800
1281
|
}
|
|
801
|
-
|
|
802
|
-
|
|
1282
|
+
for (const output of expectedGeneratedOutputs(packName, config.tools)) {
|
|
1283
|
+
if (!await fileExists(resolvedRoot, output)) {
|
|
1284
|
+
issues.push({
|
|
1285
|
+
level: "error",
|
|
1286
|
+
message: `${output} is missing. Run \`npx @contextforge/cli sync\`.`
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
const agentsMd = await readIfExists(resolvedRoot, "AGENTS.md");
|
|
1292
|
+
const claudeMd = await readIfExists(resolvedRoot, "CLAUDE.md");
|
|
1293
|
+
if (config.tools.includes("codex")) {
|
|
1294
|
+
if (hasContextForgeBlock(agentsMd)) {
|
|
1295
|
+
checks.push("AGENTS.md ContextForge block found");
|
|
1296
|
+
} else {
|
|
1297
|
+
issues.push({
|
|
1298
|
+
level: "error",
|
|
1299
|
+
message: "AGENTS.md is missing a ContextForge generated block. Run `npx @contextforge/cli sync`."
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
if (config.tools.includes("claude")) {
|
|
1304
|
+
if (hasContextForgeBlock(claudeMd)) {
|
|
1305
|
+
checks.push("CLAUDE.md ContextForge block found");
|
|
803
1306
|
} else {
|
|
804
1307
|
issues.push({
|
|
805
1308
|
level: "error",
|
|
806
|
-
message:
|
|
1309
|
+
message: "CLAUDE.md is missing a ContextForge generated block. Run `npx @contextforge/cli sync`."
|
|
807
1310
|
});
|
|
808
1311
|
}
|
|
809
1312
|
}
|
|
810
|
-
|
|
811
|
-
|
|
1313
|
+
if (tooLarge(agentsMd)) {
|
|
1314
|
+
issues.push({
|
|
1315
|
+
level: "warning",
|
|
1316
|
+
message: "AGENTS.md is large. Root instructions should stay concise; detailed content belongs in pack files and skills."
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
if (tooLarge(claudeMd)) {
|
|
1320
|
+
issues.push({
|
|
1321
|
+
level: "warning",
|
|
1322
|
+
message: "CLAUDE.md is large. Root instructions should stay concise; detailed content belongs in pack files."
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
const gitWorkflow = cachedPacks.find((pack) => pack.manifest.name === "git-workflow");
|
|
1326
|
+
if (gitWorkflow) {
|
|
1327
|
+
const gitSummary = [gitWorkflow.files.agents, gitWorkflow.files.claude, agentsMd, claudeMd].filter((content) => Boolean(content)).join("\n").toLowerCase();
|
|
1328
|
+
if (!gitSummary.includes("do not commit") || !gitSummary.includes("push") || !gitSummary.includes("explicitly")) {
|
|
812
1329
|
issues.push({
|
|
813
1330
|
level: "warning",
|
|
814
|
-
message:
|
|
1331
|
+
message: "git-workflow is installed, but the expected explicit-permission git safety warning was not found."
|
|
815
1332
|
});
|
|
816
1333
|
}
|
|
817
1334
|
}
|
|
818
|
-
for (const pack of
|
|
1335
|
+
for (const pack of cachedPacks) {
|
|
819
1336
|
if (!await packMatchesProject(pack, resolvedRoot, packageJson)) {
|
|
820
1337
|
issues.push({
|
|
821
1338
|
level: "warning",
|
|
822
|
-
message: `${pack.name}
|
|
1339
|
+
message: `${pack.manifest.name} is installed, but its detection hints do not match this project.`
|
|
823
1340
|
});
|
|
824
1341
|
}
|
|
825
1342
|
}
|
|
826
|
-
if (config.
|
|
1343
|
+
if (config.installedPacks.includes("test-driven-development") && !hasScript(packageJson, "test")) {
|
|
827
1344
|
issues.push({
|
|
828
1345
|
level: "warning",
|
|
829
|
-
message:
|
|
1346
|
+
message: "test-driven-development is installed, but package.json has no test script."
|
|
830
1347
|
});
|
|
831
1348
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1349
|
+
for (const recommendedPack of recommendPackNames(analysis)) {
|
|
1350
|
+
if (!config.installedPacks.includes(recommendedPack) && registryPacks.has(recommendedPack)) {
|
|
1351
|
+
issues.push({
|
|
1352
|
+
level: "warning",
|
|
1353
|
+
message: `${recommendedPack} matches the detected stack but is not installed. Run \`npx @contextforge/cli add ${recommendedPack}\` if you want it.`
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
837
1356
|
}
|
|
838
1357
|
return { checks, issues };
|
|
839
1358
|
}
|
|
840
1359
|
export {
|
|
841
1360
|
CONFIG_PATH,
|
|
842
1361
|
ConfigSchema,
|
|
1362
|
+
DEFAULT_CORE_PACKS,
|
|
843
1363
|
DEFAULT_REGISTRY_SOURCES,
|
|
844
1364
|
DEFAULT_TOOLS,
|
|
845
1365
|
GENERATED_BLOCK_END,
|
|
846
1366
|
GENERATED_BLOCK_START,
|
|
1367
|
+
LOCK_PATH,
|
|
847
1368
|
OFFICIAL_REGISTRY_SOURCE,
|
|
848
1369
|
OFFICIAL_REGISTRY_URL,
|
|
849
1370
|
PROJECT_PACK_CACHE,
|
|
1371
|
+
PackFileSchema,
|
|
1372
|
+
PackFileTypeSchema,
|
|
1373
|
+
PackManifestSchema,
|
|
850
1374
|
PackSchema,
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1375
|
+
RegistryIndexSchema,
|
|
1376
|
+
RegistryPackSourceSchema,
|
|
1377
|
+
RegistryPackSummarySchema,
|
|
1378
|
+
ToolSchema,
|
|
854
1379
|
addPackToConfig,
|
|
855
|
-
|
|
1380
|
+
compileAgentsMd,
|
|
1381
|
+
compileClaudeMd,
|
|
856
1382
|
compileOutputs,
|
|
857
1383
|
createConfig,
|
|
858
1384
|
detectPackageManager,
|
|
859
1385
|
detectProject,
|
|
860
1386
|
doctorProject,
|
|
1387
|
+
downloadPackToContextForge,
|
|
1388
|
+
fetchPackFile,
|
|
1389
|
+
fetchPackManifest,
|
|
1390
|
+
fetchRegistry,
|
|
1391
|
+
fetchText,
|
|
861
1392
|
findPack,
|
|
1393
|
+
findPackSummary,
|
|
1394
|
+
generateToolOutputs,
|
|
862
1395
|
getGeneratedBlock,
|
|
863
1396
|
hasPackage,
|
|
864
1397
|
hasScript,
|
|
1398
|
+
installPack,
|
|
1399
|
+
installPackAndSync,
|
|
1400
|
+
listRegistryPacks,
|
|
865
1401
|
loadConfig,
|
|
1402
|
+
loadLock,
|
|
1403
|
+
loadProjectPacks,
|
|
866
1404
|
loadRegistry,
|
|
1405
|
+
loadRemotePack,
|
|
1406
|
+
mandatoryCorePacks,
|
|
1407
|
+
manifestMatchesProject,
|
|
1408
|
+
missingMandatoryCorePacks,
|
|
1409
|
+
normalizeConfig,
|
|
867
1410
|
packMatchesProject,
|
|
868
1411
|
packageManagerLabel,
|
|
1412
|
+
pathExists,
|
|
1413
|
+
readInstalledPacks,
|
|
869
1414
|
readPackageJson,
|
|
1415
|
+
recommendPackNames,
|
|
870
1416
|
recommendPacks,
|
|
1417
|
+
registrySourceToUrl,
|
|
1418
|
+
removeGeneratedBlock,
|
|
1419
|
+
resolvePackFileUrl,
|
|
1420
|
+
resolvePackUrl,
|
|
871
1421
|
resolvePacks,
|
|
872
1422
|
safeWriteFile,
|
|
873
1423
|
saveConfig,
|
|
874
|
-
|
|
1424
|
+
saveLock,
|
|
1425
|
+
searchRegistryPacks,
|
|
1426
|
+
syncInstalledPacks,
|
|
875
1427
|
syncProject,
|
|
1428
|
+
updateContextForgeConfig,
|
|
1429
|
+
updateContextForgeLock,
|
|
876
1430
|
updateGeneratedBlock,
|
|
877
1431
|
wrapGeneratedBlock,
|
|
878
1432
|
writeGeneratedFiles
|