@contextforge/core 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -327,9 +327,13 @@ declare const LockSchema: z.ZodObject<{
327
327
  registry: z.ZodString;
328
328
  resolvedAt: z.ZodString;
329
329
  packs: z.ZodRecord<z.ZodString, z.ZodObject<{
330
+ title: z.ZodOptional<z.ZodString>;
330
331
  version: z.ZodString;
332
+ topic: z.ZodOptional<z.ZodString>;
333
+ classification: z.ZodOptional<z.ZodString>;
331
334
  path: z.ZodString;
332
335
  source: z.ZodString;
336
+ files: z.ZodOptional<z.ZodArray<z.ZodString>>;
333
337
  }, z.core.$strip>>;
334
338
  }, z.core.$strip>;
335
339
  type ContextForgeLock = z.infer<typeof LockSchema>;
@@ -347,7 +351,7 @@ declare function loadConfig(root: string): Promise<ContextForgeConfig>;
347
351
  declare function saveConfig(root: string, config: ContextForgeConfig): Promise<void>;
348
352
  declare function addPackToConfig(config: ContextForgeConfig, packName: string): ContextForgeConfig;
349
353
 
350
- declare function downloadPackToContextForge(projectRoot: string, packName: string, packManifest: PackManifest, packUrl: string, timeoutMs?: number): Promise<InstalledPack>;
354
+ declare function downloadPackToContextForge(_projectRoot: string, _packName: string, packManifest: PackManifest, packUrl: string, timeoutMs?: number): Promise<InstalledPack>;
351
355
  type InstallPackOptions = {
352
356
  force?: boolean;
353
357
  dryRun?: boolean;
@@ -361,7 +365,7 @@ type InstallPackResult = {
361
365
  summary?: RegistryPackSummary;
362
366
  packUrl?: string;
363
367
  };
364
- declare function installPack(projectRoot: string, registryUrl: string, packName: string, options?: InstallPackOptions): Promise<InstallPackResult>;
368
+ declare function installPack(_projectRoot: string, registryUrl: string, packName: string, options?: InstallPackOptions): Promise<InstallPackResult>;
365
369
 
366
370
  declare function compileOutputs(config: ContextForgeConfig, packs: InstalledPack[], analysis: ProjectAnalysis): GeneratedFile[];
367
371
 
package/dist/index.js CHANGED
@@ -247,15 +247,15 @@ async function readOptional(filePath) {
247
247
  function normalizePackFilePath(file) {
248
248
  return file.path.split(/[\\/]/u).join(path7.sep);
249
249
  }
250
- async function loadCachedPack(packRoot2) {
251
- const manifestPath = path7.join(packRoot2, "pack.json");
250
+ async function loadCachedPack(packRoot) {
251
+ const manifestPath = path7.join(packRoot, "pack.json");
252
252
  if (!await fs7.pathExists(manifestPath)) {
253
253
  return null;
254
254
  }
255
255
  const manifest = PackManifestSchema.parse(await fs7.readJson(manifestPath));
256
256
  const files = {};
257
257
  for (const file of manifest.files) {
258
- const content = await readOptional(path7.join(packRoot2, normalizePackFilePath(file)));
258
+ const content = await readOptional(path7.join(packRoot, normalizePackFilePath(file)));
259
259
  if (content !== void 0) {
260
260
  files[file.type] = content;
261
261
  }
@@ -530,9 +530,13 @@ var LockSchema = z3.object({
530
530
  packs: z3.record(
531
531
  z3.string(),
532
532
  z3.object({
533
+ title: z3.string().optional(),
533
534
  version: z3.string(),
535
+ topic: z3.string().optional(),
536
+ classification: z3.string().optional(),
534
537
  path: z3.string(),
535
- source: z3.string()
538
+ source: z3.string(),
539
+ files: z3.array(z3.string()).optional()
536
540
  })
537
541
  )
538
542
  });
@@ -557,9 +561,13 @@ async function updateContextForgeLock(root, registry, installed) {
557
561
  };
558
562
  for (const pack of installed) {
559
563
  lock.packs[pack.manifest.name] = {
564
+ title: pack.manifest.title,
560
565
  version: pack.manifest.version,
566
+ topic: pack.manifest.topic,
567
+ classification: pack.manifest.classification,
561
568
  path: pack.summary?.path ?? `packs/${pack.manifest.name}/pack.json`,
562
- source: pack.packUrl ?? ""
569
+ source: pack.packUrl ?? "",
570
+ files: pack.manifest.files.map((file) => file.type)
563
571
  };
564
572
  }
565
573
  await saveLock(root, lock);
@@ -595,28 +603,10 @@ function addPackToConfig(config, packName) {
595
603
  }
596
604
 
597
605
  // src/registry/installPacks.ts
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);
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);
606
+ async function downloadPackToContextForge(_projectRoot, _packName, packManifest, packUrl, timeoutMs) {
611
607
  const files = {};
612
- await fs12.ensureDir(root);
613
- await fs12.writeFile(path13.join(root, "pack.json"), `${JSON.stringify(packManifest, null, 2)}
614
- `);
615
608
  for (const file of packManifest.files) {
616
609
  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
610
  files[file.type] = content;
621
611
  }
622
612
  return {
@@ -625,31 +615,17 @@ async function downloadPackToContextForge(projectRoot, packName, packManifest, p
625
615
  files
626
616
  };
627
617
  }
628
- async function installPack(projectRoot, registryUrl, packName, options = {}) {
618
+ async function installPack(_projectRoot, registryUrl, packName, options = {}) {
629
619
  const registry = await fetchRegistry(registryUrl, options.timeoutMs);
630
620
  const summary = findPackSummary(registry, packName);
631
621
  if (!summary) {
632
622
  throw new Error(`Unknown ContextForge pack: ${packName}`);
633
623
  }
634
- const alreadyInstalled = await fs12.pathExists(path13.join(packRoot(projectRoot, packName), "pack.json"));
635
624
  const packUrl = resolvePackUrl(registryUrl, summary.path);
636
625
  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
626
  return {
651
627
  installed: !options.dryRun,
652
- alreadyInstalled,
628
+ alreadyInstalled: false,
653
629
  packName,
654
630
  manifest,
655
631
  summary,
@@ -838,44 +814,44 @@ async function compileClaudeMd() {
838
814
  }
839
815
 
840
816
  // src/compiler/generateToolOutputs.ts
841
- import path16 from "path";
842
- import fs15 from "fs-extra";
817
+ import path15 from "path";
818
+ import fs14 from "fs-extra";
843
819
 
844
820
  // src/fs/safeWriteFile.ts
845
- import path14 from "path";
846
- import fs13 from "fs-extra";
821
+ import path13 from "path";
822
+ import fs12 from "fs-extra";
847
823
  async function safeWriteFile(filePath, generatedContent) {
848
- const existingContent = await fs13.pathExists(filePath) ? await fs13.readFile(filePath, "utf8") : null;
824
+ const existingContent = await fs12.pathExists(filePath) ? await fs12.readFile(filePath, "utf8") : null;
849
825
  const nextContent = updateGeneratedBlock(existingContent, generatedContent);
850
- await fs13.ensureDir(path14.dirname(filePath));
851
- await fs13.writeFile(filePath, nextContent);
826
+ await fs12.ensureDir(path13.dirname(filePath));
827
+ await fs12.writeFile(filePath, nextContent);
852
828
  }
853
829
 
854
830
  // src/compiler/cleanupGeneratedFiles.ts
855
- import path15 from "path";
856
- import fs14 from "fs-extra";
831
+ import path14 from "path";
832
+ import fs13 from "fs-extra";
857
833
  function isGeneratedOnlyPath(relativePath) {
858
834
  return relativePath.startsWith(".agents/skills/") || relativePath.startsWith(".cursor/rules/") || relativePath.startsWith(".github/instructions/") || relativePath.startsWith(".contextforge/instructions/") || relativePath.startsWith(".contextforge/agents/") || relativePath.startsWith(".contextforge/skills/");
859
835
  }
860
836
  async function cleanupFile(root, relativePath) {
861
- const filePath = path15.join(root, relativePath);
862
- if (!await fs14.pathExists(filePath)) {
837
+ const filePath = path14.join(root, relativePath);
838
+ if (!await fs13.pathExists(filePath)) {
863
839
  return;
864
840
  }
865
841
  if (isGeneratedOnlyPath(relativePath)) {
866
- await fs14.remove(filePath);
842
+ await fs13.remove(filePath);
867
843
  return;
868
844
  }
869
- const content = await fs14.readFile(filePath, "utf8");
845
+ const content = await fs13.readFile(filePath, "utf8");
870
846
  if (!getGeneratedBlock(content)) {
871
847
  return;
872
848
  }
873
849
  const nextContent = removeGeneratedBlock(content);
874
850
  if (nextContent.trim().length === 0) {
875
- await fs14.remove(filePath);
851
+ await fs13.remove(filePath);
876
852
  return;
877
853
  }
878
- await fs14.writeFile(filePath, `${nextContent}
854
+ await fs13.writeFile(filePath, `${nextContent}
879
855
  `);
880
856
  }
881
857
  async function cleanupStaleGeneratedFiles(root, previousFiles, currentFiles) {
@@ -945,9 +921,9 @@ function withContextForgePreamble2(pack, type, content) {
945
921
  return ["# ContextForge Git Safety", "", warning, "", content.trim()].join("\n");
946
922
  }
947
923
  async function writeGeneratedOnlyFile(root, output) {
948
- const filePath = path16.join(root, output.path);
949
- await fs15.ensureDir(path16.dirname(filePath));
950
- await fs15.writeFile(filePath, output.content.endsWith("\n") ? output.content : `${output.content}
924
+ const filePath = path15.join(root, output.path);
925
+ await fs14.ensureDir(path15.dirname(filePath));
926
+ await fs14.writeFile(filePath, output.content.endsWith("\n") ? output.content : `${output.content}
951
927
  `);
952
928
  }
953
929
  function packOutputs(pack, tools) {
@@ -960,7 +936,7 @@ async function generateToolOutputs(root, packs, config, previousFiles = []) {
960
936
  if (isGeneratedOnly) {
961
937
  await writeGeneratedOnlyFile(root, output);
962
938
  } else {
963
- await safeWriteFile(path16.join(root, output.path), output.content);
939
+ await safeWriteFile(path15.join(root, output.path), output.content);
964
940
  }
965
941
  }
966
942
  const rootOutputs = [];
@@ -971,7 +947,7 @@ async function generateToolOutputs(root, packs, config, previousFiles = []) {
971
947
  rootOutputs.push({ path: "CLAUDE.md", content: await compileClaudeMd() });
972
948
  }
973
949
  for (const output of rootOutputs) {
974
- await safeWriteFile(path16.join(root, output.path), output.content);
950
+ await safeWriteFile(path15.join(root, output.path), output.content);
975
951
  }
976
952
  const currentFiles = [...packGeneratedOutputs, ...rootOutputs].map((output) => output.path);
977
953
  await cleanupStaleGeneratedFiles(root, previousFiles, currentFiles);
@@ -979,19 +955,19 @@ async function generateToolOutputs(root, packs, config, previousFiles = []) {
979
955
  }
980
956
 
981
957
  // src/compiler/writeGeneratedFiles.ts
982
- import path17 from "path";
958
+ import path16 from "path";
983
959
  async function writeGeneratedFiles(root, outputs, previousFiles = []) {
984
960
  const currentFiles = outputs.map((output) => output.path);
985
961
  for (const output of outputs) {
986
- await safeWriteFile(path17.join(root, output.path), output.content);
962
+ await safeWriteFile(path16.join(root, output.path), output.content);
987
963
  }
988
964
  await cleanupStaleGeneratedFiles(root, previousFiles, currentFiles);
989
965
  return currentFiles;
990
966
  }
991
967
 
992
968
  // src/sync.ts
993
- import path18 from "path";
994
- import fs16 from "fs-extra";
969
+ import path17 from "path";
970
+ import fs15 from "fs-extra";
995
971
  async function updateContextForgeConfig(projectRoot, packName, registryUrl, generatedFiles) {
996
972
  const existing = await loadOptionalConfig(projectRoot);
997
973
  const config = addPackToConfig(
@@ -1025,15 +1001,22 @@ async function loadInstalledPackFromRegistry(projectRoot, registryUrl, summary,
1025
1001
  const manifest = await fetchPackManifest(packUrl, timeoutMs);
1026
1002
  return downloadPackToContextForge(projectRoot, manifest.name, manifest, packUrl, timeoutMs);
1027
1003
  }
1004
+ async function removeLegacyPackCache(root) {
1005
+ await fs15.remove(path17.join(root, ".contextforge/packs"));
1006
+ }
1028
1007
  async function syncInstalledPacks(projectRoot, providedConfig) {
1029
- const root = path18.resolve(projectRoot);
1008
+ const root = path17.resolve(projectRoot);
1030
1009
  const analysis = await detectProject(root);
1031
1010
  const previousConfig = await loadOptionalConfig(root);
1032
1011
  const config = providedConfig ?? await loadConfig(root);
1033
1012
  const registry = await fetchRegistry(config.registry);
1034
1013
  const installed = [];
1035
1014
  const missing = [];
1036
- for (const packName of config.installedPacks) {
1015
+ const detectedPackNames = recommendPackNames(analysis).filter(
1016
+ (packName) => Boolean(findPackSummary(registry, packName))
1017
+ );
1018
+ const packNames = [.../* @__PURE__ */ new Set([...config.installedPacks, ...detectedPackNames])];
1019
+ for (const packName of packNames) {
1037
1020
  const summary = findPackSummary(registry, packName);
1038
1021
  if (!summary) {
1039
1022
  missing.push(packName);
@@ -1041,6 +1024,7 @@ async function syncInstalledPacks(projectRoot, providedConfig) {
1041
1024
  }
1042
1025
  installed.push(await loadInstalledPackFromRegistry(root, config.registry, summary));
1043
1026
  }
1027
+ await removeLegacyPackCache(root);
1044
1028
  const generatedFiles = await generateToolOutputs(
1045
1029
  root,
1046
1030
  installed,
@@ -1078,7 +1062,7 @@ async function syncProject(root, providedConfig) {
1078
1062
  return syncInstalledPacks(root, providedConfig);
1079
1063
  }
1080
1064
  async function installPackAndSync(projectRoot, registryUrl, packName, options = {}) {
1081
- const root = path18.resolve(projectRoot);
1065
+ const root = path17.resolve(projectRoot);
1082
1066
  const config = await loadOptionalConfig(root);
1083
1067
  const result = await installPack(root, registryUrl, packName, options);
1084
1068
  if (result.alreadyInstalled && !options.force && config?.installedPacks.includes(packName)) {
@@ -1111,24 +1095,33 @@ async function installPackAndSync(projectRoot, registryUrl, packName, options =
1111
1095
  };
1112
1096
  }
1113
1097
  async function readInstalledPacks(projectRoot) {
1114
- return loadProjectPacks(projectRoot);
1098
+ const config = await loadConfig(projectRoot);
1099
+ const registry = await fetchRegistry(config.registry);
1100
+ const installed = [];
1101
+ for (const packName of config.installedPacks) {
1102
+ const summary = findPackSummary(registry, packName);
1103
+ if (summary) {
1104
+ installed.push(await loadInstalledPackFromRegistry(projectRoot, config.registry, summary));
1105
+ }
1106
+ }
1107
+ return installed;
1115
1108
  }
1116
1109
  async function pathExists(root, relativePath) {
1117
- return fs16.pathExists(path18.join(root, relativePath));
1110
+ return fs15.pathExists(path17.join(root, relativePath));
1118
1111
  }
1119
1112
 
1120
1113
  // src/doctor/doctorProject.ts
1121
- import path19 from "path";
1122
- import fs17 from "fs-extra";
1114
+ import path18 from "path";
1115
+ import fs16 from "fs-extra";
1123
1116
  async function fileExists(root, relativePath) {
1124
- return fs17.pathExists(path19.join(root, relativePath));
1117
+ return fs16.pathExists(path18.join(root, relativePath));
1125
1118
  }
1126
1119
  async function readIfExists(root, relativePath) {
1127
- const filePath = path19.join(root, relativePath);
1128
- if (!await fs17.pathExists(filePath)) {
1120
+ const filePath = path18.join(root, relativePath);
1121
+ if (!await fs16.pathExists(filePath)) {
1129
1122
  return null;
1130
1123
  }
1131
- return fs17.readFile(filePath, "utf8");
1124
+ return fs16.readFile(filePath, "utf8");
1132
1125
  }
1133
1126
  function hasContextForgeBlock(content) {
1134
1127
  return Boolean(content && getGeneratedBlock(content));
@@ -1158,7 +1151,7 @@ function expectedGeneratedOutputs(packName, tools) {
1158
1151
  async function doctorProject(root, options = {}) {
1159
1152
  const checks = [];
1160
1153
  const issues = [];
1161
- const resolvedRoot = path19.resolve(root);
1154
+ const resolvedRoot = path18.resolve(root);
1162
1155
  if (!await fileExists(resolvedRoot, CONFIG_PATH)) {
1163
1156
  return {
1164
1157
  checks,
@@ -1197,8 +1190,6 @@ async function doctorProject(root, options = {}) {
1197
1190
  message: `${LOCK_PATH} is missing. Run \`npx @contextforge/cli sync\`.`
1198
1191
  });
1199
1192
  }
1200
- const cachedPacks = await loadProjectPacks(resolvedRoot);
1201
- const cachedPackNames = new Set(cachedPacks.map((pack) => pack.manifest.name));
1202
1193
  for (const packName of config.installedPacks) {
1203
1194
  if (registryPacks.size > 0 && !registryPacks.has(packName)) {
1204
1195
  issues.push({
@@ -1206,12 +1197,11 @@ async function doctorProject(root, options = {}) {
1206
1197
  message: `${packName} is installed in config, but no longer exists in the registry.`
1207
1198
  });
1208
1199
  }
1209
- if (!cachedPackNames.has(packName)) {
1200
+ if (lock && !lock.packs[packName]) {
1210
1201
  issues.push({
1211
- level: "error",
1212
- message: `.contextforge/packs/${packName}/pack.json is missing. Run \`npx @contextforge/cli sync\`.`
1202
+ level: "warning",
1203
+ message: `${packName} is installed in config, but missing from ${LOCK_PATH}. Run \`npx @contextforge/cli sync\`.`
1213
1204
  });
1214
- continue;
1215
1205
  }
1216
1206
  for (const output of expectedGeneratedOutputs(packName, config.tools)) {
1217
1207
  if (!await fileExists(resolvedRoot, output)) {
@@ -1256,16 +1246,13 @@ async function doctorProject(root, options = {}) {
1256
1246
  message: "CLAUDE.md is large. Root instructions should stay concise; detailed content belongs in pack files."
1257
1247
  });
1258
1248
  }
1259
- const gitWorkflow = cachedPacks.find((pack) => pack.manifest.name === "git-workflow");
1260
- if (gitWorkflow) {
1249
+ if (config.installedPacks.includes("git-workflow")) {
1261
1250
  const generatedGitFiles = await Promise.all(
1262
1251
  ["codex", "claude", "cursor", "copilot"].map(
1263
1252
  (tool) => readIfExists(resolvedRoot, `.contextforge/agents/${tool}/git-workflow.md`)
1264
1253
  )
1265
1254
  );
1266
1255
  const gitSummary = [
1267
- gitWorkflow.files.agents,
1268
- gitWorkflow.files.claude,
1269
1256
  ...generatedGitFiles,
1270
1257
  agentsMd,
1271
1258
  claudeMd
@@ -1277,14 +1264,6 @@ async function doctorProject(root, options = {}) {
1277
1264
  });
1278
1265
  }
1279
1266
  }
1280
- for (const pack of cachedPacks) {
1281
- if (!await packMatchesProject(pack, resolvedRoot, packageJson)) {
1282
- issues.push({
1283
- level: "warning",
1284
- message: `${pack.manifest.name} is installed, but its detection hints do not match this project.`
1285
- });
1286
- }
1287
- }
1288
1267
  if (config.installedPacks.includes("test-driven-development") && !hasScript(packageJson, "test")) {
1289
1268
  issues.push({
1290
1269
  level: "warning",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextforge/core",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",