@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.
- package/dist/index.d.ts +297 -131
- package/dist/index.js +1018 -451
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/detect/detectProject.ts
|
|
2
|
-
import
|
|
2
|
+
import path6 from "path";
|
|
3
3
|
import fg4 from "fast-glob";
|
|
4
|
-
import
|
|
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 =
|
|
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,
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
|
239
|
+
import path8 from "path";
|
|
180
240
|
|
|
181
241
|
// src/registry/localRegistry.ts
|
|
182
|
-
import
|
|
183
|
-
import
|
|
184
|
-
async function
|
|
185
|
-
|
|
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
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
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,
|
|
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
|
|
247
|
-
return
|
|
301
|
+
function resolvePackUrl(registryUrl, packPath) {
|
|
302
|
+
return new URL(packPath, registryUrl).toString();
|
|
248
303
|
}
|
|
249
|
-
function
|
|
250
|
-
return
|
|
304
|
+
function resolvePackFileUrl(packUrl, filePath) {
|
|
305
|
+
return new URL(filePath, packUrl).toString();
|
|
251
306
|
}
|
|
252
|
-
async function
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
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
|
|
303
|
-
|
|
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
|
|
353
|
+
return fetchRegistry(input);
|
|
324
354
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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/
|
|
341
|
-
import
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
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
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
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("
|
|
464
|
+
names.add("nextjs-best-practices");
|
|
465
|
+
names.add("react-performance");
|
|
466
|
+
names.add("react-composition");
|
|
381
467
|
}
|
|
382
|
-
if (analysis.
|
|
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.
|
|
389
|
-
names.add("
|
|
474
|
+
if (analysis.language === "typescript") {
|
|
475
|
+
names.add("typescript-advanced-types");
|
|
390
476
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
477
|
+
if (analysis.services.supabase) {
|
|
478
|
+
names.add("supabase");
|
|
479
|
+
names.add("security-baseline");
|
|
480
|
+
}
|
|
481
|
+
return [...names];
|
|
395
482
|
}
|
|
396
|
-
async function
|
|
397
|
-
const
|
|
398
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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
|
-
|
|
514
|
+
registry,
|
|
433
515
|
tools,
|
|
434
|
-
|
|
435
|
-
|
|
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/
|
|
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 =
|
|
446
|
-
if (!await
|
|
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
|
|
578
|
+
return ConfigSchema.parse(await fs10.readJson(configPath));
|
|
450
579
|
}
|
|
451
580
|
|
|
452
581
|
// src/config/saveConfig.ts
|
|
453
|
-
import
|
|
454
|
-
import
|
|
582
|
+
import path12 from "path";
|
|
583
|
+
import fs11 from "fs-extra";
|
|
455
584
|
async function saveConfig(root, config) {
|
|
456
|
-
const configPath =
|
|
457
|
-
await
|
|
458
|
-
await
|
|
585
|
+
const configPath = path12.join(root, CONFIG_PATH);
|
|
586
|
+
await fs11.ensureDir(path12.dirname(configPath));
|
|
587
|
+
await fs11.writeFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
459
588
|
`);
|
|
460
589
|
}
|
|
461
590
|
function addPackToConfig(config, packName) {
|
|
462
591
|
return {
|
|
463
592
|
...config,
|
|
464
|
-
|
|
593
|
+
installedPacks: [.../* @__PURE__ */ new Set([...config.installedPacks, packName])]
|
|
465
594
|
};
|
|
466
595
|
}
|
|
467
596
|
|
|
468
597
|
// src/registry/installPacks.ts
|
|
469
|
-
import
|
|
470
|
-
import
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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/
|
|
524
|
-
function
|
|
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
|
-
"#
|
|
702
|
+
"# Project Agent Instructions",
|
|
703
|
+
"",
|
|
704
|
+
"Generated by ContextForge from installed instruction packs.",
|
|
527
705
|
"",
|
|
528
|
-
"
|
|
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: ${
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
"
|
|
558
|
-
"
|
|
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
|
-
"#
|
|
737
|
+
"# Claude Code Instructions",
|
|
738
|
+
"",
|
|
739
|
+
"Generated by ContextForge from installed instruction packs.",
|
|
567
740
|
"",
|
|
568
|
-
|
|
741
|
+
"## Core Behavior",
|
|
569
742
|
"",
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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/
|
|
615
|
-
|
|
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
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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/
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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 (
|
|
684
|
-
|
|
939
|
+
if (isGeneratedOnlyPath(relativePath)) {
|
|
940
|
+
await fs16.remove(filePath);
|
|
941
|
+
return;
|
|
685
942
|
}
|
|
686
|
-
|
|
687
|
-
|
|
943
|
+
const content = await fs16.readFile(filePath, "utf8");
|
|
944
|
+
if (!getGeneratedBlock(content)) {
|
|
945
|
+
return;
|
|
688
946
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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/
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
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(
|
|
1052
|
+
await safeWriteFile(path19.join(root, output.path), output.content);
|
|
715
1053
|
}
|
|
716
|
-
|
|
1054
|
+
await cleanupStaleGeneratedFiles(root, previousFiles, currentFiles);
|
|
1055
|
+
return currentFiles;
|
|
717
1056
|
}
|
|
718
1057
|
|
|
719
1058
|
// src/sync.ts
|
|
720
|
-
import
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const
|
|
724
|
-
const config =
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
1076
|
+
registry: registryUrl,
|
|
737
1077
|
generatedFiles
|
|
738
1078
|
};
|
|
739
|
-
await saveConfig(
|
|
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
|
|
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
|
|
751
|
-
import
|
|
1187
|
+
import path21 from "path";
|
|
1188
|
+
import fs19 from "fs-extra";
|
|
752
1189
|
async function fileExists(root, relativePath) {
|
|
753
|
-
return
|
|
1190
|
+
return fs19.pathExists(path21.join(root, relativePath));
|
|
754
1191
|
}
|
|
755
|
-
async function
|
|
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 =
|
|
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
|
|
1241
|
+
const registryUrl = options.registry ?? config.registry;
|
|
1242
|
+
const [analysis, packageJson, lock] = await Promise.all([
|
|
773
1243
|
detectProject(resolvedRoot),
|
|
774
|
-
|
|
775
|
-
|
|
1244
|
+
readPackageJson(resolvedRoot),
|
|
1245
|
+
loadLock(resolvedRoot)
|
|
776
1246
|
]);
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
789
|
-
|
|
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:
|
|
1299
|
+
message: "AGENTS.md is missing a ContextForge generated block. Run `npx @contextforge/cli sync`."
|
|
794
1300
|
});
|
|
795
1301
|
}
|
|
796
1302
|
}
|
|
797
|
-
|
|
798
|
-
if (
|
|
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:
|
|
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
|
|
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}
|
|
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.
|
|
1343
|
+
if (config.installedPacks.includes("test-driven-development") && !hasScript(packageJson, "test")) {
|
|
814
1344
|
issues.push({
|
|
815
1345
|
level: "warning",
|
|
816
|
-
message:
|
|
1346
|
+
message: "test-driven-development is installed, but package.json has no test script."
|
|
817
1347
|
});
|
|
818
1348
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1375
|
+
RegistryIndexSchema,
|
|
1376
|
+
RegistryPackSourceSchema,
|
|
1377
|
+
RegistryPackSummarySchema,
|
|
1378
|
+
ToolSchema,
|
|
841
1379
|
addPackToConfig,
|
|
842
|
-
|
|
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
|
-
|
|
1424
|
+
saveLock,
|
|
1425
|
+
searchRegistryPacks,
|
|
1426
|
+
syncInstalledPacks,
|
|
862
1427
|
syncProject,
|
|
1428
|
+
updateContextForgeConfig,
|
|
1429
|
+
updateContextForgeLock,
|
|
863
1430
|
updateGeneratedBlock,
|
|
864
1431
|
wrapGeneratedBlock,
|
|
865
1432
|
writeGeneratedFiles
|