blockend-cli 1.3.1 → 1.4.1

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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +974 -298
  3. package/package.json +62 -17
  4. package/readme.md +53 -10
package/dist/index.js CHANGED
@@ -1,37 +1,395 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command } from "commander";
4
+ import { defineCommand, runMain } from "citty";
5
5
 
6
- // src/commands/init.ts
7
- import path, { join } from "path";
6
+ // src/commands/add.ts
7
+ import path, { join, dirname } from "path";
8
8
  import fs from "fs/promises";
9
+ import { exec } from "child_process";
10
+ import { intro, outro, select, spinner, confirm, isCancel } from "@clack/prompts";
11
+ import pc from "picocolors";
12
+ var REPO_OWNER = "codewithnuh";
13
+ var REPO_NAME = "blockend";
14
+ var BRANCH = "master";
15
+ var RAW_CDN_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}`;
16
+ var MANIFEST_URL = `${RAW_CDN_BASE}/registry/index.json`;
17
+ function outputError(json, message) {
18
+ if (json) {
19
+ process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
20
+ } else {
21
+ outro(pc.red(`\u2716 ${message}`));
22
+ }
23
+ }
24
+ function outputResult(json, result) {
25
+ if (json) {
26
+ process.stdout.write(JSON.stringify(result) + "\n");
27
+ } else {
28
+ if (result.success) {
29
+ outro(pc.cyan(`\u2728 ${result.message}`));
30
+ } else {
31
+ outro(pc.yellow(`\u2139 ${result.message}`));
32
+ }
33
+ }
34
+ }
35
+ function handleCancel(value) {
36
+ if (isCancel(value)) {
37
+ outro(pc.yellow("\u26A0 Operation cancelled. Exiting Blockend CLI cleanly."));
38
+ process.exit(0);
39
+ }
40
+ }
41
+ async function findUp(filename, startDir) {
42
+ let dir = startDir;
43
+ while (true) {
44
+ const checkPath = join(dir, filename);
45
+ try {
46
+ await fs.access(checkPath);
47
+ return checkPath;
48
+ } catch {
49
+ const parent = dirname(dir);
50
+ if (parent === dir) break;
51
+ dir = parent;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ async function addCommand(blockName, options = {}) {
57
+ const { yes = false, json = false } = options;
58
+ if (!json) {
59
+ intro(pc.bgBlack(pc.magenta(" Blockend Component Ingestion ")));
60
+ }
61
+ const cwd = process.cwd();
62
+ const configPath = await findUp("blockend.json", cwd);
63
+ if (!configPath) {
64
+ outputError(json, "blockend.json not found. Run 'npx blockend init' first.");
65
+ return;
66
+ }
67
+ const rootDir = dirname(configPath);
68
+ let config;
69
+ try {
70
+ const configFile = await fs.readFile(configPath, "utf-8");
71
+ config = JSON.parse(configFile);
72
+ } catch (error) {
73
+ outputError(json, "Failed to parse blockend.json layout configuration.");
74
+ if (!json) console.error(pc.dim(String(error)));
75
+ return;
76
+ }
77
+ if (config.language !== "typescript") {
78
+ if (json) {
79
+ outputError(
80
+ json,
81
+ "Blockend forces modern architectural standards. Registry exclusively supports TypeScript."
82
+ );
83
+ } else {
84
+ outro(
85
+ pc.yellow(
86
+ `
87
+ \u2139 Blockend forces modern architectural standards. Registry exclusively supports TypeScript.`
88
+ )
89
+ );
90
+ }
91
+ return;
92
+ }
93
+ const s = spinner();
94
+ if (!json) {
95
+ s.start("Connecting to remote Blockend Registry manifest...");
96
+ }
97
+ let registry;
98
+ try {
99
+ const response = await fetch(MANIFEST_URL);
100
+ if (!response.ok) throw new Error(`HTTP Error Status: ${response.status}`);
101
+ registry = await response.json();
102
+ if (!json) s.stop(pc.green("\u2714 Remote manifest synchronization complete."));
103
+ } catch (error) {
104
+ if (!json) {
105
+ s.stop(pc.red("\u2716 Failed to fetch the component registry from GitHub network paths."));
106
+ console.error(pc.dim(String(error)));
107
+ } else {
108
+ outputError(json, "Failed to fetch the component registry from GitHub network paths.");
109
+ }
110
+ return;
111
+ }
112
+ let blockMap = {};
113
+ if (registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks)) {
114
+ blockMap = registry.blocks;
115
+ } else {
116
+ blockMap = registry;
117
+ }
118
+ const envKey = String(config.environment);
119
+ let targetBlock = blockName;
120
+ if (yes && !targetBlock) {
121
+ outputError(
122
+ json,
123
+ "Block name required when using --yes flag. Usage: blockend add <block> --yes"
124
+ );
125
+ return;
126
+ }
127
+ if (!targetBlock) {
128
+ const availableOptions = Object.entries(blockMap).filter(([_, block]) => {
129
+ if (!block) return false;
130
+ const hasAdapter = block.adapters?.[envKey] !== void 0;
131
+ const hasEnvironment = block.environments?.[envKey] !== void 0;
132
+ return hasAdapter || hasEnvironment;
133
+ }).map(([key, block]) => ({
134
+ value: key,
135
+ label: `${key} - ${pc.dim(block.description)}`
136
+ }));
137
+ if (availableOptions.length === 0) {
138
+ if (json) {
139
+ outputError(
140
+ json,
141
+ `No backend blocks are currently available for your framework layer: [${envKey}].`
142
+ );
143
+ } else {
144
+ outro(
145
+ pc.yellow(
146
+ `\u26A0 No backend blocks are currently available for your framework layer: [${envKey}].`
147
+ )
148
+ );
149
+ }
150
+ return;
151
+ }
152
+ const selectBlockPrompt = await select({
153
+ message: "Which backend block would you like to inject?",
154
+ options: availableOptions
155
+ });
156
+ handleCancel(selectBlockPrompt);
157
+ targetBlock = selectBlockPrompt;
158
+ }
159
+ const blockMeta = blockMap[targetBlock];
160
+ if (!blockMeta) {
161
+ outputError(json, `Block "${targetBlock}" does not exist in the remote registry.`);
162
+ return;
163
+ }
164
+ const adapterContext = blockMeta.adapters?.[envKey] ?? blockMeta.environments?.[envKey];
165
+ if (!adapterContext) {
166
+ outputError(
167
+ json,
168
+ `The block "${targetBlock}" does not support your environment layout: ${envKey}`
169
+ );
170
+ return;
171
+ }
172
+ let selectedVariant;
173
+ const variantKeys = Object.keys(adapterContext.variants || {});
174
+ if (variantKeys.length === 0) {
175
+ outputError(
176
+ json,
177
+ `No architecture storage layout variants found for block framework: ${envKey}`
178
+ );
179
+ return;
180
+ }
181
+ if (yes) {
182
+ selectedVariant = variantKeys.includes("memory") ? "memory" : variantKeys[0];
183
+ } else if (variantKeys.includes("redis") && config.redisEnabled) {
184
+ selectedVariant = "redis";
185
+ } else if (variantKeys.length === 1) {
186
+ selectedVariant = variantKeys[0];
187
+ } else {
188
+ const selectVariantPrompt = await select({
189
+ message: "Which architectural storage variant do you want to back this block?",
190
+ options: variantKeys.map((vKey) => ({
191
+ value: vKey,
192
+ label: vKey.toUpperCase()
193
+ }))
194
+ });
195
+ handleCancel(selectVariantPrompt);
196
+ selectedVariant = selectVariantPrompt;
197
+ }
198
+ const variantMeta = adapterContext.variants[selectedVariant];
199
+ let physicalPath = config.paths.blocks;
200
+ if (physicalPath.startsWith("@")) {
201
+ physicalPath = "./src/blocks";
202
+ }
203
+ const targetFolder = path.resolve(rootDir, physicalPath, targetBlock);
204
+ const packageJsonPath = await findUp("package.json", rootDir);
205
+ if (!packageJsonPath) {
206
+ outputError(json, "Could not locate package.json in your current directory layout hierarchy.");
207
+ return;
208
+ }
209
+ const packageJsonDir = dirname(packageJsonPath);
210
+ let packageJson;
211
+ try {
212
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
213
+ packageJson = JSON.parse(packageJsonContent);
214
+ } catch (error) {
215
+ outputError(json, "Failed parsing file data configurations from target package.json location.");
216
+ if (!json) console.error(pc.dim(String(error)));
217
+ return;
218
+ }
219
+ const installedDeps = {
220
+ ...packageJson.dependencies ?? {},
221
+ ...packageJson.devDependencies ?? {}
222
+ };
223
+ const missingProdDeps = (adapterContext.dependencies ?? []).concat(variantMeta?.dependencies ?? []).filter((dep) => !(dep in installedDeps));
224
+ const missingDevDeps = (adapterContext.devDependencies ?? []).concat(variantMeta?.devDependencies ?? []).filter((dep) => !(dep in installedDeps));
225
+ const hasMissingDeps = missingProdDeps.length > 0 || missingDevDeps.length > 0;
226
+ if (hasMissingDeps) {
227
+ const allMissingNames = [...missingProdDeps, ...missingDevDeps];
228
+ if (!json) {
229
+ console.log(
230
+ pc.yellow(`
231
+ \u26A0\uFE0F Missing required infrastructure packages: ${allMissingNames.join(", ")}`)
232
+ );
233
+ }
234
+ const shouldInstallPrompt = yes ? true : await confirm({
235
+ message: "Would you like the CLI to automatically install these dependencies?",
236
+ initialValue: true
237
+ });
238
+ if (!yes) handleCancel(shouldInstallPrompt);
239
+ if (shouldInstallPrompt) {
240
+ const packageManager = await fs.access(join(packageJsonDir, "pnpm-lock.yaml")).then(() => "pnpm").catch(() => "npm");
241
+ if (!json) s.start(`Preparing native workspace via ${packageManager}...`);
242
+ try {
243
+ const installTasks = [];
244
+ if (missingProdDeps.length > 0) {
245
+ installTasks.push(
246
+ packageManager === "pnpm" ? `pnpm add ${missingProdDeps.join(" ")}` : `npm install ${missingProdDeps.join(" ")}`
247
+ );
248
+ }
249
+ if (missingDevDeps.length > 0) {
250
+ installTasks.push(
251
+ packageManager === "pnpm" ? `pnpm add -D ${missingDevDeps.join(" ")}` : `npm install --save-dev ${missingDevDeps.join(" ")}`
252
+ );
253
+ }
254
+ for (const installCmd of installTasks) {
255
+ if (!json) s.stop(pc.cyan(`Executing: ${installCmd}
256
+ `));
257
+ await new Promise((resolve, reject) => {
258
+ const child = exec(installCmd, { cwd: packageJsonDir });
259
+ child.stdout?.on("data", (data) => {
260
+ if (!json) process.stdout.write(pc.dim(data));
261
+ });
262
+ child.stderr?.on("data", (data) => {
263
+ if (!json) process.stdout.write(pc.dim(pc.red(data)));
264
+ });
265
+ child.on(
266
+ "close",
267
+ (code) => code === 0 ? resolve() : reject(new Error(`Exit code ${code}`))
268
+ );
269
+ });
270
+ }
271
+ if (!json) {
272
+ s.start(pc.green("\u2714 All dependencies synchronized successfully."));
273
+ s.stop();
274
+ }
275
+ } catch (error) {
276
+ if (!json) {
277
+ s.stop(pc.red("\u2716 Automated dependency installation failed. Please run setup manually."));
278
+ console.error(pc.dim(String(error)));
279
+ } else {
280
+ outputError(json, "Automated dependency installation failed.");
281
+ }
282
+ return;
283
+ }
284
+ }
285
+ }
286
+ const filesToDownload = [];
287
+ if (blockMeta.baseFiles) {
288
+ filesToDownload.push(...blockMeta.baseFiles);
289
+ }
290
+ if (adapterContext.core) {
291
+ filesToDownload.push({
292
+ source: adapterContext.core,
293
+ target: path.basename(adapterContext.core, ".txt")
294
+ });
295
+ }
296
+ if (variantMeta && Array.isArray(variantMeta.files)) {
297
+ for (const fileEntry of variantMeta.files) {
298
+ if (typeof fileEntry === "string") {
299
+ filesToDownload.push({
300
+ source: fileEntry,
301
+ target: path.basename(fileEntry, ".txt")
302
+ });
303
+ } else {
304
+ filesToDownload.push(fileEntry);
305
+ }
306
+ }
307
+ }
308
+ let fileExistsConflict = false;
309
+ for (const fileMap of filesToDownload) {
310
+ const destinationPath = join(targetFolder, fileMap.target);
311
+ try {
312
+ await fs.access(destinationPath);
313
+ fileExistsConflict = true;
314
+ break;
315
+ } catch {
316
+ }
317
+ }
318
+ if (fileExistsConflict) {
319
+ const overwritePrompt = yes ? true : await confirm({
320
+ message: `\u26A0 Components in [${targetBlock}] already exist. Overwrite custom revisions?`,
321
+ initialValue: false
322
+ });
323
+ if (!yes) handleCancel(overwritePrompt);
324
+ if (!overwritePrompt) {
325
+ outputResult(json, {
326
+ success: false,
327
+ reason: "aborted",
328
+ message: "Local modifications preserved."
329
+ });
330
+ return;
331
+ }
332
+ }
333
+ if (!json) {
334
+ s.start(`Downloading and building template adapter blocks [${targetBlock}]...`);
335
+ }
336
+ try {
337
+ for (const fileMap of filesToDownload) {
338
+ const fileUrl = `${RAW_CDN_BASE}/${fileMap.source}`;
339
+ const response = await fetch(fileUrl);
340
+ if (!response.ok) throw new Error(`Download failed for: ${fileMap.source}`);
341
+ const fileContent = await response.text();
342
+ const localWriteLocation = join(targetFolder, fileMap.target);
343
+ await fs.mkdir(dirname(localWriteLocation), { recursive: true });
344
+ await fs.writeFile(localWriteLocation, fileContent, "utf-8");
345
+ }
346
+ if (!json) s.stop(pc.green("\u2714 Component isolation structures written smoothly."));
347
+ outputResult(json, {
348
+ success: true,
349
+ block: targetBlock,
350
+ filesWritten: filesToDownload.map((f) => join(targetFolder, f.target)),
351
+ dependenciesInstalled: [...missingProdDeps, ...missingDevDeps],
352
+ message: `Source blocks written to ${physicalPath}/${targetBlock}/. Code ownership transferred!`
353
+ });
354
+ } catch (error) {
355
+ if (!json) {
356
+ s.stop(pc.red("\u2716 Fatal error occurred while assembling file mappings."));
357
+ console.error(pc.dim(String(error)));
358
+ } else {
359
+ outputError(json, "Fatal error occurred while assembling file mappings.");
360
+ }
361
+ }
362
+ }
363
+
364
+ // src/commands/init.ts
365
+ import path2, { join as join2 } from "path";
366
+ import fs2 from "fs/promises";
9
367
  import { existsSync } from "fs";
10
- import { intro, outro, select, text, confirm, spinner, isCancel } from "@clack/prompts";
368
+ import { intro as intro2, outro as outro2, select as select2, text, confirm as confirm2, spinner as spinner2, isCancel as isCancel2 } from "@clack/prompts";
11
369
  import { detectProject } from "@blockend/detector";
12
370
 
13
371
  // src/ui/theme.ts
14
- import pc from "picocolors";
372
+ import pc2 from "picocolors";
15
373
  var theme = {
16
374
  brand: {
17
- primary: pc.blue,
18
- title: pc.bold,
19
- logo: pc.white
375
+ primary: pc2.blue,
376
+ title: pc2.bold,
377
+ logo: pc2.white
20
378
  },
21
379
  text: {
22
- normal: pc.white,
23
- muted: pc.dim,
24
- subtle: pc.gray
380
+ normal: pc2.white,
381
+ muted: pc2.dim,
382
+ subtle: pc2.gray
25
383
  },
26
384
  state: {
27
- success: pc.green,
28
- warning: pc.yellow,
29
- error: pc.red,
30
- info: pc.blue
385
+ success: pc2.green,
386
+ warning: pc2.yellow,
387
+ error: pc2.red,
388
+ info: pc2.blue
31
389
  },
32
390
  emphasis: {
33
- strong: pc.bold,
34
- dim: pc.dim
391
+ strong: pc2.bold,
392
+ dim: pc2.dim
35
393
  }
36
394
  };
37
395
 
@@ -46,372 +404,690 @@ var format = {
46
404
  };
47
405
 
48
406
  // src/commands/init.ts
49
- async function initCommand() {
407
+ async function resolveTsConfigPaths(cwd) {
408
+ const possiblePaths = [join2(cwd, "tsconfig.json"), join2(cwd, "jsconfig.json")];
409
+ for (const configPath of possiblePaths) {
410
+ if (existsSync(configPath)) {
411
+ try {
412
+ const rawContent = await fs2.readFile(configPath, "utf-8");
413
+ const cleanJsonContent = rawContent.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
414
+ const parsed = JSON.parse(cleanJsonContent);
415
+ const baseUrl = parsed.compilerOptions?.baseUrl || ".";
416
+ const paths = parsed.compilerOptions?.paths || {};
417
+ return { baseUrl, paths };
418
+ } catch {
419
+ }
420
+ }
421
+ }
422
+ return { baseUrl: ".", paths: {} };
423
+ }
424
+ function outputInitError(json, message) {
425
+ if (json) {
426
+ process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
427
+ } else {
428
+ outro2(format.error(message));
429
+ }
430
+ }
431
+ function outputInitResult(json, result) {
432
+ if (json) {
433
+ process.stdout.write(JSON.stringify(result) + "\n");
434
+ } else {
435
+ outro2(theme.state.success(result.message));
436
+ }
437
+ }
438
+ async function initCommand(options = {}) {
439
+ const { yes = false, json = false } = options;
50
440
  const cwd = process.cwd();
51
- const configPath = join(cwd, "blockend.json");
52
- console.log(`
53
- \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
54
- \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
55
- \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
56
- \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
441
+ const configPath = join2(cwd, "blockend.json");
442
+ if (!json) {
443
+ console.log(`
444
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
445
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
446
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
447
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
57
448
  \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
58
449
  \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D
59
450
  `);
60
- intro(format.title("Blockend \xB7 Intelligent Backend Blocks Setup"));
451
+ intro2(theme.brand.primary(" Blockend \xB7 Intelligent Backend Blocks Setup "));
452
+ }
61
453
  if (existsSync(configPath)) {
62
- const action = await select({
63
- message: "blockend.json already exists. What do you want to do?",
64
- options: [
65
- { value: "keep", label: "Keep existing config (cancel init)" },
66
- { value: "overwrite", label: "Overwrite config" },
67
- { value: "regenerate", label: "Delete and regenerate" }
68
- ]
69
- });
70
- if (isCancel(action) || action === "keep") {
71
- outro(format.muted("Initialization cancelled. Existing config preserved."));
72
- return;
454
+ let action;
455
+ if (yes) {
456
+ action = "overwrite";
457
+ } else {
458
+ const actionPrompt = await select2({
459
+ message: "blockend.json already exists. What do you want to do?",
460
+ options: [
461
+ { value: "keep", label: "Keep existing config (cancel init)" },
462
+ { value: "overwrite", label: "Overwrite config" },
463
+ { value: "regenerate", label: "Delete and regenerate" }
464
+ ]
465
+ });
466
+ if (isCancel2(actionPrompt) || actionPrompt === "keep") {
467
+ if (json) {
468
+ outputInitResult(json, {
469
+ success: false,
470
+ message: "Initialization cancelled. Existing config preserved."
471
+ });
472
+ } else {
473
+ outro2(format.muted("Initialization cancelled. Existing config preserved."));
474
+ }
475
+ return;
476
+ }
477
+ action = actionPrompt;
73
478
  }
74
479
  if (action === "regenerate") {
75
- await fs.unlink(configPath);
76
- console.log(format.error("Existing config deleted"));
480
+ await fs2.unlink(configPath);
481
+ if (!json) console.log(format.error("Existing config deleted"));
77
482
  }
78
483
  }
79
- const s = spinner();
80
- s.start("Scanning project structure...");
484
+ const s = spinner2();
485
+ if (!json) s.start("Scanning project layout...");
81
486
  const context = await detectProject(cwd);
82
- s.stop(format.success("Project structure detected"));
83
- s.start("Analyzing framework signals...");
84
- await new Promise((r) => setTimeout(r, 400));
85
- s.stop(format.success(`Detected ${context.framework || "unknown"} environment`));
86
- s.start("Resolving alias mappings...");
87
- await new Promise((r) => setTimeout(r, 300));
88
- s.stop(format.success("Import strategy mapped"));
89
- console.log(
90
- format.muted(
91
- `System: ${context.framework || "unknown"} \xB7 ${context.language || "ts"} \xB7 aliases=${Object.keys(context.aliasMap || {}).length}`
92
- )
93
- );
94
- const framework = await select({
95
- message: "Confirm framework environment",
96
- initialValue: context.framework || "express",
97
- options: [
98
- { value: "express", label: "Express.js" },
99
- { value: "fastify", label: "Fastify" },
100
- { value: "next", label: "Next.js (App Router)" },
101
- { value: "hono", label: "Hono" }
102
- ]
103
- });
104
- if (isCancel(framework)) {
105
- outro(format.muted("Initialization cancelled."));
106
- return;
487
+ const hasSrcDir = existsSync(join2(cwd, "src"));
488
+ const tsConfig = await resolveTsConfigPaths(cwd);
489
+ if (!json) s.stop(format.success("Project architecture scanned"));
490
+ let framework = context.framework;
491
+ if (!framework) {
492
+ if (yes) {
493
+ framework = "express";
494
+ } else {
495
+ const frameworkSelect = await select2({
496
+ message: "Framework could not be auto-detected. Select framework environment manually:",
497
+ options: [
498
+ { value: "express", label: "Express.js" },
499
+ { value: "fastify", label: "Fastify" },
500
+ { value: "next", label: "Next.js (App Router)" },
501
+ { value: "hono", label: "Hono" }
502
+ ]
503
+ });
504
+ if (isCancel2(frameworkSelect)) {
505
+ if (json) {
506
+ outputInitResult(json, { success: false, message: "Initialization cancelled." });
507
+ } else {
508
+ outro2(format.muted("Initialization cancelled."));
509
+ }
510
+ return;
511
+ }
512
+ framework = frameworkSelect;
513
+ }
514
+ } else if (!json) {
515
+ console.log(
516
+ `${format.success("\u2714")} Framework environment detected: ${theme.state.info(framework)}`
517
+ );
107
518
  }
108
- const availableAliases = Object.keys(context.aliasMap || {});
109
- const baseAliasToken = availableAliases.length > 0 ? availableAliases[0] : "@/";
110
- const blockAliasInput = await text({
111
- message: "Configure blocks import alias",
112
- initialValue: `${baseAliasToken}blocks`,
113
- placeholder: `${baseAliasToken}blocks`
114
- });
115
- if (isCancel(blockAliasInput)) {
116
- outro(format.muted("Initialization cancelled."));
117
- return;
519
+ const defaultDir = hasSrcDir ? "src/blocks" : "blocks";
520
+ let rawPhysicalInput = defaultDir;
521
+ if (!yes) {
522
+ const directoryPrompt = await text({
523
+ message: "Configure the targeted physical directory destination for blocks:",
524
+ placeholder: defaultDir,
525
+ initialValue: defaultDir,
526
+ validate(value) {
527
+ if (value?.trim().length === 0) return "Physical path location directory cannot be empty.";
528
+ }
529
+ });
530
+ if (isCancel2(directoryPrompt)) {
531
+ outro2(format.muted("Initialization cancelled."));
532
+ return;
533
+ }
534
+ rawPhysicalInput = String(directoryPrompt).trim();
535
+ }
536
+ const relativeBlocksPath = path2.relative(cwd, path2.resolve(cwd, rawPhysicalInput));
537
+ let finalPath = relativeBlocksPath.replace(/\\/g, "/");
538
+ if (!finalPath.startsWith(".") && !finalPath.startsWith("/")) {
539
+ finalPath = `./${finalPath}`;
540
+ }
541
+ let finalBlockAlias = "@/blocks";
542
+ const configuredAliases = Object.keys(tsConfig.paths);
543
+ if (configuredAliases.length > 0) {
544
+ const matchedAliasKey = configuredAliases.find((alias) => {
545
+ const targets = tsConfig.paths[alias];
546
+ if (Array.isArray(targets) && targets.length > 0) {
547
+ const targetSubPath = targets[0].replace(/\*$/, "").replace(/\\/g, "/");
548
+ return finalPath.replace(/^\.\//, "").startsWith(targetSubPath);
549
+ }
550
+ return false;
551
+ });
552
+ if (matchedAliasKey) {
553
+ const cleanKey = matchedAliasKey.replace(/\*$/, "");
554
+ const targets = tsConfig.paths[matchedAliasKey];
555
+ const targetSubPath = targets[0].replace(/\*$/, "").replace(/\\/g, "/");
556
+ const relativeSuffix = finalPath.replace(/^\.\//, "").replace(targetSubPath, "");
557
+ finalBlockAlias = `${cleanKey}${relativeSuffix}`.replace(/\/$/, "");
558
+ } else {
559
+ const primaryAlias = configuredAliases.find((k) => k.startsWith("@")) || configuredAliases[0];
560
+ const inferredBaseAlias = primaryAlias.replace(/\*$/, "");
561
+ const folderSegmentName = path2.basename(finalPath);
562
+ finalBlockAlias = `${inferredBaseAlias}${folderSegmentName}`;
563
+ }
564
+ } else {
565
+ const folderSegmentName = path2.basename(finalPath);
566
+ finalBlockAlias = `@/${folderSegmentName}`;
118
567
  }
119
- const blockAlias = String(blockAliasInput);
120
- const aliasPrefix = availableAliases.find((a) => blockAlias.startsWith(a)) || "";
121
- const physicalPrefix = aliasPrefix ? context.aliasMap[aliasPrefix] : "./";
122
- const resolvedSubDir = blockAlias.replace(aliasPrefix, "");
123
- const assumedPhysicalDir = join(physicalPrefix, resolvedSubDir);
124
- const relativeBlocksPath = path.relative(cwd, path.resolve(cwd, assumedPhysicalDir));
125
- const normalizedPath = relativeBlocksPath.replace(/\\/g, "/");
126
- const finalPath = normalizedPath.startsWith(".") ? normalizedPath : `./${normalizedPath}`;
127
568
  let includeRedis = false;
128
569
  if (context.hasRedis) {
129
- const redisConfirm = await confirm({
130
- message: "Redis detected. Enable Redis-backed block variants automatically?",
131
- initialValue: true
132
- });
133
- if (!isCancel(redisConfirm)) {
134
- includeRedis = Boolean(redisConfirm);
570
+ if (yes) {
571
+ includeRedis = true;
572
+ } else {
573
+ const redisConfirm = await confirm2({
574
+ message: "Redis detected. Enable Redis-backed block variants automatically?",
575
+ initialValue: true
576
+ });
577
+ if (!isCancel2(redisConfirm)) {
578
+ includeRedis = Boolean(redisConfirm);
579
+ }
135
580
  }
136
581
  }
137
582
  const configPayload = {
138
583
  $schema: "https://blockend.dev/schema.json",
139
- environment: framework,
140
- language: "typescript",
584
+ environment: framework || "express",
585
+ language: context.language || "typescript",
141
586
  includeRedis,
142
587
  aliases: {
143
- blocks: blockAlias
588
+ blocks: finalBlockAlias
144
589
  },
145
590
  paths: {
146
591
  blocks: finalPath
147
592
  }
148
593
  };
149
- const writeSpinner = spinner();
150
- writeSpinner.start("Finalizing configuration...");
594
+ if (!json) s.start("Finalizing configuration...");
151
595
  try {
152
- await fs.writeFile(configPath, JSON.stringify(configPayload, null, 2), "utf-8");
153
- writeSpinner.stop(format.success("blockend.json ready"));
154
- outro(format.title("Blockend initialized successfully. Run: npx blockend add <block>"));
596
+ await fs2.writeFile(configPath, JSON.stringify(configPayload, null, 2), "utf-8");
597
+ if (!json) s.stop(format.success("blockend.json ready"));
598
+ outputInitResult(json, {
599
+ success: true,
600
+ message: "\u2728 Blockend initialized successfully. Run: npx blockend add <block>",
601
+ config: configPayload
602
+ });
155
603
  } catch {
156
- writeSpinner.stop(format.error("Failed to write configuration"));
604
+ if (!json) {
605
+ s.stop(format.error("Failed to write configuration"));
606
+ } else {
607
+ outputInitError(json, "Failed to write architectural layout configuration map.");
608
+ }
157
609
  }
158
610
  }
159
611
 
160
- // src/commands/add.ts
161
- import path2, { join as join2, dirname } from "path";
162
- import fs2 from "fs/promises";
163
- import { exec } from "child_process";
164
- import { intro as intro2, outro as outro2, select as select2, spinner as spinner2, confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
165
- import pc2 from "picocolors";
166
- var REPO_OWNER = "codewithnuh";
167
- var REPO_NAME = "blockend";
168
- var BRANCH = "master";
169
- var RAW_CDN_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}`;
170
- var MANIFEST_URL = `${RAW_CDN_BASE}/registry/index.json`;
171
- function handleCancel(value) {
172
- if (isCancel2(value)) {
173
- outro2(pc2.yellow("\u26A0 Operation cancelled. Exiting Blockend CLI cleanly."));
174
- process.exit(0);
612
+ // src/commands/detect.ts
613
+ import { detectProject as detectProject2 } from "@blockend/detector";
614
+ import { outro as outro3 } from "@clack/prompts";
615
+ import pc3 from "picocolors";
616
+ async function detectCommand(options = {}) {
617
+ const { json = false } = options;
618
+ try {
619
+ const context = await detectProject2(process.cwd());
620
+ if (json) {
621
+ process.stdout.write(JSON.stringify(context, null, 2) + "\n");
622
+ return;
623
+ }
624
+ console.log();
625
+ console.log(pc3.bold(" Detected project configuration:"));
626
+ console.log();
627
+ console.log(` Framework: ${pc3.cyan(context.framework)}`);
628
+ console.log(` Language: ${pc3.cyan(context.language)}`);
629
+ console.log(` Package manager: ${pc3.cyan(context.packageManager)}`);
630
+ console.log(` Source dir: ${pc3.dim(context.srcDir)}`);
631
+ console.log(
632
+ ` Redis: ${context.hasRedis ? pc3.green("detected") : pc3.dim("not found")}`
633
+ );
634
+ console.log(
635
+ ` Prisma: ${context.hasPrisma ? pc3.green("detected") : pc3.dim("not found")}`
636
+ );
637
+ console.log(
638
+ ` Drizzle: ${context.hasDrizzle ? pc3.green("detected") : pc3.dim("not found")}`
639
+ );
640
+ console.log();
641
+ } catch (error) {
642
+ if (json) {
643
+ process.stdout.write(JSON.stringify({ success: false, error: String(error) }) + "\n");
644
+ } else {
645
+ outro3(pc3.red(`\u2716 Detection failed: ${String(error)}`));
646
+ }
647
+ process.exit(1);
175
648
  }
176
649
  }
177
- async function findUp(filename, startDir) {
650
+
651
+ // src/commands/list.ts
652
+ import { dirname as dirname2, join as join3 } from "path";
653
+ import fs3 from "fs/promises";
654
+ import pc4 from "picocolors";
655
+ import { outro as outro4, spinner as spinner3 } from "@clack/prompts";
656
+ var REPO_OWNER2 = "codewithnuh";
657
+ var REPO_NAME2 = "blockend";
658
+ var BRANCH2 = "master";
659
+ var MANIFEST_URL2 = `https://raw.githubusercontent.com/${REPO_OWNER2}/${REPO_NAME2}/${BRANCH2}/registry/index.json`;
660
+ async function findUp2(filename, startDir) {
178
661
  let dir = startDir;
179
662
  while (true) {
180
- const checkPath = join2(dir, filename);
663
+ const checkPath = join3(dir, filename);
181
664
  try {
182
- await fs2.access(checkPath);
665
+ await fs3.access(checkPath);
183
666
  return checkPath;
184
667
  } catch {
185
- const parent = dirname(dir);
668
+ const parent = dirname2(dir);
186
669
  if (parent === dir) break;
187
670
  dir = parent;
188
671
  }
189
672
  }
190
673
  return null;
191
674
  }
192
- async function addCommand(blockName) {
193
- intro2(pc2.bgBlack(pc2.magenta(" Blockend Component Ingestion ")));
675
+ async function listCommand(options = {}) {
676
+ const { json = false } = options;
194
677
  const cwd = process.cwd();
195
- const configPath = await findUp("blockend.json", cwd);
678
+ const configPath = await findUp2("blockend.json", cwd);
196
679
  if (!configPath) {
197
- outro2(pc2.red("\u2716 blockend.json not found. Run 'npx blockend init' first."));
680
+ if (json) {
681
+ process.stdout.write(
682
+ JSON.stringify({ success: false, error: "blockend.json not found." }) + "\n"
683
+ );
684
+ } else {
685
+ outro4(pc4.red("\u2716 blockend.json not found. Run 'npx blockend init' first."));
686
+ }
198
687
  return;
199
688
  }
200
- const rootDir = dirname(configPath);
201
689
  let config;
202
690
  try {
203
- const configFile = await fs2.readFile(configPath, "utf-8");
691
+ const configFile = await fs3.readFile(configPath, "utf-8");
204
692
  config = JSON.parse(configFile);
205
693
  } catch {
206
- outro2(pc2.red("\u2716 Failed to parse blockend.json layout configuration."));
694
+ if (json) {
695
+ process.stdout.write(
696
+ JSON.stringify({ success: false, error: "Failed to parse configuration matrix." }) + "\n"
697
+ );
698
+ } else {
699
+ outro4(pc4.red("\u2716 Failed to parse blockend.json layout configuration."));
700
+ }
207
701
  return;
208
702
  }
209
- if (config.language !== "typescript") {
210
- outro2(
211
- pc2.yellow(
212
- `
213
- \u2139 Blockend forces modern architectural standards.
214
- To ensure strict type safety and absolute code ownership, the registry exclusively supports TypeScript.
215
- Please migrate your project configuration to TypeScript to ingest blocks.`
216
- )
217
- );
218
- return;
703
+ const envKey = String(config.environment);
704
+ const s = spinner3();
705
+ if (!json) {
706
+ s.start("Syncing registry metadata blocks...");
219
707
  }
220
- const s = spinner2();
221
- s.start("Connecting to remote Blockend Registry manifest...");
222
708
  let registry;
223
709
  try {
224
- const response = await fetch(MANIFEST_URL);
225
- if (!response.ok) throw new Error(`HTTP Error Status: ${response.status}`);
710
+ const response = await fetch(MANIFEST_URL2);
711
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
226
712
  registry = await response.json();
227
- s.stop(pc2.green("\u2714 Remote manifest synchronization complete."));
713
+ if (!json) s.stop(pc4.green("\u2714 Synced available block registries."));
228
714
  } catch {
229
- s.stop(pc2.red("\u2716 Failed to fetch the component registry from GitHub network paths."));
230
- return;
231
- }
232
- const envKey = String(config.environment);
233
- let targetBlock = blockName;
234
- if (!targetBlock) {
235
- const availableOptions = Object.keys(registry).filter((key) => registry[key].environments[envKey] !== void 0).map((key) => ({
236
- value: key,
237
- label: `${key} - ${pc2.dim(registry[key].description)}`
238
- }));
239
- if (availableOptions.length === 0) {
240
- outro2(
241
- pc2.yellow(
242
- `\u26A0 No backend blocks are currently available for your framework layer: [${envKey}].`
243
- )
715
+ if (json) {
716
+ process.stdout.write(
717
+ JSON.stringify({ success: false, error: "Network sync failure." }) + "\n"
244
718
  );
245
- return;
719
+ } else {
720
+ s.stop(pc4.red("\u2716 Failed to fetch the component registry from GitHub network paths."));
246
721
  }
247
- const selectBlockPrompt = await select2({
248
- message: "Which backend block would you like to inject?",
249
- options: availableOptions
250
- });
251
- handleCancel(selectBlockPrompt);
252
- targetBlock = selectBlockPrompt;
253
- }
254
- const blockMeta = registry[targetBlock];
255
- if (!blockMeta) {
256
- outro2(pc2.red(`\u2716 Block "${targetBlock}" does not exist in the remote registry.`));
257
722
  return;
258
723
  }
259
- const envConfig = blockMeta.environments[envKey];
260
- if (!envConfig) {
261
- outro2(
262
- pc2.red(`\u2716 The block "${targetBlock}" does not support your environment layout: ${envKey}`)
724
+ let blockMap = {};
725
+ if (registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks)) {
726
+ blockMap = registry.blocks;
727
+ } else {
728
+ blockMap = registry;
729
+ }
730
+ const blocksForEnv = Object.entries(blockMap).filter(([_, block]) => {
731
+ if (!block) return false;
732
+ const hasAdapter = block.adapters?.[envKey] !== void 0;
733
+ const hasEnvironment = block.environments?.[envKey] !== void 0;
734
+ return hasAdapter || hasEnvironment;
735
+ }).map(([key, block]) => {
736
+ const context = block.adapters?.[envKey] ?? block.environments?.[envKey];
737
+ return {
738
+ name: key,
739
+ description: block.description,
740
+ variants: Object.keys(context?.variants || {})
741
+ };
742
+ });
743
+ if (json) {
744
+ process.stdout.write(
745
+ JSON.stringify({ success: true, environment: envKey, blocks: blocksForEnv }) + "\n"
263
746
  );
264
- return;
747
+ } else {
748
+ console.log(`
749
+ Available backend blocks for ${pc4.magenta(envKey)}:`);
750
+ if (blocksForEnv.length === 0) {
751
+ console.log(pc4.dim(" No blocks found for this framework."));
752
+ } else {
753
+ blocksForEnv.forEach((b) => {
754
+ console.log(`
755
+ ${pc4.cyan(b.name)}`);
756
+ console.log(` ${pc4.dim(b.description)}`);
757
+ console.log(` Storage Variants: ${b.variants.map((v) => pc4.yellow(v)).join(", ")}`);
758
+ });
759
+ console.log();
760
+ }
265
761
  }
266
- let selectedVariant;
267
- let variantMeta = void 0;
268
- if (envConfig.variants && Object.keys(envConfig.variants).length > 0) {
269
- const variantKeys = Object.keys(envConfig.variants);
270
- if (variantKeys.includes("redis") && config.redisEnabled) {
271
- selectedVariant = "redis";
762
+ }
763
+
764
+ // src/commands/mcp.ts
765
+ import path3, { join as join4 } from "path";
766
+ import fs4 from "fs/promises";
767
+ import pc5 from "picocolors";
768
+ import { outro as outro5, spinner as spinner4, select as select3, confirm as confirm3 } from "@clack/prompts";
769
+ var CLIENT_MATRIX = {
770
+ claude: {
771
+ label: "Claude Code",
772
+ relativePath: ".mcp.json",
773
+ format: "json"
774
+ },
775
+ codex: {
776
+ label: "Codex CLI",
777
+ relativePath: ".codex/config.toml",
778
+ format: "toml"
779
+ },
780
+ cursor: {
781
+ label: "Cursor",
782
+ relativePath: ".cursor/mcp.json",
783
+ format: "json"
784
+ },
785
+ vscode: {
786
+ label: "VS Code",
787
+ relativePath: ".vscode/settings.json",
788
+ format: "json",
789
+ isCustomSchema: true
790
+ },
791
+ windsurf: {
792
+ label: "Windsurf",
793
+ relativePath: ".windsurf/mcp.json",
794
+ format: "json"
795
+ }
796
+ };
797
+ var BLOCKEND_SERVER = {
798
+ command: "npx",
799
+ args: ["-y", "blockend-cli", "mcp"]
800
+ };
801
+ function generateToml(server) {
802
+ const args = server.args.map((arg) => `"${arg}"`).join(", ");
803
+ return `
804
+ [mcp_servers.blockend]
805
+ command = "${server.command}"
806
+ args = [${args}]
807
+ `.trimStart();
808
+ }
809
+ async function mcpStartCommand() {
810
+ const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
811
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
812
+ const { z } = await import("zod");
813
+ const { execa } = await import("execa");
814
+ const server = new McpServer({
815
+ name: "blockend",
816
+ version: "0.1.0",
817
+ description: "Blockend MCP server \u2014 install production-ready backend blocks into any project"
818
+ });
819
+ async function safeExec(command, args, options) {
820
+ try {
821
+ const { stdout } = await execa(command, args, options);
822
+ return { text: String(stdout), isError: false };
823
+ } catch (error) {
824
+ const err = error;
825
+ const rawError = err.stderr || err.stdout || err.message || "Unknown execution error";
826
+ return { text: `Error executing command: ${String(rawError)}`, isError: true };
827
+ }
828
+ }
829
+ server.tool(
830
+ "list_blocks",
831
+ "Lists all available Blockend blocks with names, descriptions, and tags",
832
+ {},
833
+ async () => {
834
+ const result = await safeExec("npx", ["--no-install", "blockend", "list", "--json"]);
835
+ return { content: [{ type: "text", text: result.text }], isError: result.isError };
836
+ }
837
+ );
838
+ server.tool(
839
+ "add_block",
840
+ "Installs a Blockend block into a project.",
841
+ {
842
+ blockName: z.string().describe("Block name e.g. rate-limit, error-handler, logger"),
843
+ projectPath: z.string().describe("Absolute path to the project root where blockend should install")
844
+ },
845
+ async ({ blockName, projectPath }) => {
846
+ const result = await safeExec(
847
+ "npx",
848
+ ["--no-install", "blockend", "add", blockName, "--yes", "--json"],
849
+ { cwd: projectPath }
850
+ );
851
+ return { content: [{ type: "text", text: result.text }], isError: result.isError };
852
+ }
853
+ );
854
+ server.tool(
855
+ "detect_project",
856
+ "Analyzes a project context.",
857
+ {
858
+ projectPath: z.string().describe("Absolute path to the project root")
859
+ },
860
+ async ({ projectPath }) => {
861
+ const result = await safeExec("npx", ["--no-install", "blockend", "detect", "--json"], {
862
+ cwd: projectPath
863
+ });
864
+ return { content: [{ type: "text", text: result.text }], isError: result.isError };
865
+ }
866
+ );
867
+ const transport = new StdioServerTransport();
868
+ await server.connect(transport);
869
+ }
870
+ async function mcpInitCommand(options) {
871
+ const cwd = process.cwd();
872
+ let chosenClient = options.client;
873
+ if (!chosenClient) {
874
+ if (options.yes) {
875
+ chosenClient = "claude";
272
876
  } else {
273
- const selectVariantPrompt = await select2({
274
- message: "Which architectural storage variant do you want to back this block?",
275
- options: variantKeys.map((vKey) => ({
276
- value: vKey,
277
- label: vKey.toUpperCase()
877
+ const selection = await select3({
878
+ message: "Select target AI client layout profile:",
879
+ options: Object.entries(CLIENT_MATRIX).map(([key, value]) => ({
880
+ value: key,
881
+ label: value.label
278
882
  }))
279
883
  });
280
- handleCancel(selectVariantPrompt);
281
- selectedVariant = selectVariantPrompt;
884
+ if (typeof selection === "symbol") return;
885
+ chosenClient = selection;
282
886
  }
283
- variantMeta = envConfig.variants[selectedVariant];
284
887
  }
285
- const packageJsonPath = await findUp("package.json", rootDir);
286
- if (!packageJsonPath) {
287
- outro2(pc2.red("\u2716 Could not locate package.json in your current directory layout hierarchy."));
888
+ const meta = CLIENT_MATRIX[chosenClient];
889
+ if (!meta) {
890
+ outro5(pc5.red(`\u2716 Unsupported client identifier profile: ${options.client}`));
288
891
  return;
289
892
  }
290
- const packageJsonDir = dirname(packageJsonPath);
291
- let packageJson;
893
+ const absoluteConfigTarget = join4(cwd, meta.relativePath);
894
+ if (options.dryRun) {
895
+ console.log(
896
+ pc5.cyan(`
897
+ \u2139 Dry-run mode active. Intended path location: ${pc5.dim(meta.relativePath)}`)
898
+ );
899
+ if (meta.format === "json") {
900
+ const jsonConfig = meta.isCustomSchema ? { "mcp.mcpServers": { blockend: BLOCKEND_SERVER } } : { mcpServers: { blockend: BLOCKEND_SERVER } };
901
+ console.log(pc5.gray(JSON.stringify(jsonConfig, null, 2)));
902
+ } else {
903
+ console.log(pc5.gray(generateToml(BLOCKEND_SERVER)));
904
+ }
905
+ outro5(pc5.green("\u2714 Dry run evaluation complete."));
906
+ return;
907
+ }
908
+ let fileConflict = false;
292
909
  try {
293
- packageJson = JSON.parse(await fs2.readFile(packageJsonPath, "utf-8"));
910
+ await fs4.access(absoluteConfigTarget);
911
+ fileConflict = true;
294
912
  } catch {
295
- outro2(pc2.red("\u2716 Failed parsing file data configurations from target package.json location."));
296
- return;
297
913
  }
298
- const installedDeps = {
299
- ...packageJson.dependencies ?? {},
300
- ...packageJson.devDependencies ?? {}
301
- };
302
- const allRequiredDeps = [...envConfig.dependencies ?? [], ...variantMeta?.dependencies ?? []];
303
- const missingDeps = allRequiredDeps.filter((dep) => !(dep in installedDeps));
304
- if (missingDeps.length > 0) {
305
- console.log(
306
- pc2.yellow(`
307
- \u26A0\uFE0F Missing required infrastructure packages: ${missingDeps.join(", ")}`)
308
- );
309
- const shouldInstallPrompt = await confirm2({
310
- message: "Would you like the CLI to automatically install these dependencies?",
311
- initialValue: true
914
+ if (fileConflict && !options.force && !options.yes) {
915
+ const shouldOverwrite = await confirm3({
916
+ message: `\u26A0 Local configuration at [${meta.relativePath}] already exists. Overwrite?`,
917
+ initialValue: false
312
918
  });
313
- handleCancel(shouldInstallPrompt);
314
- if (shouldInstallPrompt) {
315
- const packageManager = await fs2.access(join2(packageJsonDir, "pnpm-lock.yaml")).then(() => "pnpm").catch(() => "npm");
316
- s.start(`Preparing native workspace via ${packageManager}...`);
317
- try {
318
- const installCmd = packageManager === "pnpm" ? `pnpm add ${missingDeps.join(" ")}` : `npm install ${missingDeps.join(" ")}`;
319
- s.stop(pc2.cyan(`Executing: ${installCmd}
320
- `));
321
- await new Promise((resolve, reject) => {
322
- const child = exec(installCmd, { cwd: packageJsonDir });
323
- child.stdout?.on("data", (data) => {
324
- process.stdout.write(pc2.dim(data));
325
- });
326
- child.stderr?.on("data", (data) => {
327
- process.stdout.write(pc2.dim(pc2.red(data)));
328
- });
329
- child.on("close", (code) => {
330
- if (code === 0) resolve();
331
- else reject(new Error(`Exit code ${code}`));
332
- });
333
- });
334
- console.log("");
335
- s.start(pc2.green("\u2714 Dependencies installed cleanly. Continuing components build..."));
336
- s.stop();
337
- } catch {
338
- s.stop(pc2.red("\u2716 Automated dependency installation failed. Please run setup manually."));
339
- }
919
+ if (!shouldOverwrite || typeof shouldOverwrite === "symbol") {
920
+ outro5(pc5.yellow("\u2139 Operations halted. Project layout revisions preserved."));
921
+ return;
340
922
  }
341
923
  }
342
- s.start(`Downloading clean production template block [${targetBlock}]...`);
924
+ const s = spinner4();
925
+ s.start(`Writing localized project configurations inside ${pc5.dim(meta.relativePath)}...`);
343
926
  try {
344
- const remoteFilesToDownload = [];
345
- if (envConfig.core) {
346
- remoteFilesToDownload.push(envConfig.core);
347
- }
348
- if (variantMeta && Array.isArray(variantMeta.files)) {
349
- remoteFilesToDownload.push(...variantMeta.files);
350
- }
351
- let physicalPath = config.paths.blocks;
352
- if (physicalPath.startsWith("@")) {
353
- physicalPath = "./src/blocks";
354
- }
355
- let targetFolder = path2.resolve(rootDir, physicalPath);
356
- if (variantMeta && selectedVariant) {
357
- targetFolder = join2(targetFolder, targetBlock);
358
- }
359
- let fileExistsConflict = false;
360
- for (const remoteFile of remoteFilesToDownload) {
361
- const parsedFilename = path2.basename(remoteFile, ".txt");
362
- const checkFileLocation = join2(targetFolder, parsedFilename);
363
- try {
364
- await fs2.access(checkFileLocation);
365
- fileExistsConflict = true;
366
- break;
367
- } catch {
927
+ await fs4.mkdir(path3.dirname(absoluteConfigTarget), { recursive: true });
928
+ if (meta.format === "json") {
929
+ let workingConfig = {};
930
+ if (fileConflict) {
931
+ try {
932
+ const rawContent = await fs4.readFile(absoluteConfigTarget, "utf-8");
933
+ workingConfig = JSON.parse(rawContent);
934
+ } catch {
935
+ workingConfig = {};
936
+ }
368
937
  }
369
- }
370
- if (fileExistsConflict) {
371
- s.stop();
372
- const overwritePrompt = await confirm2({
373
- message: `\u26A0 Components in [${targetBlock}] already exist. Overwrite custom revisions?`,
374
- initialValue: false
375
- });
376
- handleCancel(overwritePrompt);
377
- if (!overwritePrompt) {
378
- outro2(pc2.yellow("\u2139 Operation aborted safely. Local code modifications preserved."));
379
- return;
938
+ if (meta.isCustomSchema) {
939
+ const existingServers = workingConfig["mcp.mcpServers"] || {};
940
+ workingConfig["mcp.mcpServers"] = { ...existingServers, blockend: BLOCKEND_SERVER };
941
+ } else {
942
+ const existingServers = workingConfig.mcpServers || {};
943
+ workingConfig.mcpServers = { ...existingServers, blockend: BLOCKEND_SERVER };
380
944
  }
381
- s.start(`Re-downloading template block files [${targetBlock}]...`);
945
+ await fs4.writeFile(absoluteConfigTarget, JSON.stringify(workingConfig, null, 2), "utf-8");
946
+ } else {
947
+ await fs4.writeFile(absoluteConfigTarget, generateToml(BLOCKEND_SERVER), "utf-8");
382
948
  }
383
- await fs2.mkdir(targetFolder, { recursive: true });
384
- for (const remoteFilePath of remoteFilesToDownload) {
385
- const fileUrl = `${RAW_CDN_BASE}/${remoteFilePath}`;
386
- const response = await fetch(fileUrl);
387
- if (!response.ok) {
388
- throw new Error(`Failed downloading file: ${remoteFilePath} (${response.statusText})`);
389
- }
390
- const rawCodeTemplate = await response.text();
391
- const targetFilename = path2.basename(remoteFilePath, ".txt");
392
- const localWriteLocation = join2(targetFolder, targetFilename);
393
- await fs2.writeFile(localWriteLocation, rawCodeTemplate, "utf-8");
394
- }
395
- const cleanDisplayPath = variantMeta && selectedVariant ? `${physicalPath.replace(/\\/g, "/")}/${targetBlock}` : physicalPath.replace(/\\/g, "/");
396
- s.stop(pc2.green("\u2714 Component isolation structures written smoothly."));
397
- outro2(
398
- pc2.cyan(
399
- `\u2728 Source blocks written to ${cleanDisplayPath}/ layout. Code ownership transferred!`
400
- )
949
+ s.stop(
950
+ pc5.green(`\u2714 Local workspace integration fully complete! File target: ${meta.relativePath}`)
401
951
  );
402
- process.exit(0);
403
952
  } catch (error) {
404
- s.stop(pc2.red("\u2716 Fatal crash occurred while downloading or transferring file layouts."));
405
- console.error(error);
406
- process.exit(1);
953
+ s.stop(pc5.red("\u2716 Fatal crash reading or creating local configuration maps."));
954
+ console.error(pc5.dim(String(error)));
407
955
  }
408
956
  }
409
957
 
410
958
  // src/index.ts
411
- var program = new Command();
412
- program.name("blockend").description("Zero-dependency production backend blocks CLI").version("0.1.0");
413
- program.command("init").description("Initialize blockend configuration in your project").action(async () => await initCommand());
414
- program.command("add [block]").description("Inject unstyled, clean backend blocks directly into code paths").action(async (block) => {
415
- await addCommand(block);
959
+ var mcpInit = defineCommand({
960
+ meta: {
961
+ name: "init",
962
+ description: "Generate appropriate project-local MCP infrastructure configurations"
963
+ },
964
+ args: {
965
+ client: {
966
+ type: "string",
967
+ description: "Target specific integration client profiles: claude | codex | cursor | vscode | windsurf"
968
+ },
969
+ force: {
970
+ type: "boolean",
971
+ default: false,
972
+ description: "Force override existing asset layouts"
973
+ },
974
+ "dry-run": {
975
+ type: "boolean",
976
+ default: false,
977
+ description: "Output planned workspace updates without disk mutation logs"
978
+ },
979
+ yes: { type: "boolean", default: false, description: "Bypass verification prompt iterations" }
980
+ },
981
+ async run({ args }) {
982
+ await mcpInitCommand({
983
+ client: args.client,
984
+ force: args.force,
985
+ dryRun: args["dry-run"],
986
+ yes: args.yes
987
+ });
988
+ }
989
+ });
990
+ var mcp = defineCommand({
991
+ meta: {
992
+ name: "mcp",
993
+ description: "Connect to context protocol channels or configure localized project setups"
994
+ },
995
+ subCommands: {
996
+ init: mcpInit
997
+ },
998
+ async run({ rawArgs }) {
999
+ if (rawArgs.length === 0) {
1000
+ await mcpStartCommand();
1001
+ }
1002
+ }
1003
+ });
1004
+ var init = defineCommand({
1005
+ meta: {
1006
+ name: "init",
1007
+ description: "Initialize Blockend configuration profile (blockend.json)"
1008
+ },
1009
+ args: {
1010
+ yes: {
1011
+ type: "boolean",
1012
+ alias: "y",
1013
+ default: false,
1014
+ description: "Skip initialization questions and auto-enforce layout defaults"
1015
+ },
1016
+ json: {
1017
+ type: "boolean",
1018
+ default: false,
1019
+ description: "Output machine-readable configuration write states"
1020
+ }
1021
+ },
1022
+ async run({ args }) {
1023
+ await initCommand({ yes: args.yes, json: args.json });
1024
+ }
1025
+ });
1026
+ var list = defineCommand({
1027
+ meta: {
1028
+ name: "list",
1029
+ description: "List available component blocks matching local project runtime environment context"
1030
+ },
1031
+ args: {
1032
+ json: {
1033
+ type: "boolean",
1034
+ default: false,
1035
+ description: "Output machine-readable blocks configuration array stream"
1036
+ }
1037
+ },
1038
+ async run({ args }) {
1039
+ await listCommand({ json: args.json });
1040
+ }
1041
+ });
1042
+ var add = defineCommand({
1043
+ meta: {
1044
+ name: "add",
1045
+ description: "Inject a production-grade component block straight into your codebase"
1046
+ },
1047
+ args: {
1048
+ block: {
1049
+ type: "positional",
1050
+ required: false,
1051
+ description: "Target block key name from the remote repository registry"
1052
+ },
1053
+ yes: {
1054
+ type: "boolean",
1055
+ alias: "y",
1056
+ default: false,
1057
+ description: "Skip structural modification confirmations and force dependency downloads"
1058
+ },
1059
+ json: {
1060
+ type: "boolean",
1061
+ default: false,
1062
+ description: "Output streaming machine-readable JSON payloads for automation systems"
1063
+ }
1064
+ },
1065
+ async run({ args }) {
1066
+ await addCommand(args.block, { yes: args.yes, json: args.json });
1067
+ }
1068
+ });
1069
+ var detect = defineCommand({
1070
+ meta: {
1071
+ name: "detect",
1072
+ description: "Scan runtime directory frameworks, engines, and workspaces architectures"
1073
+ },
1074
+ args: {
1075
+ json: {
1076
+ type: "boolean",
1077
+ default: false,
1078
+ description: "Output full detected project workspace context delta maps directly as JSON"
1079
+ }
1080
+ },
1081
+ async run({ args }) {
1082
+ await detectCommand({ json: args.json });
1083
+ }
1084
+ });
1085
+ var main = defineCommand({
1086
+ meta: {
1087
+ name: "blockend",
1088
+ version: "0.1.0",
1089
+ description: "Blockend CLI - Core architectural blocks straight into your repository layout primitives"
1090
+ },
1091
+ subCommands: { init, add, detect, list, mcp }
416
1092
  });
417
- program.parse(process.argv);
1093
+ runMain(main);