blockend-cli 1.4.0 → 1.4.2

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