@contextforge/cli 0.1.2 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +191 -86
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2,19 +2,14 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import pc5 from "picocolors";
5
+ import pc7 from "picocolors";
6
6
 
7
7
  // src/commands/add.ts
8
8
  import {
9
- addPackToConfig,
10
- cacheRemotePacks,
11
- findPack,
12
- loadConfig,
13
- loadRegistry,
14
- packMatchesProject,
15
- readPackageJson,
16
- saveConfig,
17
- syncProject
9
+ fetchRegistry,
10
+ findPackSummary,
11
+ installPackAndSync,
12
+ loadConfig
18
13
  } from "@contextforge/core";
19
14
  import pc2 from "picocolors";
20
15
 
@@ -39,6 +34,7 @@ function formatAnalysis(analysis) {
39
34
  `shadcn/ui: ${yesNo(analysis.styling.shadcn)}`,
40
35
  `Prisma: ${yesNo(analysis.database.prisma)}`,
41
36
  `Drizzle: ${yesNo(analysis.database.drizzle)}`,
37
+ `Supabase: ${yesNo(analysis.services.supabase)}`,
42
38
  `Testing: ${testing || "not detected"}`,
43
39
  ""
44
40
  ].join("\n");
@@ -68,7 +64,7 @@ function formatDoctorReport(report) {
68
64
  }
69
65
 
70
66
  // src/registryOptions.ts
71
- import { DEFAULT_REGISTRY_SOURCES } from "@contextforge/core";
67
+ import { OFFICIAL_REGISTRY_URL } from "@contextforge/core";
72
68
  function collectRegistryOption(value, previous = []) {
73
69
  return [...previous, value];
74
70
  }
@@ -76,47 +72,55 @@ function envRegistries() {
76
72
  return (process.env.CONTEXTFORGE_REGISTRY_URL ?? "").split(",").map((value) => value.trim()).filter(Boolean);
77
73
  }
78
74
  function resolveRegistrySources(config, options) {
79
- const configured = config?.registries?.length ? config.registries : DEFAULT_REGISTRY_SOURCES;
80
- return [.../* @__PURE__ */ new Set([...configured, ...envRegistries(), ...options?.registry ?? []])];
75
+ const configured = config?.registry ? [config.registry] : [OFFICIAL_REGISTRY_URL];
76
+ return [.../* @__PURE__ */ new Set([...options?.registry ?? [], ...envRegistries(), ...configured])];
77
+ }
78
+ function resolveRegistryUrl(config, options) {
79
+ return resolveRegistrySources(config, options)[0] ?? OFFICIAL_REGISTRY_URL;
81
80
  }
82
81
 
83
82
  // src/commands/add.ts
83
+ async function loadOptionalConfig(root) {
84
+ try {
85
+ return await loadConfig(root);
86
+ } catch {
87
+ return void 0;
88
+ }
89
+ }
84
90
  async function addCommand(packName, options = {}) {
85
91
  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);
92
+ const config = await loadOptionalConfig(root);
93
+ const registryUrl = resolveRegistryUrl(config, options);
94
+ const registry = await fetchRegistry(registryUrl);
95
+ const summary = findPackSummary(registry, packName);
96
+ if (!summary) {
97
+ throw new Error(`Unknown ContextForge pack "${packName}". Run \`npx @contextforge/cli list\` to see available packs.`);
98
+ }
99
+ const alreadyConfigured = config?.installedPacks.includes(packName) ?? false;
100
+ if (alreadyConfigured && !options.force) {
101
+ console.log(pc2.yellow(`${packName} is already installed. Run \`npx @contextforge/cli sync\` to refresh it.`));
102
+ return;
103
+ }
104
+ if (options.dryRun) {
105
+ console.log(pc2.cyan(`Dry run: ${packName} would be downloaded from ${registryUrl}.`));
106
+ console.log(pc2.dim(`Pack path: ${summary.path}`));
107
+ return;
108
+ }
109
+ const result = await installPackAndSync(root, registryUrl, packName, {
110
+ force: options.force,
111
+ dryRun: false
112
+ });
110
113
  console.log(
111
- alreadyInstalled ? pc2.yellow(`${packName} was already installed. Synced generated files.`) : pc2.green(`Added ${packName}.`)
114
+ result.alreadyInstalled ? pc2.green(`Reinstalled ${packName}.`) : pc2.green(`Added ${packName}.`)
112
115
  );
