@contextforge/core 0.1.1 → 0.1.5

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