blockend-cli 1.3.1 → 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 +228 -154
  2. package/package.json +5 -2
package/dist/index.js CHANGED
@@ -46,18 +46,35 @@ var format = {
46
46
  };
47
47
 
48
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
+ }
49
66
  async function initCommand() {
50
67
  const cwd = process.cwd();
51
68
  const configPath = join(cwd, "blockend.json");
52
69
  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
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
57
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
58
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
59
76
  `);
60
- intro(format.title("Blockend \xB7 Intelligent Backend Blocks Setup"));
77
+ intro(theme.brand.primary(" Blockend \xB7 Intelligent Backend Blocks Setup "));
61
78
  if (existsSync(configPath)) {
62
79
  const action = await select({
63
80
  message: "blockend.json already exists. What do you want to do?",
@@ -77,53 +94,78 @@ async function initCommand() {
77
94
  }
78
95
  }
79
96
  const s = spinner();
80
- s.start("Scanning project structure...");
97
+ s.start("Scanning project layout...");
81
98
  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;
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;
107
122
  }
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`
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
+ }
114
131
  });
115
- if (isCancel(blockAliasInput)) {
132
+ if (isCancel(directoryPrompt)) {
116
133
  outro(format.muted("Initialization cancelled."));
117
134
  return;
118
135
  }
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}`;
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
+ }
127
169
  let includeRedis = false;
128
170
  if (context.hasRedis) {
129
171
  const redisConfirm = await confirm({
@@ -136,11 +178,11 @@ async function initCommand() {
136
178
  }
137
179
  const configPayload = {
138
180
  $schema: "https://blockend.dev/schema.json",
139
- environment: framework,
140
- language: "typescript",
181
+ environment: framework || "express",
182
+ language: context.language || "typescript",
141
183
  includeRedis,
142
184
  aliases: {
143
- blocks: blockAlias
185
+ blocks: finalBlockAlias
144
186
  },
145
187
  paths: {
146
188
  blocks: finalPath
@@ -151,7 +193,9 @@ async function initCommand() {
151
193
  try {
152
194
  await fs.writeFile(configPath, JSON.stringify(configPayload, null, 2), "utf-8");
153
195
  writeSpinner.stop(format.success("blockend.json ready"));
154
- 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
+ );
155
199
  } catch {
156
200
  writeSpinner.stop(format.error("Failed to write configuration"));
157
201
  }
@@ -202,17 +246,16 @@ async function addCommand(blockName) {
202
246
  try {
203
247
  const configFile = await fs2.readFile(configPath, "utf-8");
204
248
  config = JSON.parse(configFile);
205
- } catch {
249
+ } catch (error) {
206
250
  outro2(pc2.red("\u2716 Failed to parse blockend.json layout configuration."));
251
+ console.error(pc2.dim(String(error)));
207
252
  return;
208
253
  }
209
254
  if (config.language !== "typescript") {
210
255
  outro2(
211
256
  pc2.yellow(
212
257
  `
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.`
258
+ \u2139 Blockend forces modern architectural standards. Registry exclusively supports TypeScript.`
216
259
  )
217
260
  );
218
261
  return;
@@ -225,16 +268,28 @@ async function addCommand(blockName) {
225
268
  if (!response.ok) throw new Error(`HTTP Error Status: ${response.status}`);
226
269
  registry = await response.json();
227
270
  s.stop(pc2.green("\u2714 Remote manifest synchronization complete."));
228
- } catch {
271
+ } catch (error) {
229
272
  s.stop(pc2.red("\u2716 Failed to fetch the component registry from GitHub network paths."));
273
+ console.error(pc2.dim(String(error)));
230
274
  return;
231
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
+ }
232
282
  const envKey = String(config.environment);
233
283
  let targetBlock = blockName;
234
284
  if (!targetBlock) {
235
- 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]) => ({
236
291
  value: key,
237
- label: `${key} - ${pc2.dim(registry[key].description)}`
292
+ label: `${key} - ${pc2.dim(block.description)}`
238
293
  }));
239
294
  if (availableOptions.length === 0) {
240
295
  outro2(
@@ -251,37 +306,45 @@ async function addCommand(blockName) {
251
306
  handleCancel(selectBlockPrompt);
252
307
  targetBlock = selectBlockPrompt;
253
308
  }
254
- const blockMeta = registry[targetBlock];
309
+ const blockMeta = blockMap[targetBlock];
255
310
  if (!blockMeta) {
256
311
  outro2(pc2.red(`\u2716 Block "${targetBlock}" does not exist in the remote registry.`));
257
312
  return;
258
313
  }
259
- const envConfig = blockMeta.environments[envKey];
260
- if (!envConfig) {
314
+ const adapterContext = blockMeta.adapters?.[envKey] ?? blockMeta.environments?.[envKey];
315
+ if (!adapterContext) {
261
316
  outro2(
262
317
  pc2.red(`\u2716 The block "${targetBlock}" does not support your environment layout: ${envKey}`)
263
318
  );
264
319
  return;
265
320
  }
266
321
  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";
272
- } 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()
278
- }))
279
- });
280
- handleCancel(selectVariantPrompt);
281
- selectedVariant = selectVariantPrompt;
282
- }
283
- 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";
284
346
  }
