@contextforge/core 0.1.0
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/LICENSE +21 -0
- package/dist/index.d.ts +253 -0
- package/dist/index.js +866 -0
- package/package.json +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yashwanth Krishna
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
type PackageManager = "pnpm" | "npm" | "yarn" | "bun" | "unknown";
|
|
4
|
+
type ProjectFramework = "next-app-router" | "next-pages-router" | "vite-react" | "unknown";
|
|
5
|
+
type ProjectLanguage = "typescript" | "javascript";
|
|
6
|
+
type AITool = "codex" | "claude" | "cursor" | "copilot";
|
|
7
|
+
type ProjectAnalysis = {
|
|
8
|
+
root: string;
|
|
9
|
+
packageManager: PackageManager;
|
|
10
|
+
framework: ProjectFramework;
|
|
11
|
+
language: ProjectLanguage;
|
|
12
|
+
styling: {
|
|
13
|
+
tailwind: boolean;
|
|
14
|
+
shadcn: boolean;
|
|
15
|
+
};
|
|
16
|
+
database: {
|
|
17
|
+
prisma: boolean;
|
|
18
|
+
drizzle: boolean;
|
|
19
|
+
};
|
|
20
|
+
testing: {
|
|
21
|
+
vitest: boolean;
|
|
22
|
+
jest: boolean;
|
|
23
|
+
playwright: boolean;
|
|
24
|
+
};
|
|
25
|
+
aiTools: {
|
|
26
|
+
agentsMd: boolean;
|
|
27
|
+
claudeMd: boolean;
|
|
28
|
+
cursorRules: boolean;
|
|
29
|
+
copilotInstructions: boolean;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
type GeneratedFile = {
|
|
33
|
+
path: string;
|
|
34
|
+
content: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
declare function detectProject(root: string): Promise<ProjectAnalysis>;
|
|
38
|
+
|
|
39
|
+
declare function detectPackageManager(root: string): Promise<PackageManager>;
|
|
40
|
+
|
|
41
|
+
declare const PackSchema: z.ZodObject<{
|
|
42
|
+
name: z.ZodString;
|
|
43
|
+
version: z.ZodOptional<z.ZodString>;
|
|
44
|
+
title: z.ZodString;
|
|
45
|
+
description: z.ZodString;
|
|
46
|
+
category: z.ZodEnum<{
|
|
47
|
+
framework: "framework";
|
|
48
|
+
database: "database";
|
|
49
|
+
testing: "testing";
|
|
50
|
+
security: "security";
|
|
51
|
+
ui: "ui";
|
|
52
|
+
workflow: "workflow";
|
|
53
|
+
}>;
|
|
54
|
+
detect: z.ZodOptional<z.ZodObject<{
|
|
55
|
+
files: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
56
|
+
packages: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
57
|
+
}, z.core.$strip>>;
|
|
58
|
+
outputs: z.ZodObject<{
|
|
59
|
+
globalRules: z.ZodDefault<z.ZodBoolean>;
|
|
60
|
+
skill: z.ZodDefault<z.ZodBoolean>;
|
|
61
|
+
cursorRule: z.ZodDefault<z.ZodBoolean>;
|
|
62
|
+
copilotInstruction: z.ZodDefault<z.ZodBoolean>;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
type Pack = z.infer<typeof PackSchema>;
|
|
66
|
+
type LoadedPack = Pack & {
|
|
67
|
+
directory: string;
|
|
68
|
+
source: "project-cache" | "remote" | "local";
|
|
69
|
+
registryUrl?: string;
|
|
70
|
+
files: {
|
|
71
|
+
rules: string;
|
|
72
|
+
skill?: string;
|
|
73
|
+
cursor?: string;
|
|
74
|
+
copilot?: string;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
declare const RemotePackFilesSchema: z.ZodObject<{
|
|
78
|
+
pack: z.ZodOptional<z.ZodString>;
|
|
79
|
+
rules: z.ZodOptional<z.ZodString>;
|
|
80
|
+
skill: z.ZodOptional<z.ZodString>;
|
|
81
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
82
|
+
copilot: z.ZodOptional<z.ZodString>;
|
|
83
|
+
}, z.core.$strip>;
|
|
84
|
+
declare const RemotePackEntrySchema: z.ZodObject<{
|
|
85
|
+
name: z.ZodString;
|
|
86
|
+
version: z.ZodOptional<z.ZodString>;
|
|
87
|
+
baseUrl: z.ZodOptional<z.ZodString>;
|
|
88
|
+
pack: z.ZodOptional<z.ZodObject<{
|
|
89
|
+
name: z.ZodString;
|
|
90
|
+
version: z.ZodOptional<z.ZodString>;
|
|
91
|
+
title: z.ZodString;
|
|
92
|
+
description: z.ZodString;
|
|
93
|
+
category: z.ZodEnum<{
|
|
94
|
+
framework: "framework";
|
|
95
|
+
database: "database";
|
|
96
|
+
testing: "testing";
|
|
97
|
+
security: "security";
|
|
98
|
+
ui: "ui";
|
|
99
|
+
workflow: "workflow";
|
|
100
|
+
}>;
|
|
101
|
+
detect: z.ZodOptional<z.ZodObject<{
|
|
102
|
+
files: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
103
|
+
packages: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
104
|
+
}, z.core.$strip>>;
|
|
105
|
+
outputs: z.ZodObject<{
|
|
106
|
+
globalRules: z.ZodDefault<z.ZodBoolean>;
|
|
107
|
+
skill: z.ZodDefault<z.ZodBoolean>;
|
|
108
|
+
cursorRule: z.ZodDefault<z.ZodBoolean>;
|
|
109
|
+
copilotInstruction: z.ZodDefault<z.ZodBoolean>;
|
|
110
|
+
}, z.core.$strip>;
|
|
111
|
+
}, z.core.$strip>>;
|
|
112
|
+
files: z.ZodOptional<z.ZodObject<{
|
|
113
|
+
pack: z.ZodOptional<z.ZodString>;
|
|
114
|
+
rules: z.ZodOptional<z.ZodString>;
|
|
115
|
+
skill: z.ZodOptional<z.ZodString>;
|
|
116
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
117
|
+
copilot: z.ZodOptional<z.ZodString>;
|
|
118
|
+
}, z.core.$strip>>;
|
|
119
|
+
}, z.core.$strip>;
|
|
120
|
+
declare const RemoteRegistryIndexSchema: z.ZodObject<{
|
|
121
|
+
version: z.ZodDefault<z.ZodString>;
|
|
122
|
+
packs: z.ZodArray<z.ZodObject<{
|
|
123
|
+
name: z.ZodString;
|
|
124
|
+
version: z.ZodOptional<z.ZodString>;
|
|
125
|
+
baseUrl: z.ZodOptional<z.ZodString>;
|
|
126
|
+
pack: z.ZodOptional<z.ZodObject<{
|
|
127
|
+
name: z.ZodString;
|
|
128
|
+
version: z.ZodOptional<z.ZodString>;
|
|
129
|
+
title: z.ZodString;
|
|
130
|
+
description: z.ZodString;
|
|
131
|
+
category: z.ZodEnum<{
|
|
132
|
+
framework: "framework";
|
|
133
|
+
database: "database";
|
|
134
|
+
testing: "testing";
|
|
135
|
+
security: "security";
|
|
136
|
+
ui: "ui";
|
|
137
|
+
workflow: "workflow";
|
|
138
|
+
}>;
|
|
139
|
+
detect: z.ZodOptional<z.ZodObject<{
|
|
140
|
+
files: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
141
|
+
packages: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
142
|
+
}, z.core.$strip>>;
|
|
143
|
+
outputs: z.ZodObject<{
|
|
144
|
+
globalRules: z.ZodDefault<z.ZodBoolean>;
|
|
145
|
+
skill: z.ZodDefault<z.ZodBoolean>;
|
|
146
|
+
cursorRule: z.ZodDefault<z.ZodBoolean>;
|
|
147
|
+
copilotInstruction: z.ZodDefault<z.ZodBoolean>;
|
|
148
|
+
}, z.core.$strip>;
|
|
149
|
+
}, z.core.$strip>>;
|
|
150
|
+
files: z.ZodOptional<z.ZodObject<{
|
|
151
|
+
pack: z.ZodOptional<z.ZodString>;
|
|
152
|
+
rules: z.ZodOptional<z.ZodString>;
|
|
153
|
+
skill: z.ZodOptional<z.ZodString>;
|
|
154
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
155
|
+
copilot: z.ZodOptional<z.ZodString>;
|
|
156
|
+
}, z.core.$strip>>;
|
|
157
|
+
}, z.core.$strip>>;
|
|
158
|
+
}, z.core.$strip>;
|
|
159
|
+
type RemoteRegistryIndex = z.infer<typeof RemoteRegistryIndexSchema>;
|
|
160
|
+
|
|
161
|
+
declare const OFFICIAL_REGISTRY_SOURCE = "official";
|
|
162
|
+
declare const OFFICIAL_REGISTRY_URL = "https://registry.contextforge.dev/index.json";
|
|
163
|
+
declare const DEFAULT_REGISTRY_SOURCES: string[];
|
|
164
|
+
declare const PROJECT_PACK_CACHE = ".contextforge/packs";
|
|
165
|
+
type LoadRegistryOptions = {
|
|
166
|
+
root?: string;
|
|
167
|
+
sources?: string[];
|
|
168
|
+
timeoutMs?: number;
|
|
169
|
+
};
|
|
170
|
+
declare function loadRegistry(input?: string | LoadRegistryOptions): Promise<LoadedPack[]>;
|
|
171
|
+
|
|
172
|
+
type PackageJson = {
|
|
173
|
+
dependencies?: Record<string, string>;
|
|
174
|
+
devDependencies?: Record<string, string>;
|
|
175
|
+
peerDependencies?: Record<string, string>;
|
|
176
|
+
optionalDependencies?: Record<string, string>;
|
|
177
|
+
scripts?: Record<string, string>;
|
|
178
|
+
};
|
|
179
|
+
declare function readPackageJson(root: string): Promise<PackageJson | null>;
|
|
180
|
+
declare function hasPackage(packageJson: PackageJson | null, packageName: string): boolean;
|
|
181
|
+
declare function hasScript(packageJson: PackageJson | null, scriptName: string): boolean;
|
|
182
|
+
|
|
183
|
+
declare function findPack(registry: LoadedPack[], packName: string): LoadedPack | undefined;
|
|
184
|
+
declare function resolvePacks(packNames: string[], registry: LoadedPack[]): LoadedPack[];
|
|
185
|
+
declare function recommendPacks(analysis: ProjectAnalysis, registry: LoadedPack[]): LoadedPack[];
|
|
186
|
+
declare function packMatchesProject(pack: LoadedPack, root: string, packageJson: PackageJson | null): Promise<boolean>;
|
|
187
|
+
declare function packageManagerLabel(packageManager: PackageManager): string;
|
|
188
|
+
|
|
189
|
+
declare const ConfigSchema: z.ZodObject<{
|
|
190
|
+
version: z.ZodDefault<z.ZodString>;
|
|
191
|
+
registries: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
192
|
+
tools: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
193
|
+
codex: "codex";
|
|
194
|
+
claude: "claude";
|
|
195
|
+
cursor: "cursor";
|
|
196
|
+
copilot: "copilot";
|
|
197
|
+
}>>>;
|
|
198
|
+
packs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
199
|
+
packageManager: z.ZodDefault<z.ZodEnum<{
|
|
200
|
+
pnpm: "pnpm";
|
|
201
|
+
npm: "npm";
|
|
202
|
+
yarn: "yarn";
|
|
203
|
+
bun: "bun";
|
|
204
|
+
unknown: "unknown";
|
|
205
|
+
}>>;
|
|
206
|
+
generatedFiles: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
207
|
+
}, z.core.$strip>;
|
|
208
|
+
type ContextForgeConfig = z.infer<typeof ConfigSchema>;
|
|
209
|
+
|
|
210
|
+
declare const DEFAULT_TOOLS: AITool[];
|
|
211
|
+
declare function createConfig(analysis: ProjectAnalysis, packs: LoadedPack[], tools?: AITool[], registries?: string[]): ContextForgeConfig;
|
|
212
|
+
|
|
213
|
+
declare const CONFIG_PATH = ".contextforge/config.json";
|
|
214
|
+
declare function loadConfig(root: string): Promise<ContextForgeConfig>;
|
|
215
|
+
|
|
216
|
+
declare function saveConfig(root: string, config: ContextForgeConfig): Promise<void>;
|
|
217
|
+
declare function addPackToConfig(config: ContextForgeConfig, packName: string): ContextForgeConfig;
|
|
218
|
+
|
|
219
|
+
declare function cacheRemotePacks(root: string, packs: LoadedPack[]): Promise<void>;
|
|
220
|
+
declare function saveInstalledPacks(root: string, packs: LoadedPack[]): Promise<void>;
|
|
221
|
+
|
|
222
|
+
declare function compileOutputs(config: ContextForgeConfig, packs: LoadedPack[], analysis: ProjectAnalysis): GeneratedFile[];
|
|
223
|
+
|
|
224
|
+
declare function writeGeneratedFiles(root: string, outputs: GeneratedFile[]): Promise<string[]>;
|
|
225
|
+
|
|
226
|
+
declare function safeWriteFile(filePath: string, generatedContent: string): Promise<void>;
|
|
227
|
+
|
|
228
|
+
declare const GENERATED_BLOCK_START = "<!-- contextforge:start -->";
|
|
229
|
+
declare const GENERATED_BLOCK_END = "<!-- contextforge:end -->";
|
|
230
|
+
declare function getGeneratedBlock(content: string): string | null;
|
|
231
|
+
declare function wrapGeneratedBlock(content: string): string;
|
|
232
|
+
declare function updateGeneratedBlock(existingContent: string | null, generatedContent: string): string;
|
|
233
|
+
|
|
234
|
+
type SyncResult = {
|
|
235
|
+
root: string;
|
|
236
|
+
analysis: ProjectAnalysis;
|
|
237
|
+
generatedFiles: string[];
|
|
238
|
+
outputs: GeneratedFile[];
|
|
239
|
+
config: ContextForgeConfig;
|
|
240
|
+
};
|
|
241
|
+
declare function syncProject(root: string, providedConfig?: ContextForgeConfig): Promise<SyncResult>;
|
|
242
|
+
|
|
243
|
+
type DoctorIssue = {
|
|
244
|
+
level: "error" | "warning";
|
|
245
|
+
message: string;
|
|
246
|
+
};
|
|
247
|
+
type DoctorReport = {
|
|
248
|
+
checks: string[];
|
|
249
|
+
issues: DoctorIssue[];
|
|
250
|
+
};
|
|
251
|
+
declare function doctorProject(root: string): Promise<DoctorReport>;
|
|
252
|
+
|
|
253
|
+
export { type AITool, CONFIG_PATH, ConfigSchema, type ContextForgeConfig, DEFAULT_REGISTRY_SOURCES, DEFAULT_TOOLS, type DoctorIssue, type DoctorReport, GENERATED_BLOCK_END, GENERATED_BLOCK_START, type GeneratedFile, type LoadedPack, OFFICIAL_REGISTRY_SOURCE, OFFICIAL_REGISTRY_URL, PROJECT_PACK_CACHE, type Pack, PackSchema, type PackageJson, type PackageManager, type ProjectAnalysis, type ProjectFramework, type ProjectLanguage, RemotePackEntrySchema, RemotePackFilesSchema, type RemoteRegistryIndex, RemoteRegistryIndexSchema, type SyncResult, addPackToConfig, cacheRemotePacks, compileOutputs, createConfig, detectPackageManager, detectProject, doctorProject, findPack, getGeneratedBlock, hasPackage, hasScript, loadConfig, loadRegistry, packMatchesProject, packageManagerLabel, readPackageJson, recommendPacks, resolvePacks, safeWriteFile, saveConfig, saveInstalledPacks, syncProject, updateGeneratedBlock, wrapGeneratedBlock, writeGeneratedFiles };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
// src/detect/detectProject.ts
|
|
2
|
+
import path5 from "path";
|
|
3
|
+
import fg4 from "fast-glob";
|
|
4
|
+
import fs5 from "fs-extra";
|
|
5
|
+
|
|
6
|
+
// src/detect/detectAITools.ts
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fg from "fast-glob";
|
|
9
|
+
import fs from "fs-extra";
|
|
10
|
+
async function detectAITools(root) {
|
|
11
|
+
const cursorRules = await fg(".cursor/rules/**/*.{mdc,md}", {
|
|
12
|
+
cwd: root,
|
|
13
|
+
onlyFiles: true,
|
|
14
|
+
dot: true
|
|
15
|
+
});
|
|
16
|
+
const copilotInstructions = await fg(".github/instructions/*.instructions.md", {
|
|
17
|
+
cwd: root,
|
|
18
|
+
onlyFiles: true,
|
|
19
|
+
dot: true
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
agentsMd: await fs.pathExists(path.join(root, "AGENTS.md")),
|
|
23
|
+
claudeMd: await fs.pathExists(path.join(root, "CLAUDE.md")) || await fs.pathExists(path.join(root, ".claude/CLAUDE.md")),
|
|
24
|
+
cursorRules: cursorRules.length > 0 || await fs.pathExists(path.join(root, ".cursor/rules")),
|
|
25
|
+
copilotInstructions: await fs.pathExists(path.join(root, ".github/copilot-instructions.md")) || copilotInstructions.length > 0
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/detect/detectDatabase.ts
|
|
30
|
+
import path2 from "path";
|
|
31
|
+
import fg2 from "fast-glob";
|
|
32
|
+
import fs2 from "fs-extra";
|
|
33
|
+
async function detectDatabase(root) {
|
|
34
|
+
const prisma = await fs2.pathExists(path2.join(root, "prisma/schema.prisma"));
|
|
35
|
+
const drizzleConfigs = await fg2("drizzle.config.{js,ts,mjs,mts,cjs,cts}", {
|
|
36
|
+
cwd: root,
|
|
37
|
+
onlyFiles: true,
|
|
38
|
+
dot: true
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
prisma,
|
|
42
|
+
drizzle: drizzleConfigs.length > 0
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/detect/detectFramework.ts
|
|
47
|
+
import path3 from "path";
|
|
48
|
+
import fg3 from "fast-glob";
|
|
49
|
+
import fs3 from "fs-extra";
|
|
50
|
+
async function hasDirectory(root, relativePath) {
|
|
51
|
+
const statPath = path3.join(root, relativePath);
|
|
52
|
+
if (!await fs3.pathExists(statPath)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return (await fs3.stat(statPath)).isDirectory();
|
|
56
|
+
}
|
|
57
|
+
async function detectFramework(root) {
|
|
58
|
+
if (await hasDirectory(root, "src/app") || await hasDirectory(root, "app")) {
|
|
59
|
+
return "next-app-router";
|
|
60
|
+
}
|
|
61
|
+
if (await hasDirectory(root, "src/pages") || await hasDirectory(root, "pages")) {
|
|
62
|
+
return "next-pages-router";
|
|
63
|
+
}
|
|
64
|
+
const viteConfigs = await fg3("vite.config.{js,ts,mjs,mts,cjs,cts}", {
|
|
65
|
+
cwd: root,
|
|
66
|
+
onlyFiles: true,
|
|
67
|
+
dot: true
|
|
68
|
+
});
|
|
69
|
+
if (viteConfigs.length > 0) {
|
|
70
|
+
return "vite-react";
|
|
71
|
+
}
|
|
72
|
+
return "unknown";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/detect/detectPackageManager.ts
|
|
76
|
+
import path4 from "path";
|
|
77
|
+
import fs4 from "fs-extra";
|
|
78
|
+
async function detectPackageManager(root) {
|
|
79
|
+
const checks = [
|
|
80
|
+
["pnpm", "pnpm-lock.yaml"],
|
|
81
|
+
["npm", "package-lock.json"],
|
|
82
|
+
["yarn", "yarn.lock"],
|
|
83
|
+
["bun", "bun.lockb"],
|
|
84
|
+
["bun", "bun.lock"]
|
|
85
|
+
];
|
|
86
|
+
for (const [manager, fileName] of checks) {
|
|
87
|
+
if (await fs4.pathExists(path4.join(root, fileName))) {
|
|
88
|
+
return manager;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return "unknown";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/detect/detectProject.ts
|
|
95
|
+
async function hasAny(root, patterns) {
|
|
96
|
+
const matches = await fg4(patterns, {
|
|
97
|
+
cwd: root,
|
|
98
|
+
onlyFiles: true,
|
|
99
|
+
dot: true
|
|
100
|
+
});
|
|
101
|
+
return matches.length > 0;
|
|
102
|
+
}
|
|
103
|
+
async function detectProject(root) {
|
|
104
|
+
const resolvedRoot = path5.resolve(root);
|
|
105
|
+
const [packageManager, framework, database, aiTools] = await Promise.all([
|
|
106
|
+
detectPackageManager(resolvedRoot),
|
|
107
|
+
detectFramework(resolvedRoot),
|
|
108
|
+
detectDatabase(resolvedRoot),
|
|
109
|
+
detectAITools(resolvedRoot)
|
|
110
|
+
]);
|
|
111
|
+
const [typescript, tailwind, shadcn, vitest, jest, playwright] = await Promise.all([
|
|
112
|
+
fs5.pathExists(path5.join(resolvedRoot, "tsconfig.json")),
|
|
113
|
+
hasAny(resolvedRoot, ["tailwind.config.{js,ts,mjs,mts,cjs,cts}"]),
|
|
114
|
+
fs5.pathExists(path5.join(resolvedRoot, "components.json")),
|
|
115
|
+
hasAny(resolvedRoot, ["vitest.config.{js,ts,mjs,mts,cjs,cts}"]),
|
|
116
|
+
hasAny(resolvedRoot, ["jest.config.{js,ts,mjs,mts,cjs,cts}", "jest.config.json"]),
|
|
117
|
+
hasAny(resolvedRoot, ["playwright.config.{js,ts,mjs,mts,cjs,cts}"])
|
|
118
|
+
]);
|
|
119
|
+
return {
|
|
120
|
+
root: resolvedRoot,
|
|
121
|
+
packageManager,
|
|
122
|
+
framework,
|
|
123
|
+
language: typescript ? "typescript" : "javascript",
|
|
124
|
+
styling: {
|
|
125
|
+
tailwind,
|
|
126
|
+
shadcn
|
|
127
|
+
},
|
|
128
|
+
database,
|
|
129
|
+
testing: {
|
|
130
|
+
vitest,
|
|
131
|
+
jest,
|
|
132
|
+
playwright
|
|
133
|
+
},
|
|
134
|
+
aiTools
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/registry/registrySchema.ts
|
|
139
|
+
import { z } from "zod";
|
|
140
|
+
var PackSchema = z.object({
|
|
141
|
+
name: z.string().min(1),
|
|
142
|
+
version: z.string().optional(),
|
|
143
|
+
title: z.string().min(1),
|
|
144
|
+
description: z.string().min(1),
|
|
145
|
+
category: z.enum(["framework", "database", "testing", "security", "ui", "workflow"]),
|
|
146
|
+
detect: z.object({
|
|
147
|
+
files: z.array(z.string()).optional(),
|
|
148
|
+
packages: z.array(z.string()).optional()
|
|
149
|
+
}).optional(),
|
|
150
|
+
outputs: z.object({
|
|
151
|
+
globalRules: z.boolean().default(true),
|
|
152
|
+
skill: z.boolean().default(false),
|
|
153
|
+
cursorRule: z.boolean().default(true),
|
|
154
|
+
copilotInstruction: z.boolean().default(true)
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
var RemotePackFilesSchema = z.object({
|
|
158
|
+
pack: z.string().optional(),
|
|
159
|
+
rules: z.string().optional(),
|
|
160
|
+
skill: z.string().optional(),
|
|
161
|
+
cursor: z.string().optional(),
|
|
162
|
+
copilot: z.string().optional()
|
|
163
|
+
});
|
|
164
|
+
var RemotePackEntrySchema = z.object({
|
|
165
|
+
name: z.string().min(1),
|
|
166
|
+
version: z.string().optional(),
|
|
167
|
+
baseUrl: z.string().optional(),
|
|
168
|
+
pack: PackSchema.optional(),
|
|
169
|
+
files: RemotePackFilesSchema.optional()
|
|
170
|
+
}).refine((entry) => Boolean(entry.baseUrl || entry.pack || entry.files?.pack), {
|
|
171
|
+
message: "Remote pack entry must include baseUrl, inline pack metadata, or files.pack"
|
|
172
|
+
});
|
|
173
|
+
var RemoteRegistryIndexSchema = z.object({
|
|
174
|
+
version: z.string().default("1"),
|
|
175
|
+
packs: z.array(RemotePackEntrySchema)
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// src/registry/loadRegistry.ts
|
|
179
|
+
import path7 from "path";
|
|
180
|
+
|
|
181
|
+
// src/registry/localRegistry.ts
|
|
182
|
+
import path6 from "path";
|
|
183
|
+
import fs6 from "fs-extra";
|
|
184
|
+
async function loadLocalRegistry(registryRoot, source) {
|
|
185
|
+
if (!await fs6.pathExists(registryRoot)) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
const entries = await fs6.readdir(registryRoot, { withFileTypes: true });
|
|
189
|
+
const packs = [];
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
if (!entry.isDirectory()) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const directory = path6.join(registryRoot, entry.name);
|
|
195
|
+
const packPath = path6.join(directory, "pack.json");
|
|
196
|
+
if (!await fs6.pathExists(packPath)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const parsed = PackSchema.parse(await fs6.readJson(packPath));
|
|
200
|
+
const readOptional = async (fileName) => {
|
|
201
|
+
const filePath = path6.join(directory, fileName);
|
|
202
|
+
return await fs6.pathExists(filePath) ? fs6.readFile(filePath, "utf8") : void 0;
|
|
203
|
+
};
|
|
204
|
+
const rules = await readOptional("rules.md");
|
|
205
|
+
if (!rules) {
|
|
206
|
+
throw new Error(`Pack "${parsed.name}" is missing rules.md`);
|
|
207
|
+
}
|
|
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
|
+
}
|
|
220
|
+
return packs;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/registry/remoteRegistry.ts
|
|
224
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
225
|
+
async function fetchText(url, required, timeoutMs) {
|
|
226
|
+
const controller = new AbortController();
|
|
227
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
228
|
+
try {
|
|
229
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
if (!required && response.status === 404) {
|
|
232
|
+
return void 0;
|
|
233
|
+
}
|
|
234
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
235
|
+
}
|
|
236
|
+
return response.text();
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (!required) {
|
|
239
|
+
return void 0;
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
} finally {
|
|
243
|
+
clearTimeout(timeout);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function resolveUrl(value, baseUrl) {
|
|
247
|
+
return value ? new URL(value, baseUrl).toString() : void 0;
|
|
248
|
+
}
|
|
249
|
+
function defaultPackFile(baseUrl, fileName) {
|
|
250
|
+
return baseUrl ? new URL(fileName, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString() : void 0;
|
|
251
|
+
}
|
|
252
|
+
async function loadRemoteRegistry(registryUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
253
|
+
const indexText = await fetchText(registryUrl, true, timeoutMs);
|
|
254
|
+
const index = RemoteRegistryIndexSchema.parse(JSON.parse(indexText ?? "{}"));
|
|
255
|
+
const packs = [];
|
|
256
|
+
for (const entry of index.packs) {
|
|
257
|
+
const baseUrl = resolveUrl(entry.baseUrl, registryUrl);
|
|
258
|
+
const packUrl = resolveUrl(entry.files?.pack, registryUrl) ?? defaultPackFile(baseUrl, "pack.json");
|
|
259
|
+
const rawPack = entry.pack ?? (packUrl ? JSON.parse(await fetchText(packUrl, true, timeoutMs) ?? "{}") : void 0);
|
|
260
|
+
const parsed = PackSchema.parse({
|
|
261
|
+
...rawPack,
|
|
262
|
+
name: rawPack?.name ?? entry.name,
|
|
263
|
+
version: rawPack?.version ?? entry.version
|
|
264
|
+
});
|
|
265
|
+
const rulesUrl = resolveUrl(entry.files?.rules, registryUrl) ?? defaultPackFile(baseUrl, "rules.md");
|
|
266
|
+
if (!rulesUrl) {
|
|
267
|
+
throw new Error(`Remote pack "${entry.name}" is missing a rules.md URL.`);
|
|
268
|
+
}
|
|
269
|
+
packs.push({
|
|
270
|
+
...parsed,
|
|
271
|
+
directory: baseUrl ?? registryUrl,
|
|
272
|
+
source: "remote",
|
|
273
|
+
registryUrl,
|
|
274
|
+
files: {
|
|
275
|
+
rules: await fetchText(rulesUrl, true, timeoutMs) ?? "",
|
|
276
|
+
skill: await fetchText(
|
|
277
|
+
resolveUrl(entry.files?.skill, registryUrl) ?? defaultPackFile(baseUrl, "skill.md") ?? "",
|
|
278
|
+
false,
|
|
279
|
+
timeoutMs
|
|
280
|
+
),
|
|
281
|
+
cursor: await fetchText(
|
|
282
|
+
resolveUrl(entry.files?.cursor, registryUrl) ?? defaultPackFile(baseUrl, "cursor.mdc") ?? "",
|
|
283
|
+
false,
|
|
284
|
+
timeoutMs
|
|
285
|
+
),
|
|
286
|
+
copilot: await fetchText(
|
|
287
|
+
resolveUrl(entry.files?.copilot, registryUrl) ?? defaultPackFile(baseUrl, "copilot.md") ?? "",
|
|
288
|
+
false,
|
|
289
|
+
timeoutMs
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return packs;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/registry/loadRegistry.ts
|
|
298
|
+
var OFFICIAL_REGISTRY_SOURCE = "official";
|
|
299
|
+
var OFFICIAL_REGISTRY_URL = "https://registry.contextforge.dev/index.json";
|
|
300
|
+
var DEFAULT_REGISTRY_SOURCES = [OFFICIAL_REGISTRY_SOURCE];
|
|
301
|
+
var PROJECT_PACK_CACHE = ".contextforge/packs";
|
|
302
|
+
function mergePacks(packs) {
|
|
303
|
+
const byName = /* @__PURE__ */ new Map();
|
|
304
|
+
for (const pack of packs) {
|
|
305
|
+
if (!byName.has(pack.name)) {
|
|
306
|
+
byName.set(pack.name, pack);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
310
|
+
}
|
|
311
|
+
async function loadRegistrySource(source, timeoutMs) {
|
|
312
|
+
if (source === OFFICIAL_REGISTRY_SOURCE) {
|
|
313
|
+
try {
|
|
314
|
+
return await loadRemoteRegistry(OFFICIAL_REGISTRY_URL, timeoutMs ?? 1500);
|
|
315
|
+
} catch {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return loadRemoteRegistry(source, timeoutMs);
|
|
320
|
+
}
|
|
321
|
+
async function loadRegistry(input = {}) {
|
|
322
|
+
if (typeof input === "string") {
|
|
323
|
+
return mergePacks(await loadLocalRegistry(input, "local"));
|
|
324
|
+
}
|
|
325
|
+
const sources = input.sources ?? DEFAULT_REGISTRY_SOURCES;
|
|
326
|
+
const packs = [];
|
|
327
|
+
if (input.root) {
|
|
328
|
+
packs.push(...await loadLocalRegistry(path7.join(input.root, PROJECT_PACK_CACHE), "project-cache"));
|
|
329
|
+
}
|
|
330
|
+
for (const source of sources) {
|
|
331
|
+
packs.push(...await loadRegistrySource(source, input.timeoutMs));
|
|
332
|
+
}
|
|
333
|
+
return mergePacks(packs);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/registry/resolvePack.ts
|
|
337
|
+
import path9 from "path";
|
|
338
|
+
import fs8 from "fs-extra";
|
|
339
|
+
|
|
340
|
+
// src/project/packageJson.ts
|
|
341
|
+
import path8 from "path";
|
|
342
|
+
import fs7 from "fs-extra";
|
|
343
|
+
async function readPackageJson(root) {
|
|
344
|
+
const packageJsonPath = path8.join(root, "package.json");
|
|
345
|
+
if (!await fs7.pathExists(packageJsonPath)) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
return fs7.readJson(packageJsonPath);
|
|
349
|
+
}
|
|
350
|
+
function hasPackage(packageJson, packageName) {
|
|
351
|
+
if (!packageJson) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
return Boolean(
|
|
355
|
+
packageJson.dependencies?.[packageName] || packageJson.devDependencies?.[packageName] || packageJson.peerDependencies?.[packageName] || packageJson.optionalDependencies?.[packageName]
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
function hasScript(packageJson, scriptName) {
|
|
359
|
+
return Boolean(packageJson?.scripts?.[scriptName]);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/registry/resolvePack.ts
|
|
363
|
+
function findPack(registry, packName) {
|
|
364
|
+
return registry.find((pack) => pack.name === packName);
|
|
365
|
+
}
|
|
366
|
+
function resolvePacks(packNames, registry) {
|
|
367
|
+
const uniqueNames = [...new Set(packNames)];
|
|
368
|
+
return uniqueNames.map((packName) => {
|
|
369
|
+
const pack = findPack(registry, packName);
|
|
370
|
+
if (!pack) {
|
|
371
|
+
throw new Error(`Unknown ContextForge pack: ${packName}`);
|
|
372
|
+
}
|
|
373
|
+
return pack;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
function recommendPacks(analysis, registry) {
|
|
377
|
+
const names = /* @__PURE__ */ new Set();
|
|
378
|
+
names.add("env-secrets");
|
|
379
|
+
if (analysis.framework === "next-app-router") {
|
|
380
|
+
names.add("next-app-router");
|
|
381
|
+
}
|
|
382
|
+
if (analysis.database.prisma) {
|
|
383
|
+
names.add("prisma-migrations");
|
|
384
|
+
}
|
|
385
|
+
if (analysis.styling.shadcn) {
|
|
386
|
+
names.add("shadcn-ui");
|
|
387
|
+
}
|
|
388
|
+
if (analysis.testing.vitest || analysis.testing.jest || analysis.testing.playwright) {
|
|
389
|
+
names.add("testing-workflow");
|
|
390
|
+
}
|
|
391
|
+
return resolvePacks(
|
|
392
|
+
[...names].filter((name) => findPack(registry, name)),
|
|
393
|
+
registry
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
async function packMatchesProject(pack, root, packageJson) {
|
|
397
|
+
const fileChecks = await Promise.all(
|
|
398
|
+
pack.detect?.files?.map((filePattern) => fs8.pathExists(path9.join(root, filePattern))) ?? []
|
|
399
|
+
);
|
|
400
|
+
const packageChecks = pack.detect?.packages?.map((packageName) => hasPackage(packageJson, packageName)) ?? [];
|
|
401
|
+
const checks = [...fileChecks, ...packageChecks];
|
|
402
|
+
return checks.length === 0 || checks.some(Boolean);
|
|
403
|
+
}
|
|
404
|
+
function packageManagerLabel(packageManager) {
|
|
405
|
+
const labels = {
|
|
406
|
+
pnpm: "pnpm",
|
|
407
|
+
npm: "npm",
|
|
408
|
+
yarn: "Yarn",
|
|
409
|
+
bun: "Bun",
|
|
410
|
+
unknown: "Unknown"
|
|
411
|
+
};
|
|
412
|
+
return labels[packageManager];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/config/configSchema.ts
|
|
416
|
+
import { z as z2 } from "zod";
|
|
417
|
+
var ToolSchema = z2.enum(["codex", "claude", "cursor", "copilot"]);
|
|
418
|
+
var ConfigSchema = z2.object({
|
|
419
|
+
version: z2.string().default("0.1.0"),
|
|
420
|
+
registries: z2.array(z2.string()).default(DEFAULT_REGISTRY_SOURCES),
|
|
421
|
+
tools: z2.array(ToolSchema).default(["codex", "claude", "cursor", "copilot"]),
|
|
422
|
+
packs: z2.array(z2.string()).default([]),
|
|
423
|
+
packageManager: z2.enum(["pnpm", "npm", "yarn", "bun", "unknown"]).default("unknown"),
|
|
424
|
+
generatedFiles: z2.array(z2.string()).default([])
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// src/config/defaultConfig.ts
|
|
428
|
+
var DEFAULT_TOOLS = ["codex", "claude", "cursor", "copilot"];
|
|
429
|
+
function createConfig(analysis, packs, tools = DEFAULT_TOOLS, registries = DEFAULT_REGISTRY_SOURCES) {
|
|
430
|
+
return {
|
|
431
|
+
version: "0.1.0",
|
|
432
|
+
registries,
|
|
433
|
+
tools,
|
|
434
|
+
packs: packs.map((pack) => pack.name),
|
|
435
|
+
packageManager: analysis.packageManager,
|
|
436
|
+
generatedFiles: []
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/config/loadConfig.ts
|
|
441
|
+
import path10 from "path";
|
|
442
|
+
import fs9 from "fs-extra";
|
|
443
|
+
var CONFIG_PATH = ".contextforge/config.json";
|
|
444
|
+
async function loadConfig(root) {
|
|
445
|
+
const configPath = path10.join(root, CONFIG_PATH);
|
|
446
|
+
if (!await fs9.pathExists(configPath)) {
|
|
447
|
+
throw new Error("ContextForge config not found. Run `npx @contextforge/cli init` first.");
|
|
448
|
+
}
|
|
449
|
+
return ConfigSchema.parse(await fs9.readJson(configPath));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/config/saveConfig.ts
|
|
453
|
+
import path11 from "path";
|
|
454
|
+
import fs10 from "fs-extra";
|
|
455
|
+
async function saveConfig(root, config) {
|
|
456
|
+
const configPath = path11.join(root, CONFIG_PATH);
|
|
457
|
+
await fs10.ensureDir(path11.dirname(configPath));
|
|
458
|
+
await fs10.writeFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
459
|
+
`);
|
|
460
|
+
}
|
|
461
|
+
function addPackToConfig(config, packName) {
|
|
462
|
+
return {
|
|
463
|
+
...config,
|
|
464
|
+
packs: [.../* @__PURE__ */ new Set([...config.packs, packName])]
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/registry/installPacks.ts
|
|
469
|
+
import path12 from "path";
|
|
470
|
+
import fs11 from "fs-extra";
|
|
471
|
+
var INSTALLED_PACKS_PATH = ".contextforge/installed-packs.json";
|
|
472
|
+
function packJson(pack) {
|
|
473
|
+
return {
|
|
474
|
+
name: pack.name,
|
|
475
|
+
version: pack.version,
|
|
476
|
+
title: pack.title,
|
|
477
|
+
description: pack.description,
|
|
478
|
+
category: pack.category,
|
|
479
|
+
detect: pack.detect,
|
|
480
|
+
outputs: pack.outputs
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
async function writePack(root, pack) {
|
|
484
|
+
const packRoot = path12.join(root, PROJECT_PACK_CACHE, pack.name);
|
|
485
|
+
await fs11.ensureDir(packRoot);
|
|
486
|
+
await fs11.writeFile(path12.join(packRoot, "pack.json"), `${JSON.stringify(packJson(pack), null, 2)}
|
|
487
|
+
`);
|
|
488
|
+
await fs11.writeFile(path12.join(packRoot, "rules.md"), pack.files.rules);
|
|
489
|
+
const optionalFiles = [
|
|
490
|
+
["skill.md", pack.files.skill],
|
|
491
|
+
["cursor.mdc", pack.files.cursor],
|
|
492
|
+
["copilot.md", pack.files.copilot]
|
|
493
|
+
];
|
|
494
|
+
for (const [fileName, content] of optionalFiles) {
|
|
495
|
+
if (content) {
|
|
496
|
+
await fs11.writeFile(path12.join(packRoot, fileName), content);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function cacheRemotePacks(root, packs) {
|
|
501
|
+
for (const pack of packs) {
|
|
502
|
+
if (pack.source === "remote") {
|
|
503
|
+
await writePack(root, pack);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async function saveInstalledPacks(root, packs) {
|
|
508
|
+
const metadataPath = path12.join(root, INSTALLED_PACKS_PATH);
|
|
509
|
+
const metadata = {
|
|
510
|
+
version: "1",
|
|
511
|
+
packs: packs.map((pack) => ({
|
|
512
|
+
name: pack.name,
|
|
513
|
+
version: pack.version ?? "0.0.0",
|
|
514
|
+
source: pack.source,
|
|
515
|
+
registryUrl: pack.registryUrl
|
|
516
|
+
}))
|
|
517
|
+
};
|
|
518
|
+
await fs11.ensureDir(path12.dirname(metadataPath));
|
|
519
|
+
await fs11.writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}
|
|
520
|
+
`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/compiler/compileAgentsMd.ts
|
|
524
|
+
function compileAgentsMd(analysis, packs) {
|
|
525
|
+
return [
|
|
526
|
+
"# ContextForge Instructions",
|
|
527
|
+
"",
|
|
528
|
+
"Generated by ContextForge. Add project-specific guidance outside this block.",
|
|
529
|
+
"",
|
|
530
|
+
"## Detected Project",
|
|
531
|
+
`- Framework: ${analysis.framework}`,
|
|
532
|
+
`- Language: ${analysis.language}`,
|
|
533
|
+
`- Package manager: ${packageManagerLabel(analysis.packageManager)}`,
|
|
534
|
+
`- Styling: ${analysis.styling.tailwind ? "Tailwind CSS" : "not detected"}${analysis.styling.shadcn ? " + shadcn/ui" : ""}`,
|
|
535
|
+
`- Database: ${analysis.database.prisma ? "Prisma" : analysis.database.drizzle ? "Drizzle" : "not detected"}`,
|
|
536
|
+
`- Testing: ${[
|
|
537
|
+
analysis.testing.vitest && "Vitest",
|
|
538
|
+
analysis.testing.jest && "Jest",
|
|
539
|
+
analysis.testing.playwright && "Playwright"
|
|
540
|
+
].filter(Boolean).join(", ") || "not detected"}`,
|
|
541
|
+
"",
|
|
542
|
+
"## Active Packs",
|
|
543
|
+
...packs.flatMap((pack) => [
|
|
544
|
+
"",
|
|
545
|
+
`### ${pack.title}`,
|
|
546
|
+
"",
|
|
547
|
+
pack.description,
|
|
548
|
+
"",
|
|
549
|
+
pack.files.rules.trim()
|
|
550
|
+
])
|
|
551
|
+
].join("\n");
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/compiler/compileClaudeMd.ts
|
|
555
|
+
function compileClaudeMd(analysis, packs) {
|
|
556
|
+
return compileAgentsMd(analysis, packs).replace(
|
|
557
|
+
"# ContextForge Instructions",
|
|
558
|
+
"# ContextForge Claude Instructions"
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/compiler/compileCopilotInstructions.ts
|
|
563
|
+
function compileCopilotInstructions(analysis, packs) {
|
|
564
|
+
const enabledPacks = packs.filter((pack) => pack.outputs.copilotInstruction);
|
|
565
|
+
return [
|
|
566
|
+
"# ContextForge Copilot Instructions",
|
|
567
|
+
"",
|
|
568
|
+
`Project framework: ${analysis.framework}. Language: ${analysis.language}.`,
|
|
569
|
+
"",
|
|
570
|
+
...enabledPacks.flatMap((pack) => [
|
|
571
|
+
`## ${pack.title}`,
|
|
572
|
+
"",
|
|
573
|
+
(pack.files.copilot ?? pack.files.rules).trim(),
|
|
574
|
+
""
|
|
575
|
+
])
|
|
576
|
+
].join("\n");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/fs/updateGeneratedBlock.ts
|
|
580
|
+
var GENERATED_BLOCK_START = "<!-- contextforge:start -->";
|
|
581
|
+
var GENERATED_BLOCK_END = "<!-- contextforge:end -->";
|
|
582
|
+
var blockPattern = new RegExp(
|
|
583
|
+
`${escapeRegExp(GENERATED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(GENERATED_BLOCK_END)}`,
|
|
584
|
+
"m"
|
|
585
|
+
);
|
|
586
|
+
function escapeRegExp(value) {
|
|
587
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
588
|
+
}
|
|
589
|
+
function getGeneratedBlock(content) {
|
|
590
|
+
return content.match(blockPattern)?.[0] ?? null;
|
|
591
|
+
}
|
|
592
|
+
function wrapGeneratedBlock(content) {
|
|
593
|
+
return `${GENERATED_BLOCK_START}
|
|
594
|
+
${content.trim()}
|
|
595
|
+
${GENERATED_BLOCK_END}`;
|
|
596
|
+
}
|
|
597
|
+
function updateGeneratedBlock(existingContent, generatedContent) {
|
|
598
|
+
const existing = existingContent?.replace(/\s+$/u, "") ?? "";
|
|
599
|
+
const generatedBlock = getGeneratedBlock(generatedContent) ?? wrapGeneratedBlock(generatedContent);
|
|
600
|
+
if (!existing) {
|
|
601
|
+
return `${generatedContent.includes(GENERATED_BLOCK_START) ? generatedContent.trim() : generatedBlock}
|
|
602
|
+
`;
|
|
603
|
+
}
|
|
604
|
+
if (blockPattern.test(existing)) {
|
|
605
|
+
return `${existing.replace(blockPattern, generatedBlock)}
|
|
606
|
+
`;
|
|
607
|
+
}
|
|
608
|
+
return `${existing}
|
|
609
|
+
|
|
610
|
+
${generatedBlock}
|
|
611
|
+
`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/compiler/compileCursorRules.ts
|
|
615
|
+
function withCursorFrontmatter(description, content) {
|
|
616
|
+
return [
|
|
617
|
+
"---",
|
|
618
|
+
`description: ${description}`,
|
|
619
|
+
"alwaysApply: true",
|
|
620
|
+
"---",
|
|
621
|
+
"",
|
|
622
|
+
GENERATED_BLOCK_START,
|
|
623
|
+
content.trim(),
|
|
624
|
+
GENERATED_BLOCK_END
|
|
625
|
+
].join("\n");
|
|
626
|
+
}
|
|
627
|
+
function compileCursorRules(analysis, packs) {
|
|
628
|
+
const files = [
|
|
629
|
+
{
|
|
630
|
+
path: ".cursor/rules/contextforge.mdc",
|
|
631
|
+
content: withCursorFrontmatter(
|
|
632
|
+
"ContextForge generated project overview",
|
|
633
|
+
[
|
|
634
|
+
"# ContextForge Project Context",
|
|
635
|
+
"",
|
|
636
|
+
`- Framework: ${analysis.framework}`,
|
|
637
|
+
`- Language: ${analysis.language}`,
|
|
638
|
+
`- Package manager: ${analysis.packageManager}`,
|
|
639
|
+
`- Active packs: ${packs.map((pack) => pack.name).join(", ") || "none"}`
|
|
640
|
+
].join("\n")
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
];
|
|
644
|
+
for (const pack of packs.filter((item) => item.outputs.cursorRule)) {
|
|
645
|
+
files.push({
|
|
646
|
+
path: `.cursor/rules/${pack.name}.mdc`,
|
|
647
|
+
content: withCursorFrontmatter(
|
|
648
|
+
pack.description.replace(/:/g, "-"),
|
|
649
|
+
(pack.files.cursor ?? pack.files.rules).trim()
|
|
650
|
+
)
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
return files;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/compiler/compileSkills.ts
|
|
657
|
+
function escapeYaml(value) {
|
|
658
|
+
return value.replace(/"/g, '\\"');
|
|
659
|
+
}
|
|
660
|
+
function compileSkills(packs) {
|
|
661
|
+
return packs.filter((pack) => pack.outputs.skill).map((pack) => ({
|
|
662
|
+
path: `.agents/skills/${pack.name}/SKILL.md`,
|
|
663
|
+
content: [
|
|
664
|
+
"---",
|
|
665
|
+
`name: ${pack.name}`,
|
|
666
|
+
`description: "${escapeYaml(pack.description)}"`,
|
|
667
|
+
"---",
|
|
668
|
+
"",
|
|
669
|
+
GENERATED_BLOCK_START,
|
|
670
|
+
(pack.files.skill ?? pack.files.rules).trim(),
|
|
671
|
+
GENERATED_BLOCK_END
|
|
672
|
+
].join("\n")
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/compiler/compileOutputs.ts
|
|
677
|
+
function compileOutputs(config, packs, analysis) {
|
|
678
|
+
const outputs = [];
|
|
679
|
+
if (config.tools.includes("codex")) {
|
|
680
|
+
outputs.push({ path: "AGENTS.md", content: compileAgentsMd(analysis, packs) });
|
|
681
|
+
outputs.push(...compileSkills(packs));
|
|
682
|
+
}
|
|
683
|
+
if (config.tools.includes("claude")) {
|
|
684
|
+
outputs.push({ path: "CLAUDE.md", content: compileClaudeMd(analysis, packs) });
|
|
685
|
+
}
|
|
686
|
+
if (config.tools.includes("cursor")) {
|
|
687
|
+
outputs.push(...compileCursorRules(analysis, packs));
|
|
688
|
+
}
|
|
689
|
+
if (config.tools.includes("copilot")) {
|
|
690
|
+
outputs.push({
|
|
691
|
+
path: ".github/copilot-instructions.md",
|
|
692
|
+
content: compileCopilotInstructions(analysis, packs)
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
return outputs;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/compiler/writeGeneratedFiles.ts
|
|
699
|
+
import path14 from "path";
|
|
700
|
+
|
|
701
|
+
// src/fs/safeWriteFile.ts
|
|
702
|
+
import path13 from "path";
|
|
703
|
+
import fs12 from "fs-extra";
|
|
704
|
+
async function safeWriteFile(filePath, generatedContent) {
|
|
705
|
+
const existingContent = await fs12.pathExists(filePath) ? await fs12.readFile(filePath, "utf8") : null;
|
|
706
|
+
const nextContent = updateGeneratedBlock(existingContent, generatedContent);
|
|
707
|
+
await fs12.ensureDir(path13.dirname(filePath));
|
|
708
|
+
await fs12.writeFile(filePath, nextContent);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/compiler/writeGeneratedFiles.ts
|
|
712
|
+
async function writeGeneratedFiles(root, outputs) {
|
|
713
|
+
for (const output of outputs) {
|
|
714
|
+
await safeWriteFile(path14.join(root, output.path), output.content);
|
|
715
|
+
}
|
|
716
|
+
return outputs.map((output) => output.path);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/sync.ts
|
|
720
|
+
import path15 from "path";
|
|
721
|
+
async function syncProject(root, providedConfig) {
|
|
722
|
+
const resolvedRoot = path15.resolve(root);
|
|
723
|
+
const analysis = await detectProject(resolvedRoot);
|
|
724
|
+
const config = providedConfig ?? await loadConfig(resolvedRoot);
|
|
725
|
+
const registry = await loadRegistry({
|
|
726
|
+
root: resolvedRoot,
|
|
727
|
+
sources: config.registries
|
|
728
|
+
});
|
|
729
|
+
const packs = resolvePacks(config.packs, registry);
|
|
730
|
+
await cacheRemotePacks(resolvedRoot, packs);
|
|
731
|
+
await saveInstalledPacks(resolvedRoot, packs);
|
|
732
|
+
const outputs = compileOutputs(config, packs, analysis);
|
|
733
|
+
const generatedFiles = await writeGeneratedFiles(resolvedRoot, outputs);
|
|
734
|
+
const nextConfig = {
|
|
735
|
+
...config,
|
|
736
|
+
packageManager: config.packageManager === "unknown" ? analysis.packageManager : config.packageManager,
|
|
737
|
+
generatedFiles
|
|
738
|
+
};
|
|
739
|
+
await saveConfig(resolvedRoot, nextConfig);
|
|
740
|
+
return {
|
|
741
|
+
root: resolvedRoot,
|
|
742
|
+
analysis,
|
|
743
|
+
generatedFiles,
|
|
744
|
+
outputs,
|
|
745
|
+
config: nextConfig
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/doctor/doctorProject.ts
|
|
750
|
+
import path16 from "path";
|
|
751
|
+
import fs13 from "fs-extra";
|
|
752
|
+
async function fileExists(root, relativePath) {
|
|
753
|
+
return fs13.pathExists(path16.join(root, relativePath));
|
|
754
|
+
}
|
|
755
|
+
async function doctorProject(root) {
|
|
756
|
+
const checks = [];
|
|
757
|
+
const issues = [];
|
|
758
|
+
const resolvedRoot = path16.resolve(root);
|
|
759
|
+
if (!await fileExists(resolvedRoot, CONFIG_PATH)) {
|
|
760
|
+
return {
|
|
761
|
+
checks,
|
|
762
|
+
issues: [
|
|
763
|
+
{
|
|
764
|
+
level: "error",
|
|
765
|
+
message: "ContextForge config was not found. Run `npx @contextforge/cli init`."
|
|
766
|
+
}
|
|
767
|
+
]
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
checks.push("Config found");
|
|
771
|
+
const config = await loadConfig(resolvedRoot);
|
|
772
|
+
const [analysis, registry, packageJson] = await Promise.all([
|
|
773
|
+
detectProject(resolvedRoot),
|
|
774
|
+
loadRegistry({ root: resolvedRoot, sources: config.registries }),
|
|
775
|
+
readPackageJson(resolvedRoot)
|
|
776
|
+
]);
|
|
777
|
+
const packs = resolvePacks(config.packs, registry);
|
|
778
|
+
const requiredFiles = [
|
|
779
|
+
[config.tools.includes("codex"), "AGENTS.md", "Codex instructions found"],
|
|
780
|
+
[config.tools.includes("claude"), "CLAUDE.md", "Claude instructions found"],
|
|
781
|
+
[config.tools.includes("cursor"), ".cursor/rules", "Cursor rules found"],
|
|
782
|
+
[config.tools.includes("copilot"), ".github/copilot-instructions.md", "Copilot instructions found"]
|
|
783
|
+
];
|
|
784
|
+
for (const [enabled, relativePath, okMessage] of requiredFiles) {
|
|
785
|
+
if (!enabled) {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (await fileExists(resolvedRoot, relativePath)) {
|
|
789
|
+
checks.push(okMessage);
|
|
790
|
+
} else {
|
|
791
|
+
issues.push({
|
|
792
|
+
level: "error",
|
|
793
|
+
message: `${relativePath} is missing. Run \`npx @contextforge/cli sync\`.`
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
for (const generatedFile of config.generatedFiles) {
|
|
798
|
+
if (!await fileExists(resolvedRoot, generatedFile)) {
|
|
799
|
+
issues.push({
|
|
800
|
+
level: "warning",
|
|
801
|
+
message: `Previously generated file ${generatedFile} is missing.`
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
for (const pack of packs) {
|
|
806
|
+
if (!await packMatchesProject(pack, resolvedRoot, packageJson)) {
|
|
807
|
+
issues.push({
|
|
808
|
+
level: "warning",
|
|
809
|
+
message: `${pack.name} pack is installed, but its detection hints do not match this project.`
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (config.packageManager !== "unknown" && analysis.packageManager !== "unknown" && config.packageManager !== analysis.packageManager) {
|
|
814
|
+
issues.push({
|
|
815
|
+
level: "warning",
|
|
816
|
+
message: `Config says package manager is ${config.packageManager}, but ${analysis.packageManager} was detected.`
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
if (config.packs.includes("testing-workflow") && !hasScript(packageJson, "test")) {
|
|
820
|
+
issues.push({
|
|
821
|
+
level: "warning",
|
|
822
|
+
message: "testing-workflow is installed, but package.json has no test script."
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
return { checks, issues };
|
|
826
|
+
}
|
|
827
|
+
export {
|
|
828
|
+
CONFIG_PATH,
|
|
829
|
+
ConfigSchema,
|
|
830
|
+
DEFAULT_REGISTRY_SOURCES,
|
|
831
|
+
DEFAULT_TOOLS,
|
|
832
|
+
GENERATED_BLOCK_END,
|
|
833
|
+
GENERATED_BLOCK_START,
|
|
834
|
+
OFFICIAL_REGISTRY_SOURCE,
|
|
835
|
+
OFFICIAL_REGISTRY_URL,
|
|
836
|
+
PROJECT_PACK_CACHE,
|
|
837
|
+
PackSchema,
|
|
838
|
+
RemotePackEntrySchema,
|
|
839
|
+
RemotePackFilesSchema,
|
|
840
|
+
RemoteRegistryIndexSchema,
|
|
841
|
+
addPackToConfig,
|
|
842
|
+
cacheRemotePacks,
|
|
843
|
+
compileOutputs,
|
|
844
|
+
createConfig,
|
|
845
|
+
detectPackageManager,
|
|
846
|
+
detectProject,
|
|
847
|
+
doctorProject,
|
|
848
|
+
findPack,
|
|
849
|
+
getGeneratedBlock,
|
|
850
|
+
hasPackage,
|
|
851
|
+
hasScript,
|
|
852
|
+
loadConfig,
|
|
853
|
+
loadRegistry,
|
|
854
|
+
packMatchesProject,
|
|
855
|
+
packageManagerLabel,
|
|
856
|
+
readPackageJson,
|
|
857
|
+
recommendPacks,
|
|
858
|
+
resolvePacks,
|
|
859
|
+
safeWriteFile,
|
|
860
|
+
saveConfig,
|
|
861
|
+
saveInstalledPacks,
|
|
862
|
+
syncProject,
|
|
863
|
+
updateGeneratedBlock,
|
|
864
|
+
wrapGeneratedBlock,
|
|
865
|
+
writeGeneratedFiles
|
|
866
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contextforge/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"fast-glob": "^3.3.3",
|
|
18
|
+
"fs-extra": "^11.3.0",
|
|
19
|
+
"zod": "^4.1.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
}
|
|
25
|
+
}
|