@contextforge/core 0.1.2 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +297 -131
  2. package/dist/index.js +976 -422
  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,170 +494,296 @@ 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
658
  }
535
659
 
536
- // src/compiler/compileAgentsMd.ts
537
- 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
+ ] : [];
538
701
  return [
539
- "# ContextForge Instructions",
702
+ "# Project Agent Instructions",
703
+ "",
704
+ "Generated by ContextForge from installed instruction packs.",
705
+ "",
706
+ "## Core Behavior",
707
+ "",
708
+ summaries.length > 0 ? summaries.join("\n\n") : "No pack-specific agent summaries are installed yet.",
540
709
  "",
541
- "Generated by ContextForge. Add project-specific guidance outside this block.",
710
+ ...gitSafetyWarning,
711
+ "## Installed Packs",
712
+ "",
713
+ ...packs.length > 0 ? packs.map((pack) => `- ${pack.manifest.name}: ${pack.manifest.title}`) : ["- No packs installed"],
542
714
  "",
543
715
  "## Detected Project",
716
+ "",
544
717
  `- Framework: ${analysis.framework}`,
545
718
  `- Language: ${analysis.language}`,
546
- `- Package manager: ${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
- ])
719
+ `- Package manager: ${analysis.packageManager}`,
720
+ "",
721
+ "## Notes",
722
+ "",
723
+ "Detailed Codex skills live in `.agents/skills`.",
724
+ "Pack sources live in `.contextforge/packs`.",
725
+ "Pack-specific agent summaries live in `.contextforge/instructions/agents`."
564
726
  ].join("\n");
565
727
  }
566
-
567
- // 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);
728
+ function compileRootClaude(packs) {
729
+ const summaries = packs.map((pack) => pack.files.claude?.trim()).filter((summary) => Boolean(summary));
730
+ const gitSafetyWarning = packs.some((pack) => pack.manifest.name === "git-workflow") ? [
731
+ "## Git Safety",
732
+ "",
733
+ "Do not commit, push, merge, rebase, reset, delete branches, or rewrite history unless explicitly requested by the user.",
734
+ ""
735
+ ] : [];
578
736
  return [
579
- "# ContextForge Copilot Instructions",
737
+ "# Claude Code Instructions",
738
+ "",
739
+ "Generated by ContextForge from installed instruction packs.",
740
+ "",
741
+ "## Core Behavior",
580
742
  "",
581
- `Project framework: ${analysis.framework}. Language: ${analysis.language}.`,
743
+ summaries.length > 0 ? summaries.join("\n\n") : "No pack-specific Claude summaries are installed yet.",
582
744
  "",
583
- ...enabledPacks.flatMap((pack) => [
584
- `## ${pack.title}`,
585
- "",
586
- (pack.files.copilot ?? pack.files.rules).trim(),
587
- ""
588
- ])
745
+ ...gitSafetyWarning,
746
+ "## Installed Packs",
747
+ "",
748
+ ...packs.length > 0 ? packs.map((pack) => `- ${pack.manifest.name}: ${pack.manifest.title}`) : ["- No packs installed"],
749
+ "",
750
+ "## Notes",
751
+ "",
752
+ "Keep this file concise.",
753
+ "Detailed pack sources live in `.contextforge/packs`.",
754
+ "Pack-specific Claude summaries live in `.contextforge/instructions/claude`."
589
755
  ].join("\n");
590
756
  }
757
+ function compileOutputs(config, packs, analysis) {
758
+ const outputs = [];
759
+ for (const pack of packs) {
760
+ for (const file of pack.manifest.files) {
761
+ if (!shouldGenerateFile(file.type, config.tools)) {
762
+ continue;
763
+ }
764
+ const content = pack.files[file.type];
765
+ const outputPath = file.output ?? defaultOutput(pack.manifest.name, file.type);
766
+ if (!content || !outputPath) {
767
+ continue;
768
+ }
769
+ outputs.push({
770
+ path: normalizeOutputPath(outputPath),
771
+ content
772
+ });
773
+ }
774
+ }
775
+ if (config.tools.includes("codex")) {
776
+ outputs.push({ path: "AGENTS.md", content: compileRootAgents(packs, analysis) });
777
+ }
778
+ if (config.tools.includes("claude")) {
779
+ outputs.push({ path: "CLAUDE.md", content: compileRootClaude(packs) });
780
+ }
781
+ return outputs;
782
+ }
783
+
784
+ // src/compiler/compileAgentsMd.ts
785
+ import path14 from "path";
786
+ import fs13 from "fs-extra";
591
787
 
