@contextforge/cli 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 +1 -0
- package/dist/index.js +241 -0
- package/package.json +26 -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 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import pc5 from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/commands/add.ts
|
|
8
|
+
import {
|
|
9
|
+
addPackToConfig,
|
|
10
|
+
cacheRemotePacks,
|
|
11
|
+
findPack,
|
|
12
|
+
loadConfig,
|
|
13
|
+
loadRegistry,
|
|
14
|
+
packMatchesProject,
|
|
15
|
+
readPackageJson,
|
|
16
|
+
saveConfig,
|
|
17
|
+
syncProject
|
|
18
|
+
} from "@contextforge/core";
|
|
19
|
+
import pc2 from "picocolors";
|
|
20
|
+
|
|
21
|
+
// src/format.ts
|
|
22
|
+
import pc from "picocolors";
|
|
23
|
+
function yesNo(value) {
|
|
24
|
+
return value ? pc.green("yes") : pc.dim("no");
|
|
25
|
+
}
|
|
26
|
+
function formatAnalysis(analysis) {
|
|
27
|
+
const testing = [
|
|
28
|
+
analysis.testing.vitest && "Vitest",
|
|
29
|
+
analysis.testing.jest && "Jest",
|
|
30
|
+
analysis.testing.playwright && "Playwright"
|
|
31
|
+
].filter(Boolean).join(", ");
|
|
32
|
+
return [
|
|
33
|
+
pc.bold("ContextForge detected:"),
|
|
34
|
+
"",
|
|
35
|
+
`Framework: ${analysis.framework}`,
|
|
36
|
+
`Language: ${analysis.language}`,
|
|
37
|
+
`Package manager: ${analysis.packageManager}`,
|
|
38
|
+
`Tailwind CSS: ${yesNo(analysis.styling.tailwind)}`,
|
|
39
|
+
`shadcn/ui: ${yesNo(analysis.styling.shadcn)}`,
|
|
40
|
+
`Prisma: ${yesNo(analysis.database.prisma)}`,
|
|
41
|
+
`Drizzle: ${yesNo(analysis.database.drizzle)}`,
|
|
42
|
+
`Testing: ${testing || "not detected"}`,
|
|
43
|
+
""
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
function formatGeneratedFiles(files) {
|
|
47
|
+
if (files.length === 0) {
|
|
48
|
+
return pc.dim("No files generated.");
|
|
49
|
+
}
|
|
50
|
+
return ["Generated:", ...files.map((file) => `${pc.green("OK")} ${file}`)].join("\n");
|
|
51
|
+
}
|
|
52
|
+
function formatDoctorReport(report) {
|
|
53
|
+
const lines = [pc.bold("ContextForge Doctor"), ""];
|
|
54
|
+
for (const check of report.checks) {
|
|
55
|
+
lines.push(`${pc.green("OK")} ${check}`);
|
|
56
|
+
}
|
|
57
|
+
if (report.issues.length === 0) {
|
|
58
|
+
lines.push("", pc.green("No issues found."));
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
lines.push("", pc.bold("Issues:"));
|
|
62
|
+
for (const issue of report.issues) {
|
|
63
|
+
const marker = issue.level === "error" ? pc.red("ERR") : pc.yellow("WARN");
|
|
64
|
+
lines.push(`${marker} ${issue.message}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push("", "Run:", "npx @contextforge/cli sync");
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/registryOptions.ts
|
|
71
|
+
import { DEFAULT_REGISTRY_SOURCES } from "@contextforge/core";
|
|
72
|
+
function collectRegistryOption(value, previous = []) {
|
|
73
|
+
return [...previous, value];
|
|
74
|
+
}
|
|
75
|
+
function envRegistries() {
|
|
76
|
+
return (process.env.CONTEXTFORGE_REGISTRY_URL ?? "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
77
|
+
}
|
|
78
|
+
function resolveRegistrySources(config, options) {
|
|
79
|
+
const configured = config?.registries?.length ? config.registries : DEFAULT_REGISTRY_SOURCES;
|
|
80
|
+
return [.../* @__PURE__ */ new Set([...configured, ...envRegistries(), ...options?.registry ?? []])];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/commands/add.ts
|
|
84
|
+
async function addCommand(packName, options = {}) {
|
|
85
|
+
const root = process.cwd();
|
|
86
|
+
const config = await loadConfig(root);
|
|
87
|
+
const registrySources = resolveRegistrySources(config, options);
|
|
88
|
+
const registry = await loadRegistry({ root, sources: registrySources });
|
|
89
|
+
const pack = findPack(registry, packName);
|
|
90
|
+
if (!pack) {
|
|
91
|
+
throw new Error(`Unknown pack "${packName}".`);
|
|
92
|
+
}
|
|
93
|
+
const packageJson = await readPackageJson(root);
|
|
94
|
+
const compatible = await packMatchesProject(pack, root, packageJson);
|
|
95
|
+
const alreadyInstalled = config.packs.includes(packName);
|
|
96
|
+
const nextConfig = {
|
|
97
|
+
...addPackToConfig(config, packName),
|
|
98
|
+
registries: registrySources
|
|
99
|
+
};
|
|
100
|
+
if (!compatible) {
|
|
101
|
+
console.log(
|
|
102
|
+
pc2.yellow(
|
|
103
|
+
`${packName} does not match the current project detection hints. Adding it anyway; run doctor if this was intentional.`
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
await cacheRemotePacks(root, [pack]);
|
|
108
|
+
await saveConfig(root, nextConfig);
|
|
109
|
+
const result = await syncProject(root, nextConfig);
|
|
110
|
+
console.log(
|
|
111
|
+
alreadyInstalled ? pc2.yellow(`${packName} was already installed. Synced generated files.`) : pc2.green(`Added ${packName}.`)
|
|
112
|
+
);
|
|
113
|
+
console.log(formatGeneratedFiles(result.generatedFiles));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/commands/doctor.ts
|
|
117
|
+
import { doctorProject } from "@contextforge/core";
|
|
118
|
+
async function doctorCommand() {
|
|
119
|
+
const report = await doctorProject(process.cwd());
|
|
120
|
+
console.log(formatDoctorReport(report));
|
|
121
|
+
if (report.issues.some((issue) => issue.level === "error")) {
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/commands/init.ts
|
|
127
|
+
import { checkbox } from "@inquirer/prompts";
|
|
128
|
+
import {
|
|
129
|
+
DEFAULT_TOOLS,
|
|
130
|
+
cacheRemotePacks as cacheRemotePacks2,
|
|
131
|
+
createConfig,
|
|
132
|
+
detectProject,
|
|
133
|
+
loadRegistry as loadRegistry2,
|
|
134
|
+
recommendPacks,
|
|
135
|
+
saveConfig as saveConfig2,
|
|
136
|
+
syncProject as syncProject2
|
|
137
|
+
} from "@contextforge/core";
|
|
138
|
+
import ora from "ora";
|
|
139
|
+
import pc3 from "picocolors";
|
|
140
|
+
var TOOL_LABELS = {
|
|
141
|
+
codex: "Codex",
|
|
142
|
+
claude: "Claude Code",
|
|
143
|
+
cursor: "Cursor",
|
|
144
|
+
copilot: "GitHub Copilot"
|
|
145
|
+
};
|
|
146
|
+
function canPrompt() {
|
|
147
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
148
|
+
}
|
|
149
|
+
async function selectTools() {
|
|
150
|
+
if (!canPrompt()) {
|
|
151
|
+
return DEFAULT_TOOLS;
|
|
152
|
+
}
|
|
153
|
+
return checkbox({
|
|
154
|
+
message: "Which AI tools should ContextForge configure?",
|
|
155
|
+
required: true,
|
|
156
|
+
choices: DEFAULT_TOOLS.map((tool) => ({
|
|
157
|
+
name: TOOL_LABELS[tool],
|
|
158
|
+
value: tool,
|
|
159
|
+
checked: true
|
|
160
|
+
}))
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async function selectPacks(registry, recommended) {
|
|
164
|
+
if (registry.length === 0) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
if (!canPrompt()) {
|
|
168
|
+
return recommended;
|
|
169
|
+
}
|
|
170
|
+
const recommendedNames = new Set(recommended.map((pack) => pack.name));
|
|
171
|
+
const selectedNames = await checkbox({
|
|
172
|
+
message: "Which instruction packs should be installed?",
|
|
173
|
+
required: true,
|
|
174
|
+
choices: registry.map((pack) => ({
|
|
175
|
+
name: `${pack.title} (${pack.name})`,
|
|
176
|
+
value: pack.name,
|
|
177
|
+
description: pack.description,
|
|
178
|
+
checked: recommendedNames.has(pack.name)
|
|
179
|
+
}))
|
|
180
|
+
});
|
|
181
|
+
return registry.filter((pack) => selectedNames.includes(pack.name));
|
|
182
|
+
}
|
|
183
|
+
async function initCommand(options = {}) {
|
|
184
|
+
const root = process.cwd();
|
|
185
|
+
const registrySources = resolveRegistrySources(void 0, options);
|
|
186
|
+
const spinner = ora("Detecting project").start();
|
|
187
|
+
const [analysis, registry] = await Promise.all([
|
|
188
|
+
detectProject(root),
|
|
189
|
+
loadRegistry2({ root, sources: registrySources })
|
|
190
|
+
]);
|
|
191
|
+
const recommended = recommendPacks(analysis, registry);
|
|
192
|
+
spinner.succeed("Project detected");
|
|
193
|
+
console.log(formatAnalysis(analysis));
|
|
194
|
+
if (recommended.length > 0) {
|
|
195
|
+
console.log(pc3.bold("Recommended packs:"));
|
|
196
|
+
for (const pack of recommended) {
|
|
197
|
+
console.log(`${pc3.green("OK")} ${pack.name}`);
|
|
198
|
+
}
|
|
199
|
+
console.log("");
|
|
200
|
+
} else if (registry.length === 0) {
|
|
201
|
+
console.log(
|
|
202
|
+
pc3.yellow("No packs were available from the official registry. Check your network or run with a private --registry URL.")
|
|
203
|
+
);
|
|
204
|
+
console.log("");
|
|
205
|
+
}
|
|
206
|
+
const tools = await selectTools();
|
|
207
|
+
const packs = await selectPacks(registry, recommended);
|
|
208
|
+
await cacheRemotePacks2(root, packs);
|
|
209
|
+
const initialConfig = createConfig(analysis, packs, tools, registrySources);
|
|
210
|
+
await saveConfig2(root, initialConfig);
|
|
211
|
+
const result = await syncProject2(root, initialConfig);
|
|
212
|
+
console.log(pc3.green("ContextForge initialized."));
|
|
213
|
+
console.log(formatGeneratedFiles(result.generatedFiles));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/commands/sync.ts
|
|
217
|
+
import { loadConfig as loadConfig2, syncProject as syncProject3 } from "@contextforge/core";
|
|
218
|
+
import pc4 from "picocolors";
|
|
219
|
+
async function syncCommand(options = {}) {
|
|
220
|
+
const root = process.cwd();
|
|
221
|
+
const config = await loadConfig2(root);
|
|
222
|
+
const result = await syncProject3(root, {
|
|
223
|
+
...config,
|
|
224
|
+
registries: resolveRegistrySources(config, options)
|
|
225
|
+
});
|
|
226
|
+
console.log(pc4.green("ContextForge synced."));
|
|
227
|
+
console.log(formatGeneratedFiles(result.generatedFiles));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/index.ts
|
|
231
|
+
var program = new Command();
|
|
232
|
+
program.name("contextforge").description("Make existing codebases AI-agent ready").version("0.1.0");
|
|
233
|
+
program.command("init").description("Initialize AI-agent instructions").option("--registry <url>", "Static remote registry index URL", collectRegistryOption, []).action(initCommand);
|
|
234
|
+
program.command("add").argument("<pack>").description("Add an instruction pack").option("--registry <url>", "Static remote registry index URL", collectRegistryOption, []).action(addCommand);
|
|
235
|
+
program.command("sync").description("Sync generated AI instruction files").option("--registry <url>", "Static remote registry index URL", collectRegistryOption, []).action(syncCommand);
|
|
236
|
+
program.command("doctor").description("Check whether AI instructions match the repo").action(doctorCommand);
|
|
237
|
+
program.parseAsync().catch((error) => {
|
|
238
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
239
|
+
console.error(pc5.red(`Error: ${message}`));
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contextforge/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"contextforge": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@inquirer/prompts": "^7.8.0",
|
|
13
|
+
"commander": "^14.0.0",
|
|
14
|
+
"ora": "^8.2.0",
|
|
15
|
+
"picocolors": "^1.1.1",
|
|
16
|
+
"@contextforge/core": "0.1.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"tsx": "^4.20.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "tsx src/index.ts",
|
|
23
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
24
|
+
"test": "vitest --passWithNoTests"
|
|
25
|
+
}
|
|
26
|
+
}
|