blockend-cli 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +236 -249
  2. package/package.json +7 -2
package/dist/index.js CHANGED
@@ -4,103 +4,11 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
- import path, { join as join6 } from "path";
7
+ import path, { join } from "path";
8
8
  import fs from "fs/promises";
9
- import { existsSync as existsSync4 } from "fs";
10
- import { intro, outro, select, text, confirm, spinner, isCancel } from "@clack/prompts";
11
-
12
- // ../detector/dist/index.js
13
- import { join as join5 } from "path";
14
- import { readFile } from "fs/promises";
15
- import { join } from "path";
16
- import { readFile as readFile2 } from "fs/promises";
17
9
  import { existsSync } from "fs";
18
- import { join as join2 } from "path";
19
- import { existsSync as existsSync2 } from "fs";
20
- import { join as join3 } from "path";
21
- import { existsSync as existsSync3 } from "fs";
22
- import { join as join4 } from "path";
23
- async function readPackageJson(cwd) {
24
- const pkgPath = join(cwd, "package.json");
25
- try {
26
- const content = await readFile(pkgPath, "utf-8");
27
- return JSON.parse(content);
28
- } catch {
29
- throw new Error(`No package.json found at ${pkgPath}. Run blockend from your project root.`);
30
- }
31
- }
32
- async function readTsConfig(cwd) {
33
- const tsconfigPath = join2(cwd, "tsconfig.json");
34
- if (!existsSync(tsconfigPath)) return null;
35
- try {
36
- const content = await readFile2(tsconfigPath, "utf-8");
37
- const cleanLines = content.split(/\r?\n/).filter((line) => {
38
- const trimmed = line.trim();
39
- return !trimmed.startsWith("//") && !trimmed.startsWith("/*");
40
- }).join("\n").replace(/,(\s*[}\]])/g, "$1");
41
- return JSON.parse(cleanLines);
42
- } catch {
43
- return null;
44
- }
45
- }
46
- async function inferSrcDir(cwd) {
47
- const candidates = ["src", "app", "lib"];
48
- for (const dir of candidates) {
49
- if (existsSync2(join3(cwd, dir))) {
50
- return join3(cwd, dir);
51
- }
52
- }
53
- return cwd;
54
- }
55
- function detectFramework(deps) {
56
- if ("fastify" in deps) return "fastify";
57
- if ("hono" in deps) return "hono";
58
- if ("express" in deps) return "express";
59
- if ("next" in deps) return "next";
60
- return "none";
61
- }
62
- function detectRuntime(deps) {
63
- if ("@types/bun" in deps || "bun-types" in deps) return "bun";
64
- return "node";
65
- }
66
- function detectPackageManager(cwd) {
67
- if (existsSync3(join4(cwd, "pnpm-lock.yaml"))) return "pnpm";
68
- if (existsSync3(join4(cwd, "bun.lockb"))) return "bun";
69
- if (existsSync3(join4(cwd, "yarn.lock"))) return "yarn";
70
- return "npm";
71
- }
72
- async function detectProject(cwd) {
73
- const [pkg, tsConfig] = await Promise.all([readPackageJson(cwd), readTsConfig(cwd)]);
74
- const allDeps = {
75
- ...pkg.dependencies,
76
- ...pkg.devDependencies,
77
- ...pkg.peerDependencies
78
- };
79
- const srcDir = await inferSrcDir(cwd);
80
- return {
81
- root: cwd,
82
- language: tsConfig !== null ? "typescript" : "javascript",
83
- runtime: detectRuntime(allDeps),
84
- framework: detectFramework(allDeps),
85
- packageManager: detectPackageManager(cwd),
86
- hasRedis: "ioredis" in allDeps || "redis" in allDeps,
87
- hasPrisma: "@prisma/client" in allDeps,
88
- hasDrizzle: "drizzle-orm" in allDeps,
89
- aliasMap: tsConfig?.compilerOptions?.paths ? flattenTsPaths(tsConfig.compilerOptions.paths) : {},
90
- srcDir,
91
- // Default blocks directory: srcDir/lib/blocks
92
- blocksDir: join5(srcDir, "lib", "blocks")
93
- };
94
- }
95
- function flattenTsPaths(paths) {
96
- const result = {};
97
- for (const [alias, targets] of Object.entries(paths)) {
98
- if (targets[0]) {
99
- result[alias.replace("/*", "/")] = targets[0].replace("/*", "/");
100
- }
101
- }
102
- return result;
103
- }
10
+ import { intro, outro, select, text, confirm, spinner, isCancel } from "@clack/prompts";
11
+ import { detectProject } from "@blockend/detector";
104
12
 