592
788
  // src/fs/updateGeneratedBlock.ts
593
789
  var GENERATED_BLOCK_START = "<!-- contextforge:start -->";
@@ -602,6 +798,9 @@ function escapeRegExp(value) {
602
798
  function getGeneratedBlock(content) {
603
799
  return content.match(blockPattern)?.[0] ?? null;
604
800
  }
801
+ function removeGeneratedBlock(content) {
802
+ return content.replace(blockPattern, "").replace(/\s+$/u, "");
803
+ }
605
804
  function wrapGeneratedBlock(content) {
606
805
  return `${GENERATED_BLOCK_START}
607
806
  ${content.trim()}
@@ -624,151 +823,408 @@ ${generatedBlock}
624
823
  `;
625
824
  }
626
825
 
627
- // src/compiler/compileCursorRules.ts
628
- 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
+ ] : [];
629
844
  return [
630
- "---",
631
- `description: ${description}`,
632
- "alwaysApply: true",
633
- "---",
845
+ "# Project Agent Instructions",
634
846
  "",
635
847
  GENERATED_BLOCK_START,
636
- 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
+ "",
637
865
  GENERATED_BLOCK_END
638
866
  ].join("\n");
639
867
  }
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
- });
868
+
869
+ // src/compiler/compileClaudeMd.ts
870
+ import path15 from "path";
871
+ import fs14 from "fs-extra";
872
+ var CLAUDE_INSTRUCTIONS_DIR = ".contextforge/instructions/claude";
873
+ async function readInstructionSummaries2(root) {
874
+ const directory = path15.join(root, CLAUDE_INSTRUCTIONS_DIR);
875
+ if (!await fs14.pathExists(directory)) {
876
+ return [];
665
877
  }
666
- 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")));
667
880
  }
668
-
669
- // src/compiler/compileSkills.ts
670
- function escapeYaml(value) {
671
- 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");
672
912
  }
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
- }));
913
+
914
+ // src/compiler/generateToolOutputs.ts
915
+ import path18 from "path";
916
+ import fs17 from "fs-extra";
917
+
918
+ // src/fs/safeWriteFile.ts
919
+ import path16 from "path";
920
+ import fs15 from "fs-extra";
921
+ async function safeWriteFile(filePath, generatedContent) {
922
+ const existingContent = await fs15.pathExists(filePath) ? await fs15.readFile(filePath, "utf8") : null;
923
+ const nextContent = updateGeneratedBlock(existingContent, generatedContent);
924
+ await fs15.ensureDir(path16.dirname(filePath));
925
+ await fs15.writeFile(filePath, nextContent);
687
926
  }
688
927
 
