@contextforge/core 0.1.2 → 0.1.6
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 +931 -432
- 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,169 +494,167 @@ 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
|
-
}
|
|
535
|
-
|
|
536
|
-
// src/compiler/compileAgentsMd.ts
|
|
537
|
-
function compileAgentsMd(analysis, packs) {
|
|
538
|
-
return [
|
|
539
|
-
"# ContextForge Instructions",
|
|
540
|
-
"",
|
|
541
|
-
"Generated by ContextForge. Add project-specific guidance outside this block.",
|
|
542
|
-
"",
|
|
543
|
-
"## Detected Project",
|
|
544
|
-
`- Framework: ${analysis.framework}`,
|
|
545
|
-
`- Language: ${analysis.language}`,
|
|
546
|
-
`- Package manager: ${packageManagerLabel(analysis.packageManager)}`,
|
|
547
|
-
`- Styling: ${analysis.styling.tailwind ? "Tailwind CSS" : "not detected"}${analysis.styling.shadcn ? " + shadcn/ui" : ""}`,
|
|
548
|
-
`- Database: ${analysis.database.prisma ? "Prisma" : analysis.database.drizzle ? "Drizzle" : "not detected"}`,
|
|
549
|
-
`- Testing: ${[
|
|
550
|
-
analysis.testing.vitest && "Vitest",
|
|
551
|
-
analysis.testing.jest && "Jest",
|
|
552
|
-
analysis.testing.playwright && "Playwright"
|
|
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
|
-
])
|
|
564
|
-
].join("\n");
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// src/compiler/compileClaudeMd.ts
|
|
568
|
-
function compileClaudeMd(analysis, packs) {
|
|
569
|
-
return compileAgentsMd(analysis, packs).replace(
|
|
570
|
-
"# ContextForge Instructions",
|
|
571
|
-
"# ContextForge Claude Instructions"
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// src/compiler/compileCopilotInstructions.ts
|
|
576
|
-
function compileCopilotInstructions(analysis, packs) {
|
|
577
|
-
const enabledPacks = packs.filter((pack) => pack.outputs.copilotInstruction);
|
|
578
|
-
return [
|
|
579
|
-
"# ContextForge Copilot Instructions",
|
|
580
|
-
"",
|
|
581
|
-
`Project framework: ${analysis.framework}. Language: ${analysis.language}.`,
|
|
582
|
-
"",
|
|
583
|
-
...enabledPacks.flatMap((pack) => [
|
|
584
|
-
`## ${pack.title}`,
|
|
585
|
-
"",
|
|
586
|
-
(pack.files.copilot ?? pack.files.rules).trim(),
|
|
587
|
-
""
|
|
588
|
-
])
|
|
589
|
-
].join("\n");
|
|
590
658
|
}
|
|
591
659
|
|
|
592
660
|
// src/fs/updateGeneratedBlock.ts
|
|
@@ -602,6 +670,9 @@ function escapeRegExp(value) {
|
|
|
602
670
|
function getGeneratedBlock(content) {
|
|
603
671
|
return content.match(blockPattern)?.[0] ?? null;
|
|
604
672
|
}
|
|
673
|
+
function removeGeneratedBlock(content) {
|
|
674
|
+
return content.replace(blockPattern, "").replace(/\s+$/u, "");
|
|
675
|
+
}
|
|
605
676
|
function wrapGeneratedBlock(content) {
|
|
606
677
|
return `${GENERATED_BLOCK_START}
|
|
607
678
|
${content.trim()}
|
|
@@ -624,151 +695,470 @@ ${generatedBlock}
|
|
|
624
695
|
`;
|
|
625
696
|
}
|
|
626
697
|
|
|
627
|
-
// src/compiler/
|
|
628
|
-
function
|
|
698
|
+
// src/compiler/compileOutputs.ts
|
|
699
|
+
function normalizeOutputPath(outputPath) {
|
|
700
|
+
return outputPath.split(/[\\/]/u).join("/");
|
|
701
|
+
}
|
|
702
|
+
function defaultOutput(packName, type) {
|
|
703
|
+
const defaults = {
|
|
704
|
+
rules: null,
|
|
705
|
+
agents: `.contextforge/agents/codex/${packName}.md`,
|
|
706
|
+
claude: `.contextforge/agents/claude/${packName}.md`,
|
|
707
|
+
skill: `.contextforge/skills/${packName}/SKILL.md`,
|
|
708
|
+
cursor: `.contextforge/agents/cursor/${packName}.md`,
|
|
709
|
+
copilot: `.contextforge/agents/copilot/${packName}.md`
|
|
710
|
+
};
|
|
711
|
+
return defaults[type];
|
|
712
|
+
}
|
|
713
|
+
function shouldGenerateFile(type, tools) {
|
|
714
|
+
if (type === "agents") {
|
|
715
|
+
return tools.includes("codex");
|
|
716
|
+
}
|
|
717
|
+
if (type === "claude") {
|
|
718
|
+
return tools.includes("claude");
|
|
719
|
+
}
|
|
720
|
+
if (type === "skill") {
|
|
721
|
+
return tools.length > 0;
|
|
722
|
+
}
|
|
723
|
+
if (type === "cursor") {
|
|
724
|
+
return tools.includes("cursor");
|
|
725
|
+
}
|
|
726
|
+
if (type === "copilot") {
|
|
727
|
+
return tools.includes("copilot");
|
|
728
|
+
}
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
function compileRootAgents(packs, analysis) {
|
|
629
732
|
return [
|
|
630
|
-
"
|
|
631
|
-
`description: ${description}`,
|
|
632
|
-
"alwaysApply: true",
|
|
633
|
-
"---",
|
|
733
|
+
"# Project Agent Instructions",
|
|
634
734
|
"",
|
|
635
735
|
GENERATED_BLOCK_START,
|
|
636
|
-
|
|
736
|
+
"ContextForge is installed for this repo.",
|
|
737
|
+
"",
|
|
738
|
+
"Before working, read the relevant instruction files in:",
|
|
739
|
+
"",
|
|
740
|
+
"- `.contextforge/agents/codex/`",
|
|
741
|
+
"- `.contextforge/skills/`",
|
|
742
|
+
"",
|
|
743
|
+
"Follow the installed packs listed in `.contextforge/config.json`.",
|
|
744
|
+
"Do not copy these instructions into this file.",
|
|
637
745
|
GENERATED_BLOCK_END
|
|
638
746
|
].join("\n");
|
|
639
747
|
}
|
|
640
|
-
function
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
});
|
|
665
|
-
}
|
|
666
|
-
return files;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// src/compiler/compileSkills.ts
|
|
670
|
-
function escapeYaml(value) {
|
|
671
|
-
return value.replace(/"/g, '\\"');
|
|
672
|
-
}
|
|
673
|
-
function compileSkills(packs) {
|
|
674
|
-
return packs.filter((pack) => pack.outputs.skill).map((pack) => ({
|
|
675
|
-
path: `.agents/skills/${pack.name}/SKILL.md`,
|
|
676
|
-
content: [
|
|
677
|
-
"---",
|
|
678
|
-
`name: ${pack.name}`,
|
|
679
|
-
`description: "${escapeYaml(pack.description)}"`,
|
|
680
|
-
"---",
|
|
681
|
-
"",
|
|
682
|
-
GENERATED_BLOCK_START,
|
|
683
|
-
(pack.files.skill ?? pack.files.rules).trim(),
|
|
684
|
-
GENERATED_BLOCK_END
|
|
685
|
-
].join("\n")
|
|
686
|
-
}));
|
|
748
|
+
function compileRootClaude(packs) {
|
|
749
|
+
return [
|
|
750
|
+
"# Claude Code Instructions",
|
|
751
|
+
"",
|
|
752
|
+
GENERATED_BLOCK_START,
|
|
753
|
+
"ContextForge is installed for this repo.",
|
|
754
|
+
"",
|
|
755
|
+
"Before working, read the relevant instruction files in:",
|
|
756
|
+
"",
|
|
757
|
+
"- `.contextforge/agents/claude/`",
|
|
758
|
+
"- `.contextforge/skills/`",
|
|
759
|
+
"",
|
|
760
|
+
"Follow the installed packs listed in `.contextforge/config.json`.",
|
|
761
|
+
"Do not copy these instructions into this file.",
|
|
762
|
+
GENERATED_BLOCK_END
|
|
763
|
+
].join("\n");
|
|
687
764
|
}
|
|
688
|
-
|
|
689
|
-
// src/compiler/compileOutputs.ts
|
|
690
765
|
function compileOutputs(config, packs, analysis) {
|
|
691
766
|
const outputs = [];
|
|
767
|
+
for (const pack of packs) {
|
|
768
|
+
for (const file of pack.manifest.files) {
|
|
769
|
+
if (!shouldGenerateFile(file.type, config.tools)) {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const content = pack.files[file.type];
|
|
773
|
+
const outputPath = defaultOutput(pack.manifest.name, file.type);
|
|
774
|
+
if (!content || !outputPath) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
outputs.push({
|
|
778
|
+
path: normalizeOutputPath(outputPath),
|
|
779
|
+
content: withContextForgePreamble(pack, file.type, content)
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
692
783
|
if (config.tools.includes("codex")) {
|
|
693
|
-
outputs.push({ path: "AGENTS.md", content:
|
|
694
|
-
outputs.push(...compileSkills(packs));
|
|
784
|
+
outputs.push({ path: "AGENTS.md", content: compileRootAgents(packs, analysis) });
|
|
695
785
|
}
|
|
696
786
|
if (config.tools.includes("claude")) {
|
|
697
|
-
outputs.push({ path: "CLAUDE.md", content:
|
|
787
|
+
outputs.push({ path: "CLAUDE.md", content: compileRootClaude(packs) });
|
|
698
788
|
}
|
|
699
|
-
|
|
700
|
-
|
|
789
|
+
return outputs;
|
|
790
|
+
}
|
|
791
|
+
function withContextForgePreamble(pack, type, content) {
|
|
792
|
+
if (pack.manifest.name !== "git-workflow" || !["agents", "claude", "cursor", "copilot"].includes(type)) {
|
|
793
|
+
return content;
|
|
701
794
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
content: compileCopilotInstructions(analysis, packs)
|
|
706
|
-
});
|
|
795
|
+
const warning = "Do not commit, push, merge, rebase, reset, delete branches, or rewrite history unless explicitly requested by the user.";
|
|
796
|
+
if (content.includes(warning)) {
|
|
797
|
+
return content;
|
|
707
798
|
}
|
|
708
|
-
return
|
|
799
|
+
return ["# ContextForge Git Safety", "", warning, "", content.trim()].join("\n");
|
|
709
800
|
}
|
|
710
801
|
|
|
711
|
-
// src/compiler/
|
|
712
|
-
|
|
802
|
+
// src/compiler/compileAgentsMd.ts
|
|
803
|
+
async function compileAgentsMd() {
|
|
804
|
+
return [
|
|
805
|
+
"# Project Agent Instructions",
|
|
806
|
+
"",
|
|
807
|
+
GENERATED_BLOCK_START,
|
|
808
|
+
"ContextForge is installed for this repo.",
|
|
809
|
+
"",
|
|
810
|
+
"Before working, read the relevant instruction files in:",
|
|
811
|
+
"",
|
|
812
|
+
"- `.contextforge/agents/codex/`",
|
|
813
|
+
"- `.contextforge/skills/`",
|
|
814
|
+
"",
|
|
815
|
+
"Follow the installed packs listed in `.contextforge/config.json`.",
|
|
816
|
+
"Do not copy these instructions into this file.",
|
|
817
|
+
GENERATED_BLOCK_END
|
|
818
|
+
].join("\n");
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/compiler/compileClaudeMd.ts
|
|
822
|
+
async function compileClaudeMd() {
|
|
823
|
+
return [
|
|
824
|
+
"# Claude Code Instructions",
|
|
825
|
+
"",
|
|
826
|
+
GENERATED_BLOCK_START,
|
|
827
|
+
"ContextForge is installed for this repo.",
|
|
828
|
+
"",
|
|
829
|
+
"Before working, read the relevant instruction files in:",
|
|
830
|
+
"",
|
|
831
|
+
"- `.contextforge/agents/claude/`",
|
|
832
|
+
"- `.contextforge/skills/`",
|
|
833
|
+
"",
|
|
834
|
+
"Follow the installed packs listed in `.contextforge/config.json`.",
|
|
835
|
+
"Do not copy these instructions into this file.",
|
|
836
|
+
GENERATED_BLOCK_END
|
|
837
|
+
].join("\n");
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/compiler/generateToolOutputs.ts
|
|
841
|
+
import path16 from "path";
|
|
842
|
+
import fs15 from "fs-extra";
|
|
713
843
|
|
|
714
844
|
// src/fs/safeWriteFile.ts
|
|
715
|
-
import
|
|
716
|
-
import
|
|
845
|
+
import path14 from "path";
|
|
846
|
+
import fs13 from "fs-extra";
|
|
717
847
|
async function safeWriteFile(filePath, generatedContent) {
|
|
718
|
-
const existingContent = await
|
|
848
|
+
const existingContent = await fs13.pathExists(filePath) ? await fs13.readFile(filePath, "utf8") : null;
|
|
719
849
|
const nextContent = updateGeneratedBlock(existingContent, generatedContent);
|
|
720
|
-
await
|
|
721
|
-
await
|
|
850
|
+
await fs13.ensureDir(path14.dirname(filePath));
|
|
851
|
+
await fs13.writeFile(filePath, nextContent);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/compiler/cleanupGeneratedFiles.ts
|
|
855
|
+
import path15 from "path";
|
|
856
|
+
import fs14 from "fs-extra";
|
|
857
|
+
function isGeneratedOnlyPath(relativePath) {
|
|
858
|
+
return relativePath.startsWith(".agents/skills/") || relativePath.startsWith(".cursor/rules/") || relativePath.startsWith(".github/instructions/") || relativePath.startsWith(".contextforge/instructions/") || relativePath.startsWith(".contextforge/agents/") || relativePath.startsWith(".contextforge/skills/");
|
|
859
|
+
}
|
|
860
|
+
async function cleanupFile(root, relativePath) {
|
|
861
|
+
const filePath = path15.join(root, relativePath);
|
|
862
|
+
if (!await fs14.pathExists(filePath)) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (isGeneratedOnlyPath(relativePath)) {
|
|
866
|
+
await fs14.remove(filePath);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const content = await fs14.readFile(filePath, "utf8");
|
|
870
|
+
if (!getGeneratedBlock(content)) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const nextContent = removeGeneratedBlock(content);
|
|
874
|
+
if (nextContent.trim().length === 0) {
|
|
875
|
+
await fs14.remove(filePath);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
await fs14.writeFile(filePath, `${nextContent}
|
|
879
|
+
`);
|
|
880
|
+
}
|
|
881
|
+
async function cleanupStaleGeneratedFiles(root, previousFiles, currentFiles) {
|
|
882
|
+
const current = new Set(currentFiles);
|
|
883
|
+
const staleFiles = [...new Set(previousFiles)].filter((file) => !current.has(file));
|
|
884
|
+
for (const staleFile of staleFiles) {
|
|
885
|
+
await cleanupFile(root, staleFile);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/compiler/generateToolOutputs.ts
|
|
890
|
+
var GENERATED_ONLY_PREFIXES = [
|
|
891
|
+
".contextforge/agents/",
|
|
892
|
+
".contextforge/skills/"
|
|
893
|
+
];
|
|
894
|
+
function normalizeOutputPath2(outputPath) {
|
|
895
|
+
return outputPath.split(/[\\/]/u).join("/");
|
|
896
|
+
}
|
|
897
|
+
function defaultOutput2(packName, type) {
|
|
898
|
+
const defaults = {
|
|
899
|
+
rules: null,
|
|
900
|
+
agents: `.contextforge/agents/codex/${packName}.md`,
|
|
901
|
+
claude: `.contextforge/agents/claude/${packName}.md`,
|
|
902
|
+
skill: `.contextforge/skills/${packName}/SKILL.md`,
|
|
903
|
+
cursor: `.contextforge/agents/cursor/${packName}.md`,
|
|
904
|
+
copilot: `.contextforge/agents/copilot/${packName}.md`
|
|
905
|
+
};
|
|
906
|
+
return defaults[type];
|
|
907
|
+
}
|
|
908
|
+
function shouldGenerateFile2(type, tools) {
|
|
909
|
+
if (type === "agents") {
|
|
910
|
+
return tools.includes("codex");
|
|
911
|
+
}
|
|
912
|
+
if (type === "claude") {
|
|
913
|
+
return tools.includes("claude");
|
|
914
|
+
}
|
|
915
|
+
if (type === "skill") {
|
|
916
|
+
return tools.length > 0;
|
|
917
|
+
}
|
|
918
|
+
if (type === "cursor") {
|
|
919
|
+
return tools.includes("cursor");
|
|
920
|
+
}
|
|
921
|
+
if (type === "copilot") {
|
|
922
|
+
return tools.includes("copilot");
|
|
923
|
+
}
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
function generatedFileFromPack(pack, file) {
|
|
927
|
+
const content = pack.files[file.type];
|
|
928
|
+
const outputPath = defaultOutput2(pack.manifest.name, file.type);
|
|
929
|
+
if (!content || !outputPath) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
path: normalizeOutputPath2(outputPath),
|
|
934
|
+
content: withContextForgePreamble2(pack, file.type, content)
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function withContextForgePreamble2(pack, type, content) {
|
|
938
|
+
if (pack.manifest.name !== "git-workflow" || !["agents", "claude", "cursor", "copilot"].includes(type)) {
|
|
939
|
+
return content;
|
|
940
|
+
}
|
|
941
|
+
const warning = "Do not commit, push, merge, rebase, reset, delete branches, or rewrite history unless explicitly requested by the user.";
|
|
942
|
+
if (content.includes(warning)) {
|
|
943
|
+
return content;
|
|
944
|
+
}
|
|
945
|
+
return ["# ContextForge Git Safety", "", warning, "", content.trim()].join("\n");
|
|
946
|
+
}
|
|
947
|
+
async function writeGeneratedOnlyFile(root, output) {
|
|
948
|
+
const filePath = path16.join(root, output.path);
|
|
949
|
+
await fs15.ensureDir(path16.dirname(filePath));
|
|
950
|
+
await fs15.writeFile(filePath, output.content.endsWith("\n") ? output.content : `${output.content}
|
|
951
|
+
`);
|
|
952
|
+
}
|
|
953
|
+
function packOutputs(pack, tools) {
|
|
954
|
+
return pack.manifest.files.filter((file) => shouldGenerateFile2(file.type, tools)).map((file) => generatedFileFromPack(pack, file)).filter((file) => Boolean(file));
|
|
955
|
+
}
|
|
956
|
+
async function generateToolOutputs(root, packs, config, previousFiles = []) {
|
|
957
|
+
const packGeneratedOutputs = packs.flatMap((pack) => packOutputs(pack, config.tools));
|
|
958
|
+
for (const output of packGeneratedOutputs) {
|
|
959
|
+
const isGeneratedOnly = GENERATED_ONLY_PREFIXES.some((prefix) => output.path.startsWith(prefix));
|
|
960
|
+
if (isGeneratedOnly) {
|
|
961
|
+
await writeGeneratedOnlyFile(root, output);
|
|
962
|
+
} else {
|
|
963
|
+
await safeWriteFile(path16.join(root, output.path), output.content);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const rootOutputs = [];
|
|
967
|
+
if (config.tools.includes("codex")) {
|
|
968
|
+
rootOutputs.push({ path: "AGENTS.md", content: await compileAgentsMd() });
|
|
969
|
+
}
|
|
970
|
+
if (config.tools.includes("claude")) {
|
|
971
|
+
rootOutputs.push({ path: "CLAUDE.md", content: await compileClaudeMd() });
|
|
972
|
+
}
|
|
973
|
+
for (const output of rootOutputs) {
|
|
974
|
+
await safeWriteFile(path16.join(root, output.path), output.content);
|
|
975
|
+
}
|
|
976
|
+
const currentFiles = [...packGeneratedOutputs, ...rootOutputs].map((output) => output.path);
|
|
977
|
+
await cleanupStaleGeneratedFiles(root, previousFiles, currentFiles);
|
|
978
|
+
return currentFiles;
|
|
722
979
|
}
|
|
723
980
|
|
|
724
981
|
// src/compiler/writeGeneratedFiles.ts
|
|
725
|
-
|
|
982
|
+
import path17 from "path";
|
|
983
|
+
async function writeGeneratedFiles(root, outputs, previousFiles = []) {
|
|
984
|
+
const currentFiles = outputs.map((output) => output.path);
|
|
726
985
|
for (const output of outputs) {
|
|
727
|
-
await safeWriteFile(
|
|
986
|
+
await safeWriteFile(path17.join(root, output.path), output.content);
|
|
728
987
|
}
|
|
729
|
-
|
|
988
|
+
await cleanupStaleGeneratedFiles(root, previousFiles, currentFiles);
|
|
989
|
+
return currentFiles;
|
|
730
990
|
}
|
|
731
991
|
|
|
732
992
|
// src/sync.ts
|
|
733
|
-
import
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
const config =
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
993
|
+
import path18 from "path";
|
|
994
|
+
import fs16 from "fs-extra";
|
|
995
|
+
async function updateContextForgeConfig(projectRoot, packName, registryUrl, generatedFiles) {
|
|
996
|
+
const existing = await loadOptionalConfig(projectRoot);
|
|
997
|
+
const config = addPackToConfig(
|
|
998
|
+
existing ?? {
|
|
999
|
+
version: "0.1.0",
|
|
1000
|
+
registry: registryUrl,
|
|
1001
|
+
tools: ["codex", "claude", "cursor", "copilot"],
|
|
1002
|
+
installedPacks: [],
|
|
1003
|
+
defaultCorePacks: [],
|
|
1004
|
+
generatedFiles: []
|
|
1005
|
+
},
|
|
1006
|
+
packName
|
|
1007
|
+
);
|
|
747
1008
|
const nextConfig = {
|
|
748
1009
|
...config,
|
|
749
|
-
|
|
1010
|
+
registry: registryUrl,
|
|
750
1011
|
generatedFiles
|
|
751
1012
|
};
|
|
752
|
-
await saveConfig(
|
|
1013
|
+
await saveConfig(projectRoot, nextConfig);
|
|
1014
|
+
return nextConfig;
|
|
1015
|
+
}
|
|
1016
|
+
async function loadOptionalConfig(root) {
|
|
1017
|
+
try {
|
|
1018
|
+
return await loadConfig(root);
|
|
1019
|
+
} catch {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
async function loadInstalledPackFromRegistry(projectRoot, registryUrl, summary, timeoutMs) {
|
|
1024
|
+
const packUrl = resolvePackUrl(registryUrl, summary.path);
|
|
1025
|
+
const manifest = await fetchPackManifest(packUrl, timeoutMs);
|
|
1026
|
+
return downloadPackToContextForge(projectRoot, manifest.name, manifest, packUrl, timeoutMs);
|
|
1027
|
+
}
|
|
1028
|
+
async function syncInstalledPacks(projectRoot, providedConfig) {
|
|
1029
|
+
const root = path18.resolve(projectRoot);
|
|
1030
|
+
const analysis = await detectProject(root);
|
|
1031
|
+
const previousConfig = await loadOptionalConfig(root);
|
|
1032
|
+
const config = providedConfig ?? await loadConfig(root);
|
|
1033
|
+
const registry = await fetchRegistry(config.registry);
|
|
1034
|
+
const installed = [];
|
|
1035
|
+
const missing = [];
|
|
1036
|
+
for (const packName of config.installedPacks) {
|
|
1037
|
+
const summary = findPackSummary(registry, packName);
|
|
1038
|
+
if (!summary) {
|
|
1039
|
+
missing.push(packName);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
installed.push(await loadInstalledPackFromRegistry(root, config.registry, summary));
|
|
1043
|
+
}
|
|
1044
|
+
const generatedFiles = await generateToolOutputs(
|
|
1045
|
+
root,
|
|
1046
|
+
installed,
|
|
1047
|
+
config,
|
|
1048
|
+
previousConfig?.generatedFiles ?? []
|
|
1049
|
+
);
|
|
1050
|
+
const nextConfig = {
|
|
1051
|
+
...config,
|
|
1052
|
+
installedPacks: installed.map((pack) => pack.manifest.name),
|
|
1053
|
+
generatedFiles
|
|
1054
|
+
};
|
|
1055
|
+
await saveConfig(root, nextConfig);
|
|
1056
|
+
await updateContextForgeLock(
|
|
1057
|
+
root,
|
|
1058
|
+
config.registry,
|
|
1059
|
+
installed.map((pack) => ({
|
|
1060
|
+
manifest: pack.manifest,
|
|
1061
|
+
summary: registry.packs.find((summary) => summary.name === pack.manifest.name),
|
|
1062
|
+
packUrl: pack.packUrl
|
|
1063
|
+
}))
|
|
1064
|
+
);
|
|
1065
|
+
if (missing.length > 0) {
|
|
1066
|
+
throw new Error(`Installed packs missing from registry: ${missing.join(", ")}`);
|
|
1067
|
+
}
|
|
753
1068
|
return {
|
|
754
|
-
root
|
|
1069
|
+
root,
|
|
755
1070
|
analysis,
|
|
756
1071
|
generatedFiles,
|
|
757
|
-
outputs,
|
|
758
|
-
config: nextConfig
|
|
1072
|
+
outputs: generatedFiles.map((file) => ({ path: file, content: "" })),
|
|
1073
|
+
config: nextConfig,
|
|
1074
|
+
installedPacks: installed
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
async function syncProject(root, providedConfig) {
|
|
1078
|
+
return syncInstalledPacks(root, providedConfig);
|
|
1079
|
+
}
|
|
1080
|
+
async function installPackAndSync(projectRoot, registryUrl, packName, options = {}) {
|
|
1081
|
+
const root = path18.resolve(projectRoot);
|
|
1082
|
+
const config = await loadOptionalConfig(root);
|
|
1083
|
+
const result = await installPack(root, registryUrl, packName, options);
|
|
1084
|
+
if (result.alreadyInstalled && !options.force && config?.installedPacks.includes(packName)) {
|
|
1085
|
+
return {
|
|
1086
|
+
...result,
|
|
1087
|
+
generatedFiles: [],
|
|
1088
|
+
config: config ?? void 0
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
if (options.dryRun) {
|
|
1092
|
+
return {
|
|
1093
|
+
...result,
|
|
1094
|
+
generatedFiles: [],
|
|
1095
|
+
config: config ?? void 0
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
const nextConfig = {
|
|
1099
|
+
version: "0.1.0",
|
|
1100
|
+
registry: registryUrl,
|
|
1101
|
+
tools: options.tools ?? config?.tools ?? ["codex", "claude", "cursor", "copilot"],
|
|
1102
|
+
installedPacks: [.../* @__PURE__ */ new Set([...config?.installedPacks ?? [], packName])],
|
|
1103
|
+
defaultCorePacks: config?.defaultCorePacks ?? [],
|
|
1104
|
+
generatedFiles: config?.generatedFiles ?? []
|
|
759
1105
|
};
|
|
1106
|
+
const syncResult = await syncInstalledPacks(root, nextConfig);
|
|
1107
|
+
return {
|
|
1108
|
+
...result,
|
|
1109
|
+
generatedFiles: syncResult.generatedFiles,
|
|
1110
|
+
config: syncResult.config
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
async function readInstalledPacks(projectRoot) {
|
|
1114
|
+
return loadProjectPacks(projectRoot);
|
|
1115
|
+
}
|
|
1116
|
+
async function pathExists(root, relativePath) {
|
|
1117
|
+
return fs16.pathExists(path18.join(root, relativePath));
|
|
760
1118
|
}
|
|
761
1119
|
|
|
762
1120
|
// src/doctor/doctorProject.ts
|
|
763
|
-
import
|
|
764
|
-
import
|
|
1121
|
+
import path19 from "path";
|
|
1122
|
+
import fs17 from "fs-extra";
|
|
765
1123
|
async function fileExists(root, relativePath) {
|
|
766
|
-
return
|
|
1124
|
+
return fs17.pathExists(path19.join(root, relativePath));
|
|
1125
|
+
}
|
|
1126
|
+
async function readIfExists(root, relativePath) {
|
|
1127
|
+
const filePath = path19.join(root, relativePath);
|
|
1128
|
+
if (!await fs17.pathExists(filePath)) {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
return fs17.readFile(filePath, "utf8");
|
|
1132
|
+
}
|
|
1133
|
+
function hasContextForgeBlock(content) {
|
|
1134
|
+
return Boolean(content && getGeneratedBlock(content));
|
|
1135
|
+
}
|
|
1136
|
+
function tooLarge(content) {
|
|
1137
|
+
return Boolean(content && content.length > 2e4);
|
|
1138
|
+
}
|
|
1139
|
+
function expectedGeneratedOutputs(packName, tools) {
|
|
1140
|
+
const outputs = [];
|
|
1141
|
+
if (tools.length > 0) {
|
|
1142
|
+
outputs.push(`.contextforge/skills/${packName}/SKILL.md`);
|
|
1143
|
+
}
|
|
1144
|
+
if (tools.includes("codex")) {
|
|
1145
|
+
outputs.push(`.contextforge/agents/codex/${packName}.md`);
|
|
1146
|
+
}
|
|
1147
|
+
if (tools.includes("claude")) {
|
|
1148
|
+
outputs.push(`.contextforge/agents/claude/${packName}.md`);
|
|
1149
|
+
}
|
|
1150
|
+
if (tools.includes("cursor")) {
|
|
1151
|
+
outputs.push(`.contextforge/agents/cursor/${packName}.md`);
|
|
1152
|
+
}
|
|
1153
|
+
if (tools.includes("copilot")) {
|
|
1154
|
+
outputs.push(`.contextforge/agents/copilot/${packName}.md`);
|
|
1155
|
+
}
|
|
1156
|
+
return outputs;
|
|
767
1157
|
}
|
|
768
|
-
async function doctorProject(root) {
|
|
1158
|
+
async function doctorProject(root, options = {}) {
|
|
769
1159
|
const checks = [];
|
|
770
1160
|
const issues = [];
|
|
771
|
-
const resolvedRoot =
|
|
1161
|
+
const resolvedRoot = path19.resolve(root);
|
|
772
1162
|
if (!await fileExists(resolvedRoot, CONFIG_PATH)) {
|
|
773
1163
|
return {
|
|
774
1164
|
checks,
|
|
@@ -782,97 +1172,206 @@ async function doctorProject(root) {
|
|
|
782
1172
|
}
|
|
783
1173
|
checks.push("Config found");
|
|
784
1174
|
const config = await loadConfig(resolvedRoot);
|
|
785
|
-
const
|
|
1175
|
+
const registryUrl = options.registry ?? config.registry;
|
|
1176
|
+
const [analysis, packageJson, lock] = await Promise.all([
|
|
786
1177
|
detectProject(resolvedRoot),
|
|
787
|
-
|
|
788
|
-
|
|
1178
|
+
readPackageJson(resolvedRoot),
|
|
1179
|
+
loadLock(resolvedRoot)
|
|
789
1180
|
]);
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1181
|
+
let registryPacks = /* @__PURE__ */ new Set();
|
|
1182
|
+
try {
|
|
1183
|
+
const registry = await fetchRegistry(registryUrl);
|
|
1184
|
+
registryPacks = new Set(registry.packs.map((pack) => pack.name));
|
|
1185
|
+
checks.push("Registry reachable");
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
issues.push({
|
|
1188
|
+
level: "error",
|
|
1189
|
+
message: `Registry ${registryUrl} is not reachable: ${error instanceof Error ? error.message : String(error)}`
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
if (lock) {
|
|
1193
|
+
checks.push("Lock file found");
|
|
1194
|
+
} else {
|
|
1195
|
+
issues.push({
|
|
1196
|
+
level: "warning",
|
|
1197
|
+
message: `${LOCK_PATH} is missing. Run \`npx @contextforge/cli sync\`.`
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
const cachedPacks = await loadProjectPacks(resolvedRoot);
|
|
1201
|
+
const cachedPackNames = new Set(cachedPacks.map((pack) => pack.manifest.name));
|
|
1202
|
+
for (const packName of config.installedPacks) {
|
|
1203
|
+
if (registryPacks.size > 0 && !registryPacks.has(packName)) {
|
|
1204
|
+
issues.push({
|
|
1205
|
+
level: "error",
|
|
1206
|
+
message: `${packName} is installed in config, but no longer exists in the registry.`
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
if (!cachedPackNames.has(packName)) {
|
|
1210
|
+
issues.push({
|
|
1211
|
+
level: "error",
|
|
1212
|
+
message: `.contextforge/packs/${packName}/pack.json is missing. Run \`npx @contextforge/cli sync\`.`
|
|
1213
|
+
});
|
|
799
1214
|
continue;
|
|
800
1215
|
}
|
|
801
|
-
|
|
802
|
-
|
|
1216
|
+
for (const output of expectedGeneratedOutputs(packName, config.tools)) {
|
|
1217
|
+
if (!await fileExists(resolvedRoot, output)) {
|
|
1218
|
+
issues.push({
|
|
1219
|
+
level: "error",
|
|
1220
|
+
message: `${output} is missing. Run \`npx @contextforge/cli sync\`.`
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const agentsMd = await readIfExists(resolvedRoot, "AGENTS.md");
|
|
1226
|
+
const claudeMd = await readIfExists(resolvedRoot, "CLAUDE.md");
|
|
1227
|
+
if (config.tools.includes("codex")) {
|
|
1228
|
+
if (hasContextForgeBlock(agentsMd)) {
|
|
1229
|
+
checks.push("AGENTS.md ContextForge block found");
|
|
1230
|
+
} else {
|
|
1231
|
+
issues.push({
|
|
1232
|
+
level: "error",
|
|
1233
|
+
message: "AGENTS.md is missing a ContextForge generated block. Run `npx @contextforge/cli sync`."
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if (config.tools.includes("claude")) {
|
|
1238
|
+
if (hasContextForgeBlock(claudeMd)) {
|
|
1239
|
+
checks.push("CLAUDE.md ContextForge block found");
|
|
803
1240
|
} else {
|
|
804
1241
|
issues.push({
|
|
805
1242
|
level: "error",
|
|
806
|
-
message:
|
|
1243
|
+
message: "CLAUDE.md is missing a ContextForge generated block. Run `npx @contextforge/cli sync`."
|
|
807
1244
|
});
|
|
808
1245
|
}
|
|
809
1246
|
}
|
|
810
|
-
|
|
811
|
-
|
|
1247
|
+
if (tooLarge(agentsMd)) {
|
|
1248
|
+
issues.push({
|
|
1249
|
+
level: "warning",
|
|
1250
|
+
message: "AGENTS.md is large. Root instructions should stay concise; detailed content belongs in pack files and skills."
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
if (tooLarge(claudeMd)) {
|
|
1254
|
+
issues.push({
|
|
1255
|
+
level: "warning",
|
|
1256
|
+
message: "CLAUDE.md is large. Root instructions should stay concise; detailed content belongs in pack files."
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
const gitWorkflow = cachedPacks.find((pack) => pack.manifest.name === "git-workflow");
|
|
1260
|
+
if (gitWorkflow) {
|
|
1261
|
+
const generatedGitFiles = await Promise.all(
|
|
1262
|
+
["codex", "claude", "cursor", "copilot"].map(
|
|
1263
|
+
(tool) => readIfExists(resolvedRoot, `.contextforge/agents/${tool}/git-workflow.md`)
|
|
1264
|
+
)
|
|
1265
|
+
);
|
|
1266
|
+
const gitSummary = [
|
|
1267
|
+
gitWorkflow.files.agents,
|
|
1268
|
+
gitWorkflow.files.claude,
|
|
1269
|
+
...generatedGitFiles,
|
|
1270
|
+
agentsMd,
|
|
1271
|
+
claudeMd
|
|
1272
|
+
].filter((content) => Boolean(content)).join("\n").toLowerCase();
|
|
1273
|
+
if (!gitSummary.includes("do not commit") || !gitSummary.includes("push") || !gitSummary.includes("explicitly")) {
|
|
812
1274
|
issues.push({
|
|
813
1275
|
level: "warning",
|
|
814
|
-
message:
|
|
1276
|
+
message: "git-workflow is installed, but the expected explicit-permission git safety warning was not found."
|
|
815
1277
|
});
|
|
816
1278
|
}
|
|
817
1279
|
}
|
|
818
|
-
for (const pack of
|
|
1280
|
+
for (const pack of cachedPacks) {
|
|
819
1281
|
if (!await packMatchesProject(pack, resolvedRoot, packageJson)) {
|
|
820
1282
|
issues.push({
|
|
821
1283
|
level: "warning",
|
|
822
|
-
message: `${pack.name}
|
|
1284
|
+
message: `${pack.manifest.name} is installed, but its detection hints do not match this project.`
|
|
823
1285
|
});
|
|
824
1286
|
}
|
|
825
1287
|
}
|
|
826
|
-
if (config.
|
|
1288
|
+
if (config.installedPacks.includes("test-driven-development") && !hasScript(packageJson, "test")) {
|
|
827
1289
|
issues.push({
|
|
828
1290
|
level: "warning",
|
|
829
|
-
message:
|
|
1291
|
+
message: "test-driven-development is installed, but package.json has no test script."
|
|
830
1292
|
});
|
|
831
1293
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1294
|
+
for (const recommendedPack of recommendPackNames(analysis)) {
|
|
1295
|
+
if (!config.installedPacks.includes(recommendedPack) && registryPacks.has(recommendedPack)) {
|
|
1296
|
+
issues.push({
|
|
1297
|
+
level: "warning",
|
|
1298
|
+
message: `${recommendedPack} matches the detected stack but is not installed. Run \`npx @contextforge/cli add ${recommendedPack}\` if you want it.`
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
837
1301
|
}
|
|
838
1302
|
return { checks, issues };
|
|
839
1303
|
}
|
|
840
1304
|
export {
|
|
841
1305
|
CONFIG_PATH,
|
|
842
1306
|
ConfigSchema,
|
|
1307
|
+
DEFAULT_CORE_PACKS,
|
|
843
1308
|
DEFAULT_REGISTRY_SOURCES,
|
|
844
1309
|
DEFAULT_TOOLS,
|
|
845
1310
|
GENERATED_BLOCK_END,
|
|
846
1311
|
GENERATED_BLOCK_START,
|
|
1312
|
+
LOCK_PATH,
|
|
847
1313
|
OFFICIAL_REGISTRY_SOURCE,
|
|
848
1314
|
OFFICIAL_REGISTRY_URL,
|
|
849
1315
|
PROJECT_PACK_CACHE,
|
|
1316
|
+
PackFileSchema,
|
|
1317
|
+
PackFileTypeSchema,
|
|
1318
|
+
PackManifestSchema,
|
|
850
1319
|
PackSchema,
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1320
|
+
RegistryIndexSchema,
|
|
1321
|
+
RegistryPackSourceSchema,
|
|
1322
|
+
RegistryPackSummarySchema,
|
|
1323
|
+
ToolSchema,
|
|
854
1324
|
addPackToConfig,
|
|
855
|
-
|
|
1325
|
+
compileAgentsMd,
|
|
1326
|
+
compileClaudeMd,
|
|
856
1327
|
compileOutputs,
|
|
857
1328
|
createConfig,
|
|
858
1329
|
detectPackageManager,
|
|
859
1330
|
detectProject,
|
|
860
1331
|
doctorProject,
|
|
1332
|
+
downloadPackToContextForge,
|
|
1333
|
+
fetchPackFile,
|
|
1334
|
+
fetchPackManifest,
|
|
1335
|
+
fetchRegistry,
|
|
1336
|
+
fetchText,
|
|
861
1337
|
findPack,
|
|
1338
|
+
findPackSummary,
|
|
1339
|
+
generateToolOutputs,
|
|
862
1340
|
getGeneratedBlock,
|
|
863
1341
|
hasPackage,
|
|
864
1342
|
hasScript,
|
|
1343
|
+
installPack,
|
|
1344
|
+
installPackAndSync,
|
|
1345
|
+
listRegistryPacks,
|
|
865
1346
|
loadConfig,
|
|
1347
|
+
loadLock,
|
|
1348
|
+
loadProjectPacks,
|
|
866
1349
|
loadRegistry,
|
|
1350
|
+
loadRemotePack,
|
|
1351
|
+
mandatoryCorePacks,
|
|
1352
|
+
manifestMatchesProject,
|
|
1353
|
+
missingMandatoryCorePacks,
|
|
1354
|
+
normalizeConfig,
|
|
867
1355
|
packMatchesProject,
|
|
868
1356
|
packageManagerLabel,
|
|
1357
|
+
pathExists,
|
|
1358
|
+
readInstalledPacks,
|
|
869
1359
|
readPackageJson,
|
|
1360
|
+
recommendPackNames,
|
|
870
1361
|
recommendPacks,
|
|
1362
|
+
registrySourceToUrl,
|
|
1363
|
+
removeGeneratedBlock,
|
|
1364
|
+
resolvePackFileUrl,
|
|
1365
|
+
resolvePackUrl,
|
|
871
1366
|
resolvePacks,
|
|
872
1367
|
safeWriteFile,
|
|
873
1368
|
saveConfig,
|
|
874
|
-
|
|
1369
|
+
saveLock,
|
|
1370
|
+
searchRegistryPacks,
|
|
1371
|
+
syncInstalledPacks,
|
|
875
1372
|
syncProject,
|
|
1373
|
+
updateContextForgeConfig,
|
|
1374
|
+
updateContextForgeLock,
|
|
876
1375
|
updateGeneratedBlock,
|
|
877
1376
|
wrapGeneratedBlock,
|
|
878
1377
|
writeGeneratedFiles
|