347
+ const targetFolder = path2.resolve(rootDir, physicalPath, targetBlock);
285
348
  const packageJsonPath = await findUp("package.json", rootDir);
286
349
  if (!packageJsonPath) {
287
350
  outro2(pc2.red("\u2716 Could not locate package.json in your current directory layout hierarchy."));
@@ -290,21 +353,25 @@ async function addCommand(blockName) {
290
353
  const packageJsonDir = dirname(packageJsonPath);
291
354
  let packageJson;
292
355
  try {
293
- packageJson = JSON.parse(await fs2.readFile(packageJsonPath, "utf-8"));
294
- } catch {
356
+ const packageJsonContent = await fs2.readFile(packageJsonPath, "utf-8");
357
+ packageJson = JSON.parse(packageJsonContent);
358
+ } catch (error) {
295
359
  outro2(pc2.red("\u2716 Failed parsing file data configurations from target package.json location."));
360
+ console.error(pc2.dim(String(error)));
296
361
  return;
297
362
  }
298
363
  const installedDeps = {
299
364
  ...packageJson.dependencies ?? {},
300
365
  ...packageJson.devDependencies ?? {}
301
366
  };
302
- const allRequiredDeps = [...envConfig.dependencies ?? [], ...variantMeta?.dependencies ?? []];
303
- const missingDeps = allRequiredDeps.filter((dep) => !(dep in installedDeps));
304
- 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];
305
372
  console.log(
306
373
  pc2.yellow(`
307
- \u26A0\uFE0F Missing required infrastructure packages: ${missingDeps.join(", ")}`)
374
+ \u26A0\uFE0F Missing required infrastructure packages: ${allMissingNames.join(", ")}`)
308
375
  );
309
376
  const shouldInstallPrompt = await confirm2({
310
377
  message: "Would you like the CLI to automatically install these dependencies?",
@@ -315,95 +382,102 @@ async function addCommand(blockName) {
315
382
  const packageManager = await fs2.access(join2(packageJsonDir, "pnpm-lock.yaml")).then(() => "pnpm").catch(() => "npm");
316
383
  s.start(`Preparing native workspace via ${packageManager}...`);
317
384
  try {
318
- const installCmd = packageManager === "pnpm" ? `pnpm add ${missingDeps.join(" ")}` : `npm install ${missingDeps.join(" ")}`;
319
- 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}
320
398
  `));
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)));
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
+ );
328
407
  });
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..."));
408
+ }
409
+ s.start(pc2.green("\u2714 All dependencies synchronized successfully."));
336
410
  s.stop();
337
- } catch {
411
+ } catch (error) {
338
412
  s.stop(pc2.red("\u2716 Automated dependency installation failed. Please run setup manually."));
413
+ console.error(pc2.dim(String(error)));
414
+ return;
339
415
  }
340
416
  }
341
417
  }
342
- s.start(`Downloading clean production template block [${targetBlock}]...`);
343
- 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 {
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);
368
437
  }
369
438
  }
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;
380
- }
381
- s.start(`Re-downloading template block files [${targetBlock}]...`);
439
+ }
440
+ let fileExistsConflict = false;
441
+ for (const fileMap of filesToDownload) {
442
+ const destinationPath = join2(targetFolder, fileMap.target);
443
+ try {
444
+ await fs2.access(destinationPath);
445
+ fileExistsConflict = true;
446
+ break;
447
+ } catch {
382
448
  }
383
- await fs2.mkdir(targetFolder, { recursive: true });
384
- for (const remoteFilePath of remoteFilesToDownload) {
385
- const fileUrl = `${RAW_CDN_BASE}/${remoteFilePath}`;
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}`;
386
465
  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");
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");
394
471
  }
395
- const cleanDisplayPath = variantMeta && selectedVariant ? `${physicalPath.replace(/\\/g, "/")}/${targetBlock}` : physicalPath.replace(/\\/g, "/");
396
472
  s.stop(pc2.green("\u2714 Component isolation structures written smoothly."));
397
473
  outro2(
398
474
  pc2.cyan(
399
- `\u2728 Source blocks written to ${cleanDisplayPath}/ layout. Code ownership transferred!`
475
+ `\u2728 Source blocks written to ${physicalPath}/${targetBlock}/. Code ownership transferred!`
400
476
  )
401
477
  );
402
- process.exit(0);
403
478
  } 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);
479
+ s.stop(pc2.red("\u2716 Fatal error occurred while assembling file mappings."));
480
+ console.error(pc2.dim(String(error)));
407
481
  }
408
482
  }
409
483
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blockend-cli",
3
- "version": "1.3.1",
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
  },
@@ -33,6 +35,7 @@
33
35
  },
34
36
  "devDependencies": {
35
37
  "@types/node": "^25.9.3",
36
- "tsup": "^8.5.1"
38
+ "tsup": "^8.5.1",
39
+ "vitest": "^1.6.1"
37
40
  }
38
41
  }