689
- // src/compiler/compileOutputs.ts
690
- function compileOutputs(config, packs, analysis) {
691
- const outputs = [];
692
- if (config.tools.includes("codex")) {
693
- outputs.push({ path: "AGENTS.md", content: compileAgentsMd(analysis, packs) });
694
- 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;
695
938
  }
696
- if (config.tools.includes("claude")) {
697
- outputs.push({ path: "CLAUDE.md", content: compileClaudeMd(analysis, packs) });
939
+ if (isGeneratedOnlyPath(relativePath)) {
940
+ await fs16.remove(filePath);
941
+ return;
698
942
  }
699
- if (config.tools.includes("cursor")) {
700
- outputs.push(...compileCursorRules(analysis, packs));
943
+ const content = await fs16.readFile(filePath, "utf8");
944
+ if (!getGeneratedBlock(content)) {
945
+ return;
701
946
  }
702
- if (config.tools.includes("copilot")) {
703
- outputs.push({
704
- path: ".github/copilot-instructions.md",
705
- content: compileCopilotInstructions(analysis, packs)
706
- });
947
+ const nextContent = removeGeneratedBlock(content);
948
+ if (nextContent.trim().length === 0) {
949
+ await fs16.remove(filePath);
950
+ return;
951
+ }
952
+ await fs16.writeFile(filePath, `${nextContent}
953
+ `);
954
+ }
955
+ async function cleanupStaleGeneratedFiles(root, previousFiles, currentFiles) {
956
+ const current = new Set(currentFiles);
957
+ const staleFiles = [...new Set(previousFiles)].filter((file) => !current.has(file));
958
+ for (const staleFile of staleFiles) {
959
+ await cleanupFile(root, staleFile);
707
960
  }
708
- return outputs;
709
961
  }
710
962
 
711
- // src/compiler/writeGeneratedFiles.ts
712
- import path14 from "path";
713
-
714
- // src/fs/safeWriteFile.ts
715
- import path13 from "path";
716
- import fs12 from "fs-extra";
717
- async function safeWriteFile(filePath, generatedContent) {
718
- const existingContent = await fs12.pathExists(filePath) ? await fs12.readFile(filePath, "utf8") : null;
719
- const nextContent = updateGeneratedBlock(existingContent, generatedContent);
720
- await fs12.ensureDir(path13.dirname(filePath));
721
- 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;
722
1045
  }
723
1046
 
724
1047
  // src/compiler/writeGeneratedFiles.ts
725
- 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);
726
1051
  for (const output of outputs) {
727
- await safeWriteFile(path14.join(root, output.path), output.content);
1052
+ await safeWriteFile(path19.join(root, output.path), output.content);
728
1053
  }
729
- return outputs.map((output) => output.path);
1054
+ await cleanupStaleGeneratedFiles(root, previousFiles, currentFiles);
1055
+ return currentFiles;
730
1056
  }
731
1057
 
732
1058
  // 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);
1059
+ import path20 from "path";
1060
+ import fs18 from "fs-extra";
1061
+ async function updateContextForgeConfig(projectRoot, packName, registryUrl, generatedFiles) {
1062
+ const existing = await loadOptionalConfig(projectRoot);
1063
+ const config = addPackToConfig(
1064
+ existing ?? {
1065
+ version: "0.1.0",
1066
+ registry: registryUrl,
1067
+ tools: ["codex", "claude", "cursor", "copilot"],
1068
+ installedPacks: [],
1069
+ defaultCorePacks: [],
1070
+ generatedFiles: []
1071
+ },
1072
+ packName
1073
+ );
747
1074
  const nextConfig = {
748
1075
  ...config,
749
- packageManager: config.packageManager === "unknown" ? analysis.packageManager : config.packageManager,
1076
+ registry: registryUrl,
750
1077
  generatedFiles
751
1078
  };
752
- 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
+ }
753
1134
  return {
754
- root: resolvedRoot,
1135
+ root,
755
1136
  analysis,
756
1137
  generatedFiles,
757
- outputs,
758
- config: nextConfig
1138
+ outputs: generatedFiles.map((file) => ({ path: file, content: "" })),
1139
+ config: nextConfig,
1140
+ installedPacks: installed
759
1141
  };
760
1142
  }