105
13
  // src/ui/theme.ts
106
14
  import pc from "picocolors";
@@ -138,19 +46,36 @@ var format = {
138
46
  };
139
47
 
140
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
+ }
63
+ }
64
+ return { baseUrl: ".", paths: {} };
65
+ }
141
66
  async function initCommand() {
142
67
  const cwd = process.cwd();
143
- const configPath = join6(cwd, "blockend.json");
68
+ const configPath = join(cwd, "blockend.json");
144
69
  console.log(`
145
- \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
146
- \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
147
- \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
148
- \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
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
149
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
150
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
151
76
  `);
152
- intro(format.title("Blockend \xB7 Intelligent Backend Blocks Setup"));
153
- if (existsSync4(configPath)) {
77
+ intro(theme.brand.primary(" Blockend \xB7 Intelligent Backend Blocks Setup "));
78
+ if (existsSync(configPath)) {
154
79
  const action = await select({
155
80
  message: "blockend.json already exists. What do you want to do?",
156
81
  options: [
@@ -169,53 +94,78 @@ async function initCommand() {
169
94
  }
170
95
  }
171
96
  const s = spinner();
172
- s.start("Scanning project structure...");
97
+ s.start("Scanning project layout...");
173
98
  const context = await detectProject(cwd);
174
- s.stop(format.success("Project structure detected"));
175
- s.start("Analyzing framework signals...");
176
- await new Promise((r) => setTimeout(r, 400));
177
- s.stop(format.success(`Detected ${context.framework || "unknown"} environment`));
178
- s.start("Resolving alias mappings...");
179
- await new Promise((r) => setTimeout(r, 300));
180
- s.stop(format.success("Import strategy mapped"));
181
- console.log(
182
- format.muted(
183
- `System: ${context.framework || "unknown"} \xB7 ${context.language || "ts"} \xB7 aliases=${Object.keys(context.aliasMap || {}).length}`
184
- )
185
- );
186
- const framework = await select({
187
- message: "Confirm framework environment",
188
- initialValue: context.framework || "express",
189
- options: [
190
- { value: "express", label: "Express.js" },
191
- { value: "fastify", label: "Fastify" },
192
- { value: "next", label: "Next.js (App Router)" },
193
- { value: "hono", label: "Hono" }
194
- ]
195
- });
196
- if (isCancel(framework)) {
197
- outro(format.muted("Initialization cancelled."));
198
- return;
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
+ );
107
+ } 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;
199
122
  }
200
- const availableAliases = Object.keys(context.aliasMap || {});
201
- const baseAliasToken = availableAliases.length > 0 ? availableAliases[0] : "@/";
202
- const blockAliasInput = await text({
203
- message: "Configure blocks import alias",
204
- initialValue: `${baseAliasToken}blocks`,
205
- placeholder: `${baseAliasToken}blocks`
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
+ }
206
131
  });
207
- if (isCancel(blockAliasInput)) {
132
+ if (isCancel(directoryPrompt)) {
208
133
  outro(format.muted("Initialization cancelled."));
209
134
  return;
210
135
  }
211
- const blockAlias = String(blockAliasInput);
212
- const aliasPrefix = availableAliases.find((a) => blockAlias.startsWith(a)) || "";
213
- const physicalPrefix = aliasPrefix ? context.aliasMap[aliasPrefix] : "./";
214
- const resolvedSubDir = blockAlias.replace(aliasPrefix, "");
215
- const assumedPhysicalDir = join6(physicalPrefix, resolvedSubDir);
216
- const relativeBlocksPath = path.relative(cwd, path.resolve(cwd, assumedPhysicalDir));
217
- const normalizedPath = relativeBlocksPath.replace(/\\/g, "/");
218
- const finalPath = normalizedPath.startsWith(".") ? normalizedPath : `./${normalizedPath}`;
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(/\/$/, "");
159
+ } 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
+ }
219
169
  let includeRedis = false;
220
170
  if (context.hasRedis) {
221
171
  const redisConfirm = await confirm({
@@ -228,11 +178,11 @@ async function initCommand() {
228
178
  }
229
179
  const configPayload = {
230
180
  $schema: "https://blockend.dev/schema.json",
231
- environment: framework,
232
- language: "typescript",
181
+ environment: framework || "express",
182
+ language: context.language || "typescript",
233
183
  includeRedis,
234
184
  aliases: {
235
- blocks: blockAlias
185
+ blocks: finalBlockAlias
236
186
  },
237
187
  paths: {
238
188
  blocks: finalPath
@@ -243,14 +193,16 @@ async function initCommand() {
243
193
  try {
244
194
  await fs.writeFile(configPath, JSON.stringify(configPayload, null, 2), "utf-8");
245
195
  writeSpinner.stop(format.success("blockend.json ready"));
246
- outro(format.title("Blockend initialized successfully. Run: npx blockend add <block>"));
196
+ outro(
197
+ theme.state.success("\u2728 Blockend initialized successfully. Run: npx blockend add <block>")
198
+ );
247
199
  } catch {
248
200
  writeSpinner.stop(format.error("Failed to write configuration"));
249
201
  }
250
202
  }
251
203
 
252
204
  // src/commands/add.ts
253
- import path2, { join as join7, dirname } from "path";
205
+ import path2, { join as join2, dirname } from "path";
254
206
  import fs2 from "fs/promises";
255
207
  import { exec } from "child_process";
256
208
  import { intro as intro2, outro as outro2, select as select2, spinner as spinner2, confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
@@ -269,7 +221,7 @@ function handleCancel(value) {
269
221
  async function findUp(filename, startDir) {
270
222
  let dir = startDir;
271
223
  while (true) {
272
- const checkPath = join7(dir, filename);
224
+ const checkPath = join2(dir, filename);
273
225
  try {
274
226
  await fs2.access(checkPath);
275
227
  return checkPath;
@@ -294,17 +246,16 @@ async function addCommand(blockName) {
294
246
  try {
295
247
  const configFile = await fs2.readFile(configPath, "utf-8");
296
248
  config = JSON.parse(configFile);
297
- } catch {
249
+ } catch (error) {
298
250
  outro2(pc2.red("\u2716 Failed to parse blockend.json layout configuration."));
251
+ console.error(pc2.dim(String(error)));
299
252
  return;
300
253
  }
301
254
  if (config.language !== "typescript") {
302
255
  outro2(
303
256
  pc2.yellow(
304
257
  `
305
- \u2139 Blockend forces modern architectural standards.
306
- To ensure strict type safety and absolute code ownership, the registry exclusively supports TypeScript.
307
- Please migrate your project configuration to TypeScript to ingest blocks.`
258
+ \u2139 Blockend forces modern architectural standards. Registry exclusively supports TypeScript.`
308
259
  )
309
260
  );
310
261
  return;
@@ -317,16 +268,28 @@ async function addCommand(blockName) {
317
268
  if (!response.ok) throw new Error(`HTTP Error Status: ${response.status}`);
318
269
  registry = await response.json();
319
270
  s.stop(pc2.green("\u2714 Remote manifest synchronization complete."));
320
- } catch {
271
+ } catch (error) {
321
272
  s.stop(pc2.red("\u2716 Failed to fetch the component registry from GitHub network paths."));
273
+ console.error(pc2.dim(String(error)));
322
274
  return;
323
275
  }
276
+ let blockMap = {};
277
+ if (registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks)) {
278
+ blockMap = registry.blocks;
279
+ } else {
280
+ blockMap = registry;
281
+ }
324
282
  const envKey = String(config.environment);
325
283
  let targetBlock = blockName;
326
284
  if (!targetBlock) {
327
- const availableOptions = Object.keys(registry).filter((key) => registry[key].environments[envKey] !== void 0).map((key) => ({
285
+ const availableOptions = Object.entries(blockMap).filter(([_, block]) => {
286
+ if (!block) return false;
287
+ const hasAdapter = block.adapters?.[envKey] !== void 0;
288
+ const hasEnvironment = block.environments?.[envKey] !== void 0;
289
+ return hasAdapter || hasEnvironment;
290
+ }).map(([key, block]) => ({
328
291
  value: key,
329
- label: `${key} - ${pc2.dim(registry[key].description)}`
292
+ label: `${key} - ${pc2.dim(block.description)}`
330
293
  }));
331
294
  if (availableOptions.length === 0) {
332
295
  outro2(
@@ -343,37 +306,45 @@ async function addCommand(blockName) {
343
306
  handleCancel(selectBlockPrompt);
344
307
  targetBlock = selectBlockPrompt;
345
308
  }
346
- const blockMeta = registry[targetBlock];
309
+ const blockMeta = blockMap[targetBlock];
347
310
  if (!blockMeta) {
348
311
  outro2(pc2.red(`\u2716 Block "${targetBlock}" does not exist in the remote registry.`));
349
312
  return;
350
313
  }
351
- const envConfig = blockMeta.environments[envKey];
352
- if (!envConfig) {
314
+ const adapterContext = blockMeta.adapters?.[envKey] ?? blockMeta.environments?.[envKey];
315
+ if (!adapterContext) {
353
316
  outro2(
354
317
  pc2.red(`\u2716 The block "${targetBlock}" does not support your environment layout: ${envKey}`)
355
318
  );
356
319
  return;
357
320
  }
358
321
  let selectedVariant;
359
- let variantMeta = void 0;
360
- if (envConfig.variants && Object.keys(envConfig.variants).length > 0) {
361
- const variantKeys = Object.keys(envConfig.variants);
362
- if (variantKeys.includes("redis") && config.redisEnabled) {
363
- selectedVariant = "redis";
364
- } else {
365
- const selectVariantPrompt = await select2({
366
- message: "Which architectural storage variant do you want to back this block?",
367
- options: variantKeys.map((vKey) => ({
368
- value: vKey,
369
- label: vKey.toUpperCase()
370
- }))
371
- });
372
- handleCancel(selectVariantPrompt);
373
- selectedVariant = selectVariantPrompt;
374
- }
375
- variantMeta = envConfig.variants[selectedVariant];
322
+ const variantKeys = Object.keys(adapterContext.variants || {});
323
+ if (variantKeys.length === 0) {
324
+ outro2(pc2.red(`\u2716 No architecture storage layout variants found for block framework: ${envKey}`));
325
+ return;
326
+ }
327
+ if (variantKeys.includes("redis") && config.redisEnabled) {
328
+ selectedVariant = "redis";
329
+ } else if (variantKeys.length === 1) {
330
+ selectedVariant = variantKeys[0];
331
+ } else {
332
+ const selectVariantPrompt = await select2({
333
+ message: "Which architectural storage variant do you want to back this block?",
334
+ options: variantKeys.map((vKey) => ({
335
+ value: vKey,
336
+ label: vKey.toUpperCase()
337
+ }))
338
+ });
339
+ handleCancel(selectVariantPrompt);
340
+ selectedVariant = selectVariantPrompt;
341
+ }
342
+ const variantMeta = adapterContext.variants[selectedVariant];
343
+ let physicalPath = config.paths.blocks;
344
+ if (physicalPath.startsWith("@")) {
345
+ physicalPath = "./src/blocks";
376
346
  }
347
+ const targetFolder = path2.resolve(rootDir, physicalPath, targetBlock);
377
348
  const packageJsonPath = await findUp("package.json", rootDir);
378
349
  if (!packageJsonPath) {
379
350
  outro2(pc2.red("\u2716 Could not locate package.json in your current directory layout hierarchy."));
@@ -382,21 +353,25 @@ async function addCommand(blockName) {
382
353
  const packageJsonDir = dirname(packageJsonPath);
383
354
  let packageJson;
384
355
  try {
385
- packageJson = JSON.parse(await fs2.readFile(packageJsonPath, "utf-8"));
386
- } catch {
356
+ const packageJsonContent = await fs2.readFile(packageJsonPath, "utf-8");
357
+ packageJson = JSON.parse(packageJsonContent);
358
+ } catch (error) {
387
359
  outro2(pc2.red("\u2716 Failed parsing file data configurations from target package.json location."));
360
+ console.error(pc2.dim(String(error)));
388
361
  return;
389
362
  }
390
363
  const installedDeps = {
391
364
  ...packageJson.dependencies ?? {},
392
365
  ...packageJson.devDependencies ?? {}
393
366
  };
394
- const allRequiredDeps = [...envConfig.dependencies ?? [], ...variantMeta?.dependencies ?? []];
395
- const missingDeps = allRequiredDeps.filter((dep) => !(dep in installedDeps));
396
- if (missingDeps.length > 0) {
367
+ const missingProdDeps = (adapterContext.dependencies ?? []).concat(variantMeta?.dependencies ?? []).filter((dep) => !(dep in installedDeps));
368
+ const missingDevDeps = (adapterContext.devDependencies ?? []).concat(variantMeta?.devDependencies ?? []).filter((dep) => !(dep in installedDeps));
369
+ const hasMissingDeps = missingProdDeps.length > 0 || missingDevDeps.length > 0;
370
+ if (hasMissingDeps) {
371
+ const allMissingNames = [...missingProdDeps, ...missingDevDeps];
397
372
  console.log(
398
373
  pc2.yellow(`
399
- \u26A0\uFE0F Missing required infrastructure packages: ${missingDeps.join(", ")}`)
374
+ \u26A0\uFE0F Missing required infrastructure packages: ${allMissingNames.join(", ")}`)
400
375
  );
401
376
  const shouldInstallPrompt = await confirm2({
402
377
  message: "Would you like the CLI to automatically install these dependencies?",
@@ -404,93 +379,105 @@ async function addCommand(blockName) {
404
379
  });
405
380
  handleCancel(shouldInstallPrompt);
406
381
  if (shouldInstallPrompt) {
407
- const packageManager = await fs2.access(join7(packageJsonDir, "pnpm-lock.yaml")).then(() => "pnpm").catch(() => "npm");
382
+ const packageManager = await fs2.access(join2(packageJsonDir, "pnpm-lock.yaml")).then(() => "pnpm").catch(() => "npm");
408
383
  s.start(`Preparing native workspace via ${packageManager}...`);
409
384
  try {
410
- const installCmd = packageManager === "pnpm" ? `pnpm add ${missingDeps.join(" ")}` : `npm install ${missingDeps.join(" ")}`;
411
- s.stop(pc2.cyan(`Executing: ${installCmd}
385
+ const installTasks = [];
386
+ if (missingProdDeps.length > 0) {
387
+ installTasks.push(
388
+ packageManager === "pnpm" ? `pnpm add ${missingProdDeps.join(" ")}` : `npm install ${missingProdDeps.join(" ")}`
389
+ );
390
+ }
391
+ if (missingDevDeps.length > 0) {
392
+ installTasks.push(
393
+ packageManager === "pnpm" ? `pnpm add -D ${missingDevDeps.join(" ")}` : `npm install --save-dev ${missingDevDeps.join(" ")}`
394
+ );
395
+ }
396
+ for (const installCmd of installTasks) {
397
+ s.stop(pc2.cyan(`Executing: ${installCmd}
412
398
  `));
413
- await new Promise((resolve, reject) => {
414
- const child = exec(installCmd, { cwd: packageJsonDir });
415
- child.stdout?.on("data", (data) => {
416
- process.stdout.write(pc2.dim(data));
417
- });
418
- child.stderr?.on("data", (data) => {
419
- process.stdout.write(pc2.dim(pc2.red(data)));
399
+ await new Promise((resolve, reject) => {
400
+ 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))));
403
+ child.on(
404
+ "close",
405
+ (code) => code === 0 ? resolve() : reject(new Error(`Exit code ${code}`))
406
+ );
420
407
  });
421
- child.on("close", (code) => {
422
- if (code === 0) resolve();
423
- else reject(new Error(`Exit code ${code}`));
424
- });
425
- });
426
- console.log("");
427
- s.start(pc2.green("\u2714 Dependencies installed cleanly. Continuing components build..."));
408
+ }
409
+ s.start(pc2.green("\u2714 All dependencies synchronized successfully."));
428
410
  s.stop();
429
- } catch {
411
+ } catch (error) {
430
412
  s.stop(pc2.red("\u2716 Automated dependency installation failed. Please run setup manually."));
413
+ console.error(pc2.dim(String(error)));
414
+ return;
431
415
  }
432
416
  }
433
417
  }
434
- s.start(`Downloading clean production template block [${targetBlock}]...`);
435
- try {
436
- const BASE_URL = RAW_CDN_BASE;
437
- const coreFileUrl = `${BASE_URL}/${envConfig.core}`;
438
- const coreFetchResponse = await fetch(coreFileUrl);
439
- if (!coreFetchResponse.ok) {
440
- throw new Error(`Failed downloading core component file: ${coreFetchResponse.statusText}`);
441
- }
442
- const coreCodeTemplate = await coreFetchResponse.text();
443
- let physicalPath = config.paths.blocks;
444
- if (physicalPath.startsWith("@")) {
445
- physicalPath = "./src/blocks";
446
- }
447
- let targetFolder = path2.resolve(rootDir, physicalPath);
448
- if (variantMeta && selectedVariant) {
449
- targetFolder = join7(targetFolder, targetBlock);
418
+ const filesToDownload = [];
419
+ if (blockMeta.baseFiles) {
420
+ filesToDownload.push(...blockMeta.baseFiles);
421
+ }
422
+ if (adapterContext.core) {
423
+ filesToDownload.push({
424
+ source: adapterContext.core,
425
+ target: path2.basename(adapterContext.core, ".txt")
426
+ });
427
+ }
428
+ if (variantMeta && Array.isArray(variantMeta.files)) {
429
+ for (const fileEntry of variantMeta.files) {
430
+ if (typeof fileEntry === "string") {
431
+ filesToDownload.push({
432
+ source: fileEntry,
433
+ target: path2.basename(fileEntry, ".txt")
434
+ });
435
+ } else {
436
+ filesToDownload.push(fileEntry);
437
+ }
450
438
  }
451
- await fs2.mkdir(targetFolder, { recursive: true });
452
- const coreOutputFilename = `${targetBlock}.ts`;
453
- const coreTargetFileLocation = join7(targetFolder, coreOutputFilename);
439
+ }
440
+ let fileExistsConflict = false;
441
+ for (const fileMap of filesToDownload) {
442
+ const destinationPath = join2(targetFolder, fileMap.target);
454
443
  try {
455
- await fs2.access(coreTargetFileLocation);
456
- s.stop();
457
- const overwritePrompt = await confirm2({
458
- message: `\u26A0 Block component file [${coreOutputFilename}] already exists. Overwrite custom code revisions?`,
459
- initialValue: false
460
- });
461
- handleCancel(overwritePrompt);
462
- if (!overwritePrompt) {
463
- outro2(pc2.yellow("\u2139 Operation aborted safely. Local code modifications preserved."));
464
- return;
465
- }
466
- s.start(`Re-downloading template block [${targetBlock}]...`);
444
+ await fs2.access(destinationPath);
445
+ fileExistsConflict = true;
446
+ break;
467
447
  } catch {
468
448
  }
469
- await fs2.writeFile(coreTargetFileLocation, coreCodeTemplate, "utf-8");
470
- if (variantMeta && selectedVariant) {
471
- const variantFileUrl = `${BASE_URL}/${variantMeta.path}`;
472
- const variantFetchResponse = await fetch(variantFileUrl);
473
- if (!variantFetchResponse.ok) {
474
- throw new Error(
475
- `Failed downloading storage variant file: ${variantFetchResponse.statusText}`
476
- );
477
- }
478
- const variantCodeTemplate = await variantFetchResponse.text();
479
- const variantOutputFilename = `store-${selectedVariant}.ts`;
480
- await fs2.writeFile(join7(targetFolder, variantOutputFilename), variantCodeTemplate, "utf-8");
449
+ }
450
+ if (fileExistsConflict) {
451
+ const overwritePrompt = await confirm2({
452
+ message: `\u26A0 Components in [${targetBlock}] already exist. Overwrite custom revisions?`,
453
+ initialValue: false
454
+ });
455
+ handleCancel(overwritePrompt);
456
+ if (!overwritePrompt) {
457
+ outro2(pc2.yellow("\u2139 Operation aborted safely. Local code modifications preserved."));
458
+ return;
459
+ }
460
+ }
461
+ s.start(`Downloading and building template adapter blocks [${targetBlock}]...`);
462
+ try {
463
+ for (const fileMap of filesToDownload) {
464
+ const fileUrl = `${RAW_CDN_BASE}/${fileMap.source}`;
465
+ const response = await fetch(fileUrl);
466
+ if (!response.ok) throw new Error(`Download failed for: ${fileMap.source}`);
467
+ 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");
481
471
  }
482
- const cleanDisplayPath = variantMeta && selectedVariant ? `${physicalPath.replace(/\\/g, "/")}/${targetBlock}` : physicalPath.replace(/\\/g, "/");
483
472
  s.stop(pc2.green("\u2714 Component isolation structures written smoothly."));
484
473
  outro2(
485
474
  pc2.cyan(
486
- `\u2728 Source blocks written to ${cleanDisplayPath}/ layout. Code ownership transferred!`
475
+ `\u2728 Source blocks written to ${physicalPath}/${targetBlock}/. Code ownership transferred!`
487
476
  )
488
477
  );
489
- process.exit(0);
490
478
  } catch (error) {
491
- s.stop(pc2.red("\u2716 Fatal crash occurred while downloading or transferring file layouts."));
492
- console.error(error);
493
- process.exit(1);
479
+ s.stop(pc2.red("\u2716 Fatal error occurred while assembling file mappings."));
480
+ console.error(pc2.dim(String(error)));
494
481
  }
495
482
  }
496
483
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blockend-cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Intelligent, modular backend blocks right inside your terminal",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -12,6 +12,8 @@
12
12
  "readme.md"
13
13
  ],
14
14
  "scripts": {
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
15
17
  "build": "tsup src/index.ts --format esm --dts --out-dir dist",
16
18
  "typecheck": "tsc --noEmit"
17
19
  },
@@ -26,11 +28,14 @@
26
28
  "license": "ISC",
27
29
  "packageManager": "pnpm@10.33.2",
28
30
  "dependencies": {
31
+ "@blockend/detector": "workspace:*",
29
32
  "@clack/prompts": "^1.5.1",
30
33
  "commander": "^15.0.0",
31
34
  "picocolors": "^1.1.1"
32
35
  },
33
36
  "devDependencies": {
34
- "@types/node": "^25.9.3"
37
+ "@types/node": "^25.9.3",
38
+ "tsup": "^8.5.1",
39
+ "vitest": "^1.6.1"
35
40
  }
36
41
  }