113
116
  console.log(formatGeneratedFiles(result.generatedFiles));
114
117
  }
115
118
 
116
119
  // src/commands/doctor.ts
117
120
  import { doctorProject } from "@contextforge/core";
118
- async function doctorCommand() {
119
- const report = await doctorProject(process.cwd());
121
+ async function doctorCommand(options = {}) {
122
+ const registry = options.registry?.at(-1);
123
+ const report = await doctorProject(process.cwd(), registry ? { registry } : void 0);
120
124
  console.log(formatDoctorReport(report));
121
125
  if (report.issues.some((issue) => issue.level === "error")) {
122
126
  process.exitCode = 1;
@@ -124,16 +128,17 @@ async function doctorCommand() {
124
128
  }
125
129
 
126
130
  // src/commands/init.ts
127
- import { checkbox } from "@inquirer/prompts";
131
+ import { checkbox, confirm } from "@inquirer/prompts";
128
132
  import {
133
+ DEFAULT_CORE_PACKS,
129
134
  DEFAULT_TOOLS,
130
- cacheRemotePacks as cacheRemotePacks2,
131
135
  createConfig,
132
136
  detectProject,
133
- loadRegistry as loadRegistry2,
137
+ fetchRegistry as fetchRegistry2,
138
+ installPack,
139
+ missingMandatoryCorePacks,
134
140
  recommendPacks,
135
- saveConfig as saveConfig2,
136
- syncProject as syncProject2
141
+ syncProject
137
142
  } from "@contextforge/core";
138
143
  import ora from "ora";
139
144
  import pc3 from "picocolors";
@@ -143,6 +148,12 @@ var TOOL_LABELS = {
143
148
  cursor: "Cursor",
144
149
  copilot: "GitHub Copilot"
145
150
  };
151
+ var CAPABILITY_PACKS = [
152
+ "system-design",
153
+ "frontend-system-design",
154
+ "api-design",
155
+ "test-driven-development"
156
+ ];
146
157
  function canPrompt() {
147
158
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
148
159
  }
@@ -150,92 +161,186 @@ async function selectTools() {
150
161
  if (!canPrompt()) {
151
162
  return DEFAULT_TOOLS;
152
163
  }
153
- return checkbox({
164
+ const selected = await checkbox({
154
165
  message: "Which AI tools should ContextForge configure?",
155
166
  required: true,
156
- choices: DEFAULT_TOOLS.map((tool) => ({
157
- name: TOOL_LABELS[tool],
158
- value: tool,
167
+ choices: [
168
+ { name: "All agents", value: "all", checked: true },
169
+ ...DEFAULT_TOOLS.map((tool) => ({
170
+ name: TOOL_LABELS[tool],
171
+ value: tool,
172
+ checked: false
173
+ }))
174
+ ]
175
+ });
176
+ return selected.includes("all") ? DEFAULT_TOOLS : selected.filter((tool) => tool !== "all");
177
+ }
178
+ async function selectRecommendedPacks(recommended) {
179
+ if (recommended.length === 0) {
180
+ return [];
181
+ }
182
+ if (!canPrompt()) {
183
+ return [];
184
+ }
185
+ const wantsRecommendations = await confirm({
186
+ message: "Install recommended stack-specific packs?",
187
+ default: true
188
+ });
189
+ if (!wantsRecommendations) {
190
+ return [];
191
+ }
192
+ const selected = await checkbox({
193
+ message: "Select recommended packs to install",
194
+ choices: recommended.map((pack) => ({
195
+ name: `${pack.title} (${pack.name})`,
196
+ value: pack.name,
197
+ description: pack.description,
159
198
  checked: true
160
199
  }))
161
200
  });
201
+ return recommended.filter((pack) => selected.includes(pack.name));
162
202
  }
163
- async function selectPacks(registry, recommended) {
164
- if (registry.length === 0) {
203
+ async function selectCapabilityPacks(registryPacks) {
204
+ if (!canPrompt()) {
165
205
  return [];
166
206
  }
167
- if (!canPrompt()) {
168
- return recommended;
207
+ const available = CAPABILITY_PACKS.map(
208
+ (packName) => registryPacks.find((pack) => pack.name === packName)
209
+ ).filter((pack) => Boolean(pack));
210
+ if (available.length === 0) {
211
+ return [];
169
212
  }
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) => ({
213
+ const selected = await checkbox({
214
+ message: "Add optional architecture or testing packs?",
215
+ choices: available.map((pack) => ({
175
216
  name: `${pack.title} (${pack.name})`,
176
217
  value: pack.name,
177
218
  description: pack.description,
178
- checked: recommendedNames.has(pack.name)
219
+ checked: false
179
220
  }))
180
221
  });
181
- return registry.filter((pack) => selectedNames.includes(pack.name));
222
+ return available.filter((pack) => selected.includes(pack.name));
182
223
  }
183
224
  async function initCommand(options = {}) {
184
225
  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
- ]);
226
+ const registryUrl = resolveRegistryUrl(void 0, options);
227
+ const spinner = ora("Detecting project and fetching registry").start();
228
+ const [analysis, registry] = await Promise.all([detectProject(root), fetchRegistry2(registryUrl)]);
191
229
  const recommended = await recommendPacks(analysis, registry);
230
+ const missingMandatory = missingMandatoryCorePacks(registry);
192
231
  spinner.succeed("Project detected");
193
232
  console.log(formatAnalysis(analysis));
233
+ if (missingMandatory.length > 0) {
234
+ console.log(pc3.yellow(`Mandatory packs missing from registry: ${missingMandatory.join(", ")}`));
235
+ console.log("");
236
+ }
194
237
  if (recommended.length > 0) {
195
- console.log(pc3.bold("Recommended packs:"));
238
+ console.log(pc3.bold("Recommended optional packs:"));
196
239
  for (const pack of recommended) {
197
240
  console.log(`${pc3.green("OK")} ${pack.name}`);
198
241
  }
199
242
  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
243
  }
206
244
  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);
245
+ const optionalPacks = await selectRecommendedPacks(recommended);
246
+ const capabilityPacks = await selectCapabilityPacks(registry.packs);
247
+ const packNames = [
248
+ .../* @__PURE__ */ new Set([
249
+ ...DEFAULT_CORE_PACKS.filter((packName) => registry.packs.some((pack) => pack.name === packName)),
250
+ ...optionalPacks.map((pack) => pack.name),
251
+ ...capabilityPacks.map((pack) => pack.name)
252
+ ])
253
+ ];
254
+ for (const packName of packNames) {
255
+ await installPack(root, registryUrl, packName, { force: true });
256
+ }
257
+ const initialConfig = createConfig(
258
+ analysis,
259
+ packNames.map((name) => ({ name })),
260
+ tools,
261
+ registryUrl
262
+ );
263
+ const result = await syncProject(root, initialConfig);
212
264
  console.log(pc3.green("ContextForge initialized."));
213
265
  console.log(formatGeneratedFiles(result.generatedFiles));
214
266
  }
215
267
 
216
- // src/commands/sync.ts
217
- import { loadConfig as loadConfig2, syncProject as syncProject3 } from "@contextforge/core";
268
+ // src/commands/list.ts
269
+ import { fetchRegistry as fetchRegistry3, listRegistryPacks } from "@contextforge/core";
218
270
  import pc4 from "picocolors";
271
+ async function listCommand(options = {}) {
272
+ const registryUrl = resolveRegistryUrl(void 0, options);
273
+ const registry = await fetchRegistry3(registryUrl);
274
+ const packs = listRegistryPacks(registry);
275
+ const topics = /* @__PURE__ */ new Map();
276
+ for (const pack of packs) {
277
+ const topicPacks = topics.get(pack.topic) ?? [];
278
+ topicPacks.push(pack);
279
+ topics.set(pack.topic, topicPacks);
280
+ }
281
+ console.log(pc4.bold("ContextForge Registry"));
282
+ console.log(pc4.dim(registryUrl));
283
+ console.log("");
284
+ for (const [topic, topicPacks] of topics) {
285
+ console.log(pc4.bold(topic));
286
+ for (const pack of topicPacks) {
287
+ console.log(` ${pc4.green(pack.name)} - ${pack.title}`);
288
+ if (pack.description) {
289
+ console.log(` ${pc4.dim(pack.description)}`);
290
+ }
291
+ }
292
+ console.log("");
293
+ }
294
+ }
295
+
296
+ // src/commands/search.ts
297
+ import { fetchRegistry as fetchRegistry4, searchRegistryPacks } from "@contextforge/core";
298
+ import pc5 from "picocolors";
299
+ async function searchCommand(query, options = {}) {
300
+ const registryUrl = resolveRegistryUrl(void 0, options);
301
+ const registry = await fetchRegistry4(registryUrl);
302
+ const results = searchRegistryPacks(registry, query);
303
+ if (results.length === 0) {
304
+ console.log(pc5.yellow(`No packs found for "${query}".`));
305
+ return;
306
+ }
307
+ console.log(pc5.bold(`ContextForge packs matching "${query}"`));
308
+ console.log("");
309
+ for (const pack of results) {
310
+ console.log(`${pc5.green(pack.name)} ${pc5.dim(`[${pack.topic}]`)}`);
311
+ console.log(` ${pack.title}`);
312
+ if (pack.description) {
313
+ console.log(` ${pc5.dim(pack.description)}`);
314
+ }
315
+ }
316
+ }
317
+
318
+ // src/commands/sync.ts
319
+ import { loadConfig as loadConfig2, syncProject as syncProject2 } from "@contextforge/core";
320
+ import pc6 from "picocolors";
219
321
  async function syncCommand(options = {}) {
220
322
  const root = process.cwd();
221
323
  const config = await loadConfig2(root);
222
- const result = await syncProject3(root, {
324
+ const registry = resolveRegistryUrl(config, options);
325
+ const result = await syncProject2(root, {
223
326
  ...config,
224
- registries: resolveRegistrySources(config, options)
327
+ registry
225
328
  });
226
- console.log(pc4.green("ContextForge synced."));
329
+ console.log(pc6.green("ContextForge synced."));
227
330
  console.log(formatGeneratedFiles(result.generatedFiles));
228
331
  }
229
332
 
230
333
  // src/index.ts
231
334
  var program = new Command();
232
- program.name("contextforge").description("Make existing codebases AI-agent ready").version("0.1.2");
335
+ program.name("contextforge").description("Make existing codebases AI-agent ready").version("0.1.5");
233
336
  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);
337
+ program.command("add").argument("<pack>").description("Add an instruction pack").option("--registry <url>", "Static remote registry index URL", collectRegistryOption, []).option("--force", "Re-download and regenerate even when the pack is already installed").option("--dry-run", "Show what would be installed without writing files").action(addCommand);
235
338
  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);
339
+ program.command("doctor").description("Check whether AI instructions match the repo").option("--registry <url>", "Static remote registry index URL", collectRegistryOption, []).action(doctorCommand);
340
+ program.command("list").description("List available instruction packs").option("--registry <url>", "Static remote registry index URL", collectRegistryOption, []).action(listCommand);
341
+ program.command("search").argument("<query>").description("Search available instruction packs").option("--registry <url>", "Static remote registry index URL", collectRegistryOption, []).action(searchCommand);
237
342
  program.parseAsync().catch((error) => {
238
343
  const message = error instanceof Error ? error.message : String(error);
239
- console.error(pc5.red(`Error: ${message}`));
344
+ console.error(pc7.red(`Error: ${message}`));
240
345
  process.exitCode = 1;
241
346
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextforge/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "contextforge": "dist/index.js"
@@ -13,7 +13,7 @@
13
13
  "commander": "^14.0.0",
14
14
  "ora": "^8.2.0",
15
15
  "picocolors": "^1.1.1",
16
- "@contextforge/core": "0.1.2"
16
+ "@contextforge/core": "0.1.5"
17
17
  },
18
18
  "devDependencies": {
19
19
  "tsx": "^4.20.0"