1143
+ async function syncProject(root, providedConfig) {
1144
+ return syncInstalledPacks(root, providedConfig);
1145
+ }
1146
+ async function installPackAndSync(projectRoot, registryUrl, packName, options = {}) {
1147
+ const root = path20.resolve(projectRoot);
1148
+ const config = await loadOptionalConfig(root);
1149
+ const result = await installPack(root, registryUrl, packName, options);
1150
+ if (result.alreadyInstalled && !options.force && config?.installedPacks.includes(packName)) {
1151
+ return {
1152
+ ...result,
1153
+ generatedFiles: [],
1154
+ config: config ?? void 0
1155
+ };
1156
+ }
1157
+ if (options.dryRun) {
1158
+ return {
1159
+ ...result,
1160
+ generatedFiles: [],
1161
+ config: config ?? void 0
1162
+ };
1163
+ }
1164
+ const nextConfig = {
1165
+ version: "0.1.0",
1166
+ registry: registryUrl,
1167
+ tools: options.tools ?? config?.tools ?? ["codex", "claude", "cursor", "copilot"],
1168
+ installedPacks: [.../* @__PURE__ */ new Set([...config?.installedPacks ?? [], packName])],
1169
+ defaultCorePacks: config?.defaultCorePacks ?? [],
1170
+ generatedFiles: config?.generatedFiles ?? []
1171
+ };
1172
+ const syncResult = await syncInstalledPacks(root, nextConfig);
1173
+ return {
1174
+ ...result,
1175
+ generatedFiles: syncResult.generatedFiles,
1176
+ config: syncResult.config
1177
+ };
1178
+ }
1179
+ async function readInstalledPacks(projectRoot) {
1180
+ return loadProjectPacks(projectRoot);
1181
+ }
1182
+ async function pathExists(root, relativePath) {
1183
+ return fs18.pathExists(path20.join(root, relativePath));
1184
+ }
761
1185
 
762
1186
  // src/doctor/doctorProject.ts
763
- import path16 from "path";
764
- import fs13 from "fs-extra";
1187
+ import path21 from "path";
1188
+ import fs19 from "fs-extra";
765
1189
  async function fileExists(root, relativePath) {
766
- return fs13.pathExists(path16.join(root, relativePath));
1190
+ return fs19.pathExists(path21.join(root, relativePath));
1191
+ }
1192
+ async function readIfExists(root, relativePath) {
1193
+ const filePath = path21.join(root, relativePath);
1194
+ if (!await fs19.pathExists(filePath)) {
1195
+ return null;
1196
+ }
1197
+ return fs19.readFile(filePath, "utf8");
1198
+ }
1199
+ function hasContextForgeBlock(content) {
1200
+ return Boolean(content && getGeneratedBlock(content));
1201
+ }
1202
+ function tooLarge(content) {
1203
+ return Boolean(content && content.length > 2e4);
767
1204
  }
