@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +297 -131
  2. package/dist/index.js +931 -432
  3. 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 PackSchema = z.object({
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
- description: z.string().min(1),
173
- category: z.enum(["framework", "database", "testing", "security", "ui", "workflow"]),
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
- outputs: z.object({
179
- globalRules: z.boolean().default(true),
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 loadLocalRegistry(registryRoot, source) {
213
- if (!await fs7.pathExists(registryRoot)) {
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(registryRoot, { withFileTypes: true });
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 directory = path7.join(registryRoot, entry.name);
223
- const packPath = path7.join(directory, "pack.json");
224
- if (!await fs7.pathExists(packPath)) {
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, required, timeoutMs) {
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 resolveUrl(value, baseUrl) {
275
- return value ? new URL(value, baseUrl).toString() : void 0;
301
+ function resolvePackUrl(registryUrl, packPath) {
302
+ return new URL(packPath, registryUrl).toString();
276
303
  }
277
- function defaultPackFile(baseUrl, fileName) {
278
- return baseUrl ? new URL(fileName, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString() : void 0;
304
+ function resolvePackFileUrl(packUrl, filePath) {
305
+ return new URL(filePath, packUrl).toString();
279
306
  }
280
- async function loadRemoteRegistry(registryUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
281
- const indexText = await fetchText(registryUrl, true, timeoutMs);
282
- const index = RemoteRegistryIndexSchema.parse(JSON.parse(indexText ?? "{}"));
283
- const packs = [];
284
- for (const entry of index.packs) {
285
- const baseUrl = resolveUrl(entry.baseUrl, registryUrl);
286
- const packUrl = resolveUrl(entry.files?.pack, registryUrl) ?? defaultPackFile(baseUrl, "pack.json");
287
- const rawPack = entry.pack ?? (packUrl ? JSON.parse(await fetchText(packUrl, true, timeoutMs) ?? "{}") : void 0);
288
- const parsed = PackSchema.parse({
289
- ...rawPack,
290
- name: rawPack?.name ?? entry.name,
291
- version: rawPack?.version ?? entry.version
292
- });
293
- const rulesUrl = resolveUrl(entry.files?.rules, registryUrl) ?? defaultPackFile(baseUrl, "rules.md");
294
- if (!rulesUrl) {
295
- throw new Error(`Remote pack "${entry.name}" is missing a rules.md URL.`);
296
- }
297
- packs.push({
298
- ...parsed,
299
- directory: baseUrl ?? registryUrl,
300
- source: "remote",
301
- registryUrl,
302
- files: {
303
- rules: await fetchText(rulesUrl, true, timeoutMs) ?? "",
304
- skill: await fetchText(
305
- resolveUrl(entry.files?.skill, registryUrl) ?? defaultPackFile(baseUrl, "skill.md") ?? "",
306
- false,
307
- timeoutMs
308
- ),
309
- cursor: await fetchText(
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 packs;
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 mergePacks(packs) {
331
- const byName = /* @__PURE__ */ new Map();
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 mergePacks(await loadLocalRegistry(input, "local"));
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
- for (const source of sources) {
359
- packs.push(...await loadRegistrySource(source, input.timeoutMs));
360
- }
361
- return mergePacks(packs);
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 resolvePacks(packNames, registry) {
372
- const uniqueNames = [...new Set(packNames)];
373
- return uniqueNames.map((packName) => {
374
- const pack = findPack(registry, packName);
375
- if (!pack) {
376
- throw new Error(`Unknown ContextForge pack: ${packName}`);
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 hasDetectHints(pack) {
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
- pack.detect?.files?.map((filePattern) => hasDetectFile(root, filePattern)) ?? []
455
+ manifest.detect?.files?.map((filePattern) => hasDetectFile(root, filePattern)) ?? []
412
456
  );
413
- const packageChecks = pack.detect?.packages?.map((packageName) => hasPackage(packageJson, packageName)) ?? [];
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
- // src/config/configSchema.ts
429
- import { z as z2 } from "zod";
430
- var ToolSchema = z2.enum(["codex", "claude", "cursor", "copilot"]);
431
- var ConfigSchema = z2.object({
432
- version: z2.string().default("0.1.0"),
433
- registries: z2.array(z2.string()).default(DEFAULT_REGISTRY_SOURCES),
434
- tools: z2.array(ToolSchema).default(["codex", "claude", "cursor", "copilot"]),
435
- packs: z2.array(z2.string()).default([]),
436
- packageManager: z2.enum(["pnpm", "npm", "yarn", "bun", "unknown"]).default("unknown"),
437
- generatedFiles: z2.array(z2.string()).default([])
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
- var DEFAULT_TOOLS = ["codex", "claude", "cursor", "copilot"];
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
- registries,
514
+ registry,
446
515
  tools,
447
- packs: packs.map((pack) => pack.name),
448
- packageManager: analysis.packageManager,
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/loadConfig.ts
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 = path10.join(root, CONFIG_PATH);
459
- if (!await fs9.pathExists(configPath)) {
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 fs9.readJson(configPath));
578
+ return ConfigSchema.parse(await fs10.readJson(configPath));
463
579
  }
464
580
 
465
581
  // src/config/saveConfig.ts
466
- import path11 from "path";
467
- import fs10 from "fs-extra";
582
+ import path12 from "path";
583
+ import fs11 from "fs-extra";
468
584
  async function saveConfig(root, config) {
469
- const configPath = path11.join(root, CONFIG_PATH);
470
- await fs10.ensureDir(path11.dirname(configPath));
471
- await fs10.writeFile(configPath, `${JSON.stringify(config, null, 2)}
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
- packs: [.../* @__PURE__ */ new Set([...config.packs, packName])]
593
+ installedPacks: [.../* @__PURE__ */ new Set([...config.installedPacks, packName])]
478
594
  };
479
595
  }
480
596
 
481
597
  // src/registry/installPacks.ts
482
- import path12 from "path";
483
- import fs11 from "fs-extra";
484
- var INSTALLED_PACKS_PATH = ".contextforge/installed-packs.json";
485
- function packJson(pack) {
486
- return {
487
- name: pack.name,
488
- version: pack.version,
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
- async function writePack(root, pack) {
497
- const packRoot = path12.join(root, PROJECT_PACK_CACHE, pack.name);
498
- await fs11.ensureDir(packRoot);
499
- await fs11.writeFile(path12.join(packRoot, "pack.json"), `${JSON.stringify(packJson(pack), null, 2)}
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
- await fs11.writeFile(path12.join(packRoot, "rules.md"), pack.files.rules);
502
- const optionalFiles = [
503
- ["skill.md", pack.files.skill],
504
- ["cursor.mdc", pack.files.cursor],
505
- ["copilot.md", pack.files.copilot]
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 cacheRemotePacks(root, packs) {
514
- for (const pack of packs) {
515
- if (pack.source === "remote") {
516
- await writePack(root, pack);
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
- async function saveInstalledPacks(root, packs) {
521
- const metadataPath = path12.join(root, INSTALLED_PACKS_PATH);
522
- const metadata = {
523
- version: "1",
524
- packs: packs.map((pack) => ({
525
- name: pack.name,
526
- version: pack.version ?? "0.0.0",
527
- source: pack.source,
528
- registryUrl: pack.registryUrl
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/compileCursorRules.ts
628
- function withCursorFrontmatter(description, content) {
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
- content.trim(),
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 compileCursorRules(analysis, packs) {
641
- const files = [
642
- {
643
- path: ".cursor/rules/contextforge.mdc",
644
- content: withCursorFrontmatter(
645
- "ContextForge generated project overview",
646
- [
647
- "# ContextForge Project Context",
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
- });
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: compileAgentsMd(analysis, packs) });
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: compileClaudeMd(analysis, packs) });
787
+ outputs.push({ path: "CLAUDE.md", content: compileRootClaude(packs) });
698
788
  }
699
- if (config.tools.includes("cursor")) {
700
- outputs.push(...compileCursorRules(analysis, packs));
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
- if (config.tools.includes("copilot")) {
703
- outputs.push({
704
- path: ".github/copilot-instructions.md",
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 outputs;
799
+ return ["# ContextForge Git Safety", "", warning, "", content.trim()].join("\n");
709
800
  }
710
801
 
711
- // src/compiler/writeGeneratedFiles.ts
712
- import path14 from "path";
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 path13 from "path";
716
- import fs12 from "fs-extra";
845
+ import path14 from "path";
846
+ import fs13 from "fs-extra";
717
847
  async function safeWriteFile(filePath, generatedContent) {
718
- const existingContent = await fs12.pathExists(filePath) ? await fs12.readFile(filePath, "utf8") : null;
848
+ const existingContent = await fs13.pathExists(filePath) ? await fs13.readFile(filePath, "utf8") : null;
719
849
  const nextContent = updateGeneratedBlock(existingContent, generatedContent);
720
- await fs12.ensureDir(path13.dirname(filePath));
721
- await fs12.writeFile(filePath, nextContent);
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
- async function writeGeneratedFiles(root, outputs) {
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(path14.join(root, output.path), output.content);
986
+ await safeWriteFile(path17.join(root, output.path), output.content);
728
987
  }
729
- return outputs.map((output) => output.path);
988
+ await cleanupStaleGeneratedFiles(root, previousFiles, currentFiles);
989
+ return currentFiles;
730
990
  }
731
991
 
732
992
  // src/sync.ts
733
- import path15 from "path";
734
- async function syncProject(root, providedConfig) {
735
- const resolvedRoot = path15.resolve(root);
736
- const analysis = await detectProject(resolvedRoot);
737
- const config = providedConfig ?? await loadConfig(resolvedRoot);
738
- const registry = await loadRegistry({
739
- root: resolvedRoot,
740
- sources: config.registries
741
- });
742
- const packs = resolvePacks(config.packs, registry);
743
- await cacheRemotePacks(resolvedRoot, packs);
744
- await saveInstalledPacks(resolvedRoot, packs);
745
- const outputs = compileOutputs(config, packs, analysis);
746
- const generatedFiles = await writeGeneratedFiles(resolvedRoot, outputs);
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
- packageManager: config.packageManager === "unknown" ? analysis.packageManager : config.packageManager,
1010
+ registry: registryUrl,
750
1011
  generatedFiles
751
1012
  };
752
- await saveConfig(resolvedRoot, nextConfig);
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: resolvedRoot,
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 path16 from "path";
764
- import fs13 from "fs-extra";
1121
+ import path19 from "path";
1122
+ import fs17 from "fs-extra";
765
1123
  async function fileExists(root, relativePath) {
766
- return fs13.pathExists(path16.join(root, relativePath));
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 = path16.resolve(root);
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 [analysis, registry, packageJson] = await Promise.all([
1175
+ const registryUrl = options.registry ?? config.registry;
1176
+ const [analysis, packageJson, lock] = await Promise.all([
786
1177
  detectProject(resolvedRoot),
787
- loadRegistry({ root: resolvedRoot, sources: config.registries }),
788
- readPackageJson(resolvedRoot)
1178
+ readPackageJson(resolvedRoot),
1179
+ loadLock(resolvedRoot)
789
1180
  ]);
790
- const packs = resolvePacks(config.packs, registry);
791
- const requiredFiles = [
792
- [config.tools.includes("codex"), "AGENTS.md", "Codex instructions found"],
793
- [config.tools.includes("claude"), "CLAUDE.md", "Claude instructions found"],
794
- [config.tools.includes("cursor"), ".cursor/rules", "Cursor rules found"],
795
- [config.tools.includes("copilot"), ".github/copilot-instructions.md", "Copilot instructions found"]
796
- ];
797
- for (const [enabled, relativePath, okMessage] of requiredFiles) {
798
- if (!enabled) {
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
- if (await fileExists(resolvedRoot, relativePath)) {
802
- checks.push(okMessage);
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: `${relativePath} is missing. Run \`npx @contextforge/cli sync\`.`
1243
+ message: "CLAUDE.md is missing a ContextForge generated block. Run `npx @contextforge/cli sync`."
807
1244
  });
808
1245
  }
809
1246
  }
810
- for (const generatedFile of config.generatedFiles) {
811
- if (!await fileExists(resolvedRoot, generatedFile)) {
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: `Previously generated file ${generatedFile} is missing.`
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 packs) {
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} pack is installed, but its detection hints do not match this project.`
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.packageManager !== "unknown" && analysis.packageManager !== "unknown" && config.packageManager !== analysis.packageManager) {
1288
+ if (config.installedPacks.includes("test-driven-development") && !hasScript(packageJson, "test")) {
827
1289
  issues.push({
828
1290
  level: "warning",
829
- message: `Config says package manager is ${config.packageManager}, but ${analysis.packageManager} was detected.`
1291
+ message: "test-driven-development is installed, but package.json has no test script."
830
1292
  });
831
1293
  }
832
- if (config.packs.includes("testing-workflow") && !hasScript(packageJson, "test")) {
833
- issues.push({
834
- level: "warning",
835
- message: "testing-workflow is installed, but package.json has no test script."
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
- RemotePackEntrySchema,
852
- RemotePackFilesSchema,
853
- RemoteRegistryIndexSchema,
1320
+ RegistryIndexSchema,
1321
+ RegistryPackSourceSchema,
1322
+ RegistryPackSummarySchema,
1323
+ ToolSchema,
854
1324
  addPackToConfig,
855
- cacheRemotePacks,
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
- saveInstalledPacks,
1369
+ saveLock,
1370
+ searchRegistryPacks,
1371
+ syncInstalledPacks,
875
1372
  syncProject,
1373
+ updateContextForgeConfig,
1374
+ updateContextForgeLock,
876
1375
  updateGeneratedBlock,
877
1376
  wrapGeneratedBlock,
878
1377
  writeGeneratedFiles