768
- async function doctorProject(root) {
1205
+ function expectedGeneratedOutputs(packName, tools) {
1206
+ const outputs = [];
1207
+ if (tools.includes("codex") || tools.includes("claude")) {
1208
+ outputs.push(`.agents/skills/${packName}/SKILL.md`);
1209
+ }
1210
+ if (tools.includes("cursor")) {
1211
+ outputs.push(`.cursor/rules/${packName}.mdc`);
1212
+ }
1213
+ if (tools.includes("copilot")) {
1214
+ outputs.push(`.github/instructions/${packName}.instructions.md`);
1215
+ }
1216
+ if (tools.includes("codex")) {
1217
+ outputs.push(`.contextforge/instructions/agents/${packName}.md`);
1218
+ }
1219
+ if (tools.includes("claude")) {
1220
+ outputs.push(`.contextforge/instructions/claude/${packName}.md`);
1221
+ }
1222
+ return outputs;
1223
+ }
1224
+ async function doctorProject(root, options = {}) {
769
1225
  const checks = [];
770
1226
  const issues = [];
771
- const resolvedRoot = path16.resolve(root);
1227
+ const resolvedRoot = path21.resolve(root);
772
1228
  if (!await fileExists(resolvedRoot, CONFIG_PATH)) {
773
1229
  return {
774
1230
  checks,
@@ -782,97 +1238,195 @@ async function doctorProject(root) {
782
1238
  }
783
1239
  checks.push("Config found");
784
1240
  const config = await loadConfig(resolvedRoot);
785
- const [analysis, registry, packageJson] = await Promise.all([
1241
+ const registryUrl = options.registry ?? config.registry;
1242
+ const [analysis, packageJson, lock] = await Promise.all([
786
1243
  detectProject(resolvedRoot),
787
- loadRegistry({ root: resolvedRoot, sources: config.registries }),
788
- readPackageJson(resolvedRoot)
1244
+ readPackageJson(resolvedRoot),
1245
+ loadLock(resolvedRoot)
789
1246
  ]);
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) {
1247
+ let registryPacks = /* @__PURE__ */ new Set();
1248
+ try {
1249
+ const registry = await fetchRegistry(registryUrl);
1250
+ registryPacks = new Set(registry.packs.map((pack) => pack.name));
1251
+ checks.push("Registry reachable");
1252
+ } catch (error) {
1253
+ issues.push({
1254
+ level: "error",
1255
+ message: `Registry ${registryUrl} is not reachable: ${error instanceof Error ? error.message : String(error)}`
1256
+ });
1257
+ }
1258
+ if (lock) {
1259
+ checks.push("Lock file found");
1260
+ } else {
1261
+ issues.push({
1262
+ level: "warning",
1263
+ message: `${LOCK_PATH} is missing. Run \`npx @contextforge/cli sync\`.`
1264
+ });
1265
+ }
1266
+ const cachedPacks = await loadProjectPacks(resolvedRoot);
1267
+ const cachedPackNames = new Set(cachedPacks.map((pack) => pack.manifest.name));
1268
+ for (const packName of config.installedPacks) {
1269
+ if (registryPacks.size > 0 && !registryPacks.has(packName)) {
1270
+ issues.push({
1271
+ level: "error",
1272
+ message: `${packName} is installed in config, but no longer exists in the registry.`
1273
+ });
1274
+ }
1275
+ if (!cachedPackNames.has(packName)) {
1276
+ issues.push({
1277
+ level: "error",
1278
+ message: `.contextforge/packs/${packName}/pack.json is missing. Run \`npx @contextforge/cli sync\`.`
1279
+ });
799
1280
  continue;
800
1281
  }
801
- if (await fileExists(resolvedRoot, relativePath)) {
802
- 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");
1296
+ } else {
1297
+ issues.push({
1298
+ level: "error",
1299
+ message: "AGENTS.md is missing a ContextForge generated block. Run `npx @contextforge/cli sync`."
1300
+ });
1301
+ }
1302
+ }
1303
+ if (config.tools.includes("claude")) {
1304
+ if (hasContextForgeBlock(claudeMd)) {
1305
+ checks.push("CLAUDE.md ContextForge block found");
803
1306
  } else {
804
1307
  issues.push({
805
1308
  level: "error",
806
- message: `${relativePath} is missing. Run \`npx @contextforge/cli sync\`.`
1309
+ message: "CLAUDE.md is missing a ContextForge generated block. Run `npx @contextforge/cli sync`."
807
1310
  });
808
1311
  }
809
1312
  }
810
- for (const generatedFile of config.generatedFiles) {
811
- if (!await fileExists(resolvedRoot, generatedFile)) {
1313
+ if (tooLarge(agentsMd)) {
1314
+ issues.push({
1315
+ level: "warning",
1316
+ message: "AGENTS.md is large. Root instructions should stay concise; detailed content belongs in pack files and skills."
1317
+ });
1318
+ }
1319
+ if (tooLarge(claudeMd)) {
1320
+ issues.push({
1321
+ level: "warning",
1322
+ message: "CLAUDE.md is large. Root instructions should stay concise; detailed content belongs in pack files."
1323
+ });
1324
+ }
1325
+ const gitWorkflow = cachedPacks.find((pack) => pack.manifest.name === "git-workflow");
1326
+ if (gitWorkflow) {
1327
+ const gitSummary = [gitWorkflow.files.agents, gitWorkflow.files.claude, agentsMd, claudeMd].filter((content) => Boolean(content)).join("\n").toLowerCase();
1328
+ if (!gitSummary.includes("do not commit") || !gitSummary.includes("push") || !gitSummary.includes("explicitly")) {
812
1329
  issues.push({
813
1330
  level: "warning",
814
- message: `Previously generated file ${generatedFile} is missing.`
1331
+ message: "git-workflow is installed, but the expected explicit-permission git safety warning was not found."
815
1332
  });
816
1333
  }
817
1334
  }
818
- for (const pack of packs) {
1335
+ for (const pack of cachedPacks) {
819
1336
  if (!await packMatchesProject(pack, resolvedRoot, packageJson)) {
820
1337
  issues.push({
821
1338
  level: "warning",
822
- message: `${pack.name} 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.`
823
1340
  });
824
1341
  }
825
1342
  }
826
- if (config.packageManager !== "unknown" && analysis.packageManager !== "unknown" && config.packageManager !== analysis.packageManager) {
1343
+ if (config.installedPacks.includes("test-driven-development") && !hasScript(packageJson, "test")) {
827
1344
  issues.push({
828
1345
  level: "warning",
829
- 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."
830
1347
  });
831
1348
  }
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
- });
1349
+ for (const recommendedPack of recommendPackNames(analysis)) {
1350
+ if (!config.installedPacks.includes(recommendedPack) && registryPacks.has(recommendedPack)) {
1351
+ issues.push({
1352
+ level: "warning",
1353
+ message: `${recommendedPack} matches the detected stack but is not installed. Run \`npx @contextforge/cli add ${recommendedPack}\` if you want it.`
1354
+ });
1355
+ }
837
1356
  }
838
1357
  return { checks, issues };
839
1358
  }
840
1359
  export {
841
1360
  CONFIG_PATH,
842
1361
  ConfigSchema,
1362
+ DEFAULT_CORE_PACKS,
843
1363
  DEFAULT_REGISTRY_SOURCES,
844
1364
  DEFAULT_TOOLS,
845
1365
  GENERATED_BLOCK_END,
846
1366
  GENERATED_BLOCK_START,
1367
+ LOCK_PATH,
847
1368
  OFFICIAL_REGISTRY_SOURCE,
848
1369
  OFFICIAL_REGISTRY_URL,
849
1370
  PROJECT_PACK_CACHE,
1371
+ PackFileSchema,
1372
+ PackFileTypeSchema,
1373
+ PackManifestSchema,
850
1374
  PackSchema,
851
- RemotePackEntrySchema,
852
- RemotePackFilesSchema,
853
- RemoteRegistryIndexSchema,
1375
+ RegistryIndexSchema,
1376
+ RegistryPackSourceSchema,
1377
+ RegistryPackSummarySchema,
1378
+ ToolSchema,
854
1379
  addPackToConfig,
855
- cacheRemotePacks,
1380
+ compileAgentsMd,
1381
+ compileClaudeMd,
856
1382
  compileOutputs,
857
1383
  createConfig,
858
1384
  detectPackageManager,
859
1385
  detectProject,
860
1386
  doctorProject,
1387
+ downloadPackToContextForge,
1388
+ fetchPackFile,
1389
+ fetchPackManifest,
1390
+ fetchRegistry,
1391
+ fetchText,
861
1392
  findPack,
1393
+ findPackSummary,
1394
+ generateToolOutputs,
862
1395
  getGeneratedBlock,
863
1396
  hasPackage,
864
1397
  hasScript,
1398
+ installPack,
1399
+ installPackAndSync,
1400
+ listRegistryPacks,
865
1401
  loadConfig,
1402
+ loadLock,
1403
+ loadProjectPacks,
866
1404
  loadRegistry,
1405
+ loadRemotePack,
1406
+ mandatoryCorePacks,
1407
+ manifestMatchesProject,
1408
+ missingMandatoryCorePacks,
1409
+ normalizeConfig,
867
1410
  packMatchesProject,
868
1411
  packageManagerLabel,
1412
+ pathExists,
1413
+ readInstalledPacks,
869
1414
  readPackageJson,
1415
+ recommendPackNames,
870
1416
  recommendPacks,
1417
+ registrySourceToUrl,
1418
+ removeGeneratedBlock,
1419
+ resolvePackFileUrl,
1420
+ resolvePackUrl,
871
1421
  resolvePacks,
872
1422
  safeWriteFile,
873
1423
  saveConfig,
874
- saveInstalledPacks,
1424
+ saveLock,
1425
+ searchRegistryPacks,
1426
+ syncInstalledPacks,
875
1427
  syncProject,
1428
+ updateContextForgeConfig,
1429
+ updateContextForgeLock,
876
1430
  updateGeneratedBlock,
877
1431
  wrapGeneratedBlock,
878
1432
  writeGeneratedFiles