blockend-cli 1.4.3 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +203 -233
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,6 +14,29 @@ var REPO_NAME = "blockend";
14
14
  var BRANCH = "master";
15
15
  var RAW_CDN_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}`;
16
16
  var MANIFEST_URL = `${RAW_CDN_BASE}/registry/index.json`;
17
+ function blockSupportsEnv(block, envKey) {
18
+ if (block.frameworks && block.frameworks.length > 0) {
19
+ return block.frameworks.includes("*") || block.frameworks.includes(envKey);
20
+ }
21
+ return block.adapters?.[envKey] !== void 0 || block.environments?.[envKey] !== void 0;
22
+ }
23
+ function resolveAdapterContext(block, envKey) {
24
+ return block.adapters?.[envKey] ?? block.adapters?.["*"] ?? block.environments?.[envKey] ?? block.environments?.["*"] ?? null;
25
+ }
26
+ function rewriteFileImports(fileContent, writtenFilePath, blocksRoot, aliasBase) {
27
+ if (!aliasBase || aliasBase.startsWith(".")) {
28
+ return fileContent;
29
+ }
30
+ const importRegex = /((?:from|import)\s*)(['"`])(\.\.?\/[^'"` \n\r]+)(['"`])/g;
31
+ const fileDir = dirname(writtenFilePath);
32
+ return fileContent.replace(importRegex, (match, keyword, openQuote, relativePath, closeQuote) => {
33
+ const absoluteTarget = path.resolve(fileDir, relativePath);
34
+ const fromBlocksRoot = path.relative(blocksRoot, absoluteTarget).replace(/\\/g, "/");
35
+ if (fromBlocksRoot.startsWith("..")) return match;
36
+ const aliased = `${aliasBase}/${fromBlocksRoot}`.replace(/\/+/g, "/");
37
+ return `${keyword}${openQuote}${aliased}${closeQuote}`;
38
+ });
39
+ }
17
40
  function outputError(json, message) {
18
41
  if (json) {
19
42
  process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
@@ -25,11 +48,7 @@ function outputResult(json, result) {
25
48
  if (json) {
26
49
  process.stdout.write(JSON.stringify(result) + "\n");
27
50
  } else {
28
- if (result.success) {
29
- outro(pc.green(`\u2728 ${result.message}`));
30
- } else {
31
- outro(pc.yellow(`\u2139 ${result.message}`));
32
- }
51
+ outro(result.success ? pc.green(`\u2728 ${result.message}`) : pc.yellow(`\u2139 ${result.message}`));
33
52
  }
34
53
  }
35
54
  function handleCancel(value) {
@@ -41,10 +60,10 @@ function handleCancel(value) {
41
60
  async function findUp(filename, startDir) {
42
61
  let dir = startDir;
43
62
  while (true) {
44
- const checkPath = join(dir, filename);
63
+ const candidate = join(dir, filename);
45
64
  try {
46
- await fs.access(checkPath);
47
- return checkPath;
65
+ await fs.access(candidate);
66
+ return candidate;
48
67
  } catch {
49
68
  const parent = dirname(dir);
50
69
  if (parent === dir) break;
@@ -68,281 +87,236 @@ async function addCommand(blockName, options = {}) {
68
87
  const rootDir = dirname(configPath);
69
88
  let config;
70
89
  try {
71
- const configFile = await fs.readFile(configPath, "utf-8");
72
- config = JSON.parse(configFile);
73
- } catch (error) {
90
+ config = JSON.parse(await fs.readFile(configPath, "utf-8"));
91
+ } catch (err) {
74
92
  outputError(json, "Failed to parse blockend.json.");
75
- if (!json) log.error(pc.dim(String(error)));
93
+ if (!json) log.error(pc.dim(String(err)));
76
94
  return;
77
95
  }
78
96
  if (config.language !== "typescript") {
79
- if (json) {
80
- outputError(json, "Registry exclusively supports TypeScript projects.");
81
- } else {
82
- log.warn("Blockend currently only supports TypeScript projects.");
83
- outro(pc.dim("Exiting."));
84
- }
97
+ outputError(json, "Blockend currently only supports TypeScript projects.");
85
98
  return;
86
99
  }
100
+ const physicalBlocksPath = config.paths.blocks;
101
+ const blocksRootAbsolute = path.resolve(rootDir, physicalBlocksPath);
102
+ const aliasBase = config.aliases.blocks;
103
+ const envKey = config.environment;
87
104
  const s = spinner();
88
- if (!json) {
89
- s.start("Fetching registry...");
90
- }
105
+ if (!json) s.start("Fetching registry...");
91
106
  let registry;
92
107
  try {
93
- const response = await fetch(MANIFEST_URL);
94
- if (!response.ok) throw new Error(`HTTP Error Status: ${response.status}`);
95
- registry = await response.json();
108
+ const res = await fetch(MANIFEST_URL);
109
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
110
+ registry = await res.json();
96
111
  if (!json) s.stop("Registry synced.");
97
- } catch (error) {
112
+ } catch (err) {
98
113
  if (!json) {
99
114
  s.stop("Failed to fetch registry.");
100
- log.error(pc.dim(String(error)));
101
- } else {
102
- outputError(json, "Failed to fetch the component registry from GitHub.");
103
- }
115
+ log.error(pc.dim(String(err)));
116
+ } else outputError(json, "Failed to fetch registry from GitHub.");
104
117
  return;
105
118
  }
106
- let blockMap = {};
107
- if (registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks)) {
108
- blockMap = registry.blocks;
109
- } else {
110
- blockMap = registry;
111
- }
112
- const envKey = String(config.environment);
113
- let targetBlock = blockName;
114
- if (yes && !targetBlock) {
115
- outputError(
116
- json,
117
- "Block name required when using --yes flag. Usage: blockend add <block> --yes"
118
- );
119
+ const blockMap = registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks) ? registry.blocks : registry;
120
+ if (yes && !blockName) {
121
+ outputError(json, "Block name required with --yes. Usage: blockend add <block> --yes");
119
122
  return;
120
123
  }
124
+ let targetBlock = blockName;
121
125
  if (!targetBlock) {
122
- const filteredBlocks = Object.entries(blockMap).filter(([_, block]) => {
123
- if (!block) return false;
124
- const hasAdapter = block.adapters?.[envKey] !== void 0;
125
- const hasEnvironment = block.environments?.[envKey] !== void 0;
126
- return hasAdapter || hasEnvironment;
127
- });
128
- if (filteredBlocks.length === 0) {
129
- const msg = `No backend blocks are currently available for: ${envKey}.`;
130
- if (json) {
131
- outputError(json, msg);
132
- } else {
133
- log.warn(msg);
134
- outro(pc.dim("Exiting."));
135
- }
126
+ const available = Object.entries(blockMap).filter(
127
+ ([, block]) => block != null && blockSupportsEnv(block, envKey)
128
+ );
129
+ if (available.length === 0) {
130
+ outputError(json, `No blocks available for environment: ${envKey}`);
131
+ if (!json) outro(pc.dim("Exiting."));
136
132
  return;
137
133
  }
138
- const maxKeyLength = Math.max(...filteredBlocks.map(([key]) => key.length), 0);
139
- const availableOptions = filteredBlocks.map(([key, block], index) => {
140
- const paddedKey = key.padEnd(maxKeyLength + 4, " ");
141
- const maxDescWidth = 60;
142
- const optimizedDescription = block.description.length > maxDescWidth ? `${block.description.slice(0, maxDescWidth)}...` : block.description;
143
- return {
144
- value: key,
145
- label: `${pc.dim(`${index + 1}.`)} ${pc.bold(pc.cyan(paddedKey))}${pc.dim(optimizedDescription)}`
146
- };
147
- });
148
- const selectBlockPrompt = await select({
134
+ const maxKeyLen = Math.max(...available.map(([k]) => k.length), 0);
135
+ const selectPrompt = await select({
149
136
  message: "Which block would you like to add?",
150
- options: availableOptions
137
+ options: available.map(([key, block], i) => ({
138
+ value: key,
139
+ label: `${pc.dim(`${i + 1}.`)} ${pc.bold(pc.cyan(key.padEnd(maxKeyLen + 4)))}${pc.dim(
140
+ block.description.length > 60 ? `${block.description.slice(0, 60)}...` : block.description
141
+ )}`
142
+ }))
151
143
  });
152
- handleCancel(selectBlockPrompt);
153
- targetBlock = selectBlockPrompt;
144
+ handleCancel(selectPrompt);
145
+ targetBlock = selectPrompt;
154
146
  }
155
147
  const blockMeta = blockMap[targetBlock];
156
148
  if (!blockMeta) {
157
- outputError(json, `Block "${targetBlock}" does not exist in the registry.`);
149
+ outputError(json, `Block "${targetBlock}" not found in the registry.`);
158
150
  return;
159
151
  }
160
- const adapterContext = blockMeta.adapters?.[envKey] ?? blockMeta.environments?.[envKey];
152
+ if (!blockSupportsEnv(blockMeta, envKey)) {
153
+ outputError(json, `Block "${targetBlock}" does not support environment: ${envKey}`);
154
+ return;
155
+ }
156
+ const adapterContext = resolveAdapterContext(blockMeta, envKey);
161
157
  if (!adapterContext) {
162
- outputError(json, `The block "${targetBlock}" does not support your environment: ${envKey}`);
158
+ outputError(
159
+ json,
160
+ `Block "${targetBlock}" has no adapter or environment entry for: ${envKey}. This is a registry configuration issue \u2014 please open an issue.`
161
+ );
163
162
  return;
164
163
  }
165
- let selectedVariant;
166
- const variantKeys = Object.keys(adapterContext.variants || {});
164
+ const variantKeys = Object.keys(adapterContext.variants ?? {});
167
165
  if (variantKeys.length === 0) {
168
- outputError(json, `No storage variants found for block environment: ${envKey}`);
166
+ outputError(json, `No variants found for "${targetBlock}" / "${envKey}".`);
169
167
  return;
170
168
  }
169
+ let selectedVariant;
171
170
  if (yes) {
172
171
  selectedVariant = variantKeys.includes("memory") ? "memory" : variantKeys[0];
173
- } else if (variantKeys.includes("redis") && config.redisEnabled) {
172
+ } else if (variantKeys.includes("redis") && (config.redisEnabled || config.includeRedis)) {
174
173
  selectedVariant = "redis";
175
174
  } else if (variantKeys.length === 1) {
176
175
  selectedVariant = variantKeys[0];
177
176
  } else {
178
- const selectVariantPrompt = await select({
177
+ const variantPrompt = await select({
179
178
  message: "Select a storage variant:",
180
- options: variantKeys.map((vKey) => ({
181
- value: vKey,
182
- label: vKey.toUpperCase()
183
- }))
179
+ options: variantKeys.map((v) => ({ value: v, label: v.toUpperCase() }))
184
180
  });
185
- handleCancel(selectVariantPrompt);
186
- selectedVariant = selectVariantPrompt;
181
+ handleCancel(variantPrompt);
182
+ selectedVariant = variantPrompt;
187
183
  }
188
184
  const variantMeta = adapterContext.variants[selectedVariant];
189
- let physicalPath = config.paths.blocks;
190
- if (physicalPath.startsWith("@")) {
191
- physicalPath = "./src/blocks";
192
- }
193
- const targetFolder = path.resolve(rootDir, physicalPath, targetBlock);
185
+ const targetFolder = path.resolve(blocksRootAbsolute, targetBlock);
194
186
  const packageJsonPath = await findUp("package.json", rootDir);
195
187
  if (!packageJsonPath) {
196
- outputError(json, "Could not locate package.json in your current directory.");
188
+ outputError(json, "Could not locate package.json.");
197
189
  return;
198
190
  }
199
191
  const packageJsonDir = dirname(packageJsonPath);
200
192
  let packageJson;
201
193
  try {
202
- const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
203
- packageJson = JSON.parse(packageJsonContent);
204
- } catch (error) {
205
- outputError(json, "Failed parsing package.json.");
206
- if (!json) log.error(pc.dim(String(error)));
194
+ packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
195
+ } catch (err) {
196
+ outputError(json, "Failed to parse package.json.");
197
+ if (!json) log.error(pc.dim(String(err)));
207
198
  return;
208
199
  }
209
200
  const installedDeps = {
210
201
  ...packageJson.dependencies ?? {},
211
202
  ...packageJson.devDependencies ?? {}
212
203
  };
213
- const missingProdDeps = (adapterContext.dependencies ?? []).concat(variantMeta?.dependencies ?? []).filter((dep) => !(dep in installedDeps));
214
- const missingDevDeps = (adapterContext.devDependencies ?? []).concat(variantMeta?.devDependencies ?? []).filter((dep) => !(dep in installedDeps));
215
- const hasMissingDeps = missingProdDeps.length > 0 || missingDevDeps.length > 0;
216
- if (hasMissingDeps) {
217
- const allMissingNames = [...missingProdDeps, ...missingDevDeps];
218
- if (!json) {
219
- log.warn(`Missing dependencies: ${pc.cyan(allMissingNames.join(", "))}`);
220
- }
221
- const shouldInstallPrompt = yes ? true : await confirm({
222
- message: "Install missing dependencies?",
223
- initialValue: true
224
- });
225
- if (!yes) handleCancel(shouldInstallPrompt);
226
- if (shouldInstallPrompt) {
227
- const packageManager = await fs.access(join(packageJsonDir, "pnpm-lock.yaml")).then(() => "pnpm").catch(() => "npm");
228
- if (!json) s.start(`Installing via ${packageManager}...`);
204
+ const missingProd = [
205
+ ...adapterContext.dependencies ?? [],
206
+ ...variantMeta?.dependencies ?? []
207
+ ].filter((d) => !(d in installedDeps));
208
+ const missingDev = [
209
+ ...adapterContext.devDependencies ?? [],
210
+ ...variantMeta?.devDependencies ?? []
211
+ ].filter((d) => !(d in installedDeps));
212
+ if (missingProd.length > 0 || missingDev.length > 0) {
213
+ if (!json) log.warn(`Missing: ${pc.cyan([...missingProd, ...missingDev].join(", "))}`);
214
+ const shouldInstall = yes ? true : await confirm({ message: "Install missing dependencies?", initialValue: true });
215
+ if (!yes) handleCancel(shouldInstall);
216
+ if (shouldInstall) {
217
+ const pm = await fs.access(join(packageJsonDir, "pnpm-lock.yaml")).then(() => "pnpm").catch(() => "npm");
218
+ if (!json) s.start(`Installing via ${pm}...`);
219
+ const tasks = [];
220
+ if (missingProd.length > 0) {
221
+ tasks.push(
222
+ pm === "pnpm" ? `pnpm add ${missingProd.join(" ")}` : `npm install ${missingProd.join(" ")}`
223
+ );
224
+ }
225
+ if (missingDev.length > 0) {
226
+ tasks.push(
227
+ pm === "pnpm" ? `pnpm add -D ${missingDev.join(" ")}` : `npm install --save-dev ${missingDev.join(" ")}`
228
+ );
229
+ }
229
230
  try {
230
- const installTasks = [];
231
- if (missingProdDeps.length > 0) {
232
- installTasks.push(
233
- packageManager === "pnpm" ? `pnpm add ${missingProdDeps.join(" ")}` : `npm install ${missingProdDeps.join(" ")}`
234
- );
235
- }
236
- if (missingDevDeps.length > 0) {
237
- installTasks.push(
238
- packageManager === "pnpm" ? `pnpm add -D ${missingDevDeps.join(" ")}` : `npm install --save-dev ${missingDevDeps.join(" ")}`
239
- );
240
- }
241
- for (const installCmd of installTasks) {
242
- if (!json) s.message(`Executing: ${installCmd}`);
231
+ for (const cmd of tasks) {
232
+ if (!json) s.message(`Running: ${cmd}`);
243
233
  await new Promise((resolve, reject) => {
244
- const child = exec(installCmd, { cwd: packageJsonDir });
245
- child.stdout?.on("data", (data) => {
246
- if (!json) process.stdout.write(pc.dim(data));
234
+ const child = exec(cmd, { cwd: packageJsonDir });
235
+ child.stdout?.on("data", (d) => {
236
+ if (!json) process.stdout.write(pc.dim(d));
247
237
  });
248
- child.stderr?.on("data", (data) => {
249
- if (!json) process.stdout.write(pc.dim(pc.red(data)));
238
+ child.stderr?.on("data", (d) => {
239
+ if (!json) process.stdout.write(pc.dim(pc.red(d)));
250
240
  });
251
241
  child.on(
252
242
  "close",
253
- (code) => code === 0 ? resolve() : reject(new Error(`Exit code ${code}`))
243
+ (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`))
254
244
  );
255
245
  });
256
246
  }
257
- if (!json) {
258
- s.stop("Dependencies installed.");
259
- }
260
- } catch (error) {
247
+ if (!json) s.stop("Dependencies installed.");
248
+ } catch (err) {
261
249
  if (!json) {
262
250
  s.stop("Installation failed.");
263
- log.error(pc.dim(String(error)));
264
- } else {
265
- outputError(json, "Automated dependency installation failed.");
266
- }
251
+ log.error(pc.dim(String(err)));
252
+ } else outputError(json, "Dependency installation failed.");
267
253
  return;
268
254
  }
269
255
  }
270
256
  }
271
257
  const filesToDownload = [];
272
- if (blockMeta.baseFiles) {
258
+ if (blockMeta.baseFiles?.length) {
273
259
  filesToDownload.push(...blockMeta.baseFiles);
274
260
  }
275
261
  if (adapterContext.core) {
276
262
  filesToDownload.push({
277
263
  source: adapterContext.core,
278
- target: path.basename(adapterContext.core, ".txt")
264
+ target: path.basename(adapterContext.core)
279
265
  });
280
266
  }
281
267
  if (variantMeta && Array.isArray(variantMeta.files)) {
282
- for (const fileEntry of variantMeta.files) {
283
- if (typeof fileEntry === "string") {
284
- filesToDownload.push({
285
- source: fileEntry,
286
- target: path.basename(fileEntry, ".txt")
287
- });
288
- } else {
289
- filesToDownload.push(fileEntry);
290
- }
268
+ for (const entry of variantMeta.files) {
269
+ filesToDownload.push(
270
+ typeof entry === "string" ? { source: entry, target: path.basename(entry) } : entry
271
+ );
291
272
  }
292
273
  }
293
- let fileExistsConflict = false;
294
- for (const fileMap of filesToDownload) {
295
- const destinationPath = join(targetFolder, fileMap.target);
274
+ let hasConflict = false;
275
+ for (const fm of filesToDownload) {
296
276
  try {
297
- await fs.access(destinationPath);
298
- fileExistsConflict = true;
277
+ await fs.access(join(targetFolder, fm.target));
278
+ hasConflict = true;
299
279
  break;
300
280
  } catch {
301
281
  }
302
282
  }
303
- if (fileExistsConflict) {
304
- const overwritePrompt = yes ? true : await confirm({
283
+ if (hasConflict) {
284
+ const overwrite = yes ? true : await confirm({
305
285
  message: `Files for "${targetBlock}" already exist. Overwrite?`,
306
286
  initialValue: false
307
287
  });
308
- if (!yes) handleCancel(overwritePrompt);
309
- if (!overwritePrompt) {
310
- outputResult(json, {
311
- success: false,
312
- reason: "aborted",
313
- message: "Local modifications preserved."
314
- });
288
+ if (!yes) handleCancel(overwrite);
289
+ if (!overwrite) {
290
+ outputResult(json, { success: false, reason: "aborted", message: "Local files preserved." });
315
291
  return;
316
292
  }
317
293
  }
318
- if (!json) {
319
- s.start(`Downloading ${targetBlock}...`);
320
- }
294
+ if (!json) s.start(`Downloading ${targetBlock}...`);
295
+ const filesWritten = [];
321
296
  try {
322
- for (const fileMap of filesToDownload) {
323
- const fileUrl = `${RAW_CDN_BASE}/${fileMap.source}`;
324
- const response = await fetch(fileUrl);
325
- if (!response.ok) throw new Error(`Download failed for: ${fileMap.source}`);
326
- const fileContent = await response.text();
327
- const localWriteLocation = join(targetFolder, fileMap.target);
328
- await fs.mkdir(dirname(localWriteLocation), { recursive: true });
329
- await fs.writeFile(localWriteLocation, fileContent, "utf-8");
297
+ for (const fm of filesToDownload) {
298
+ const res = await fetch(`${RAW_CDN_BASE}/${fm.source}`);
299
+ if (!res.ok) throw new Error(`Download failed: ${fm.source} (HTTP ${res.status})`);
300
+ let content = await res.text();
301
+ const writtenFilePath = path.join(targetFolder, fm.target);
302
+ content = rewriteFileImports(content, writtenFilePath, blocksRootAbsolute, aliasBase);
303
+ await fs.mkdir(dirname(writtenFilePath), { recursive: true });
304
+ await fs.writeFile(writtenFilePath, content, "utf-8");
305
+ filesWritten.push(writtenFilePath);
330
306
  }
331
- if (!json) s.stop("Files copied.");
307
+ if (!json) s.stop("Files written.");
332
308
  outputResult(json, {
333
309
  success: true,
334
310
  block: targetBlock,
335
- filesWritten: filesToDownload.map((f) => join(targetFolder, f.target)),
336
- dependenciesInstalled: [...missingProdDeps, ...missingDevDeps],
337
- message: `Block added to ${physicalPath}/${targetBlock}`
311
+ filesWritten,
312
+ dependenciesInstalled: [...missingProd, ...missingDev],
313
+ message: `${targetBlock} added to ${physicalBlocksPath}/${targetBlock}`
338
314
  });
339
- } catch (error) {
315
+ } catch (err) {
340
316
  if (!json) {
341
- s.stop("Failed to write files.");
342
- log.error(pc.dim(String(error)));
343
- } else {
344
- outputError(json, "Fatal error occurred while writing block files.");
345
- }
317
+ s.stop("Failed.");
318
+ log.error(pc.dim(String(err)));
319
+ } else outputError(json, "Fatal error while writing block files.");
346
320
  }
347
321
  }
348
322
 
@@ -498,22 +472,40 @@ var format = {
498
472
 
499
473
  // src/commands/init.ts
500
474
  async function resolveTsConfigPaths(cwd) {
501
- const possiblePaths = [join7(cwd, "tsconfig.json"), join7(cwd, "jsconfig.json")];
502
- for (const configPath of possiblePaths) {
503
- if (existsSync4(configPath)) {
504
- try {
505
- const rawContent = await fs2.readFile(configPath, "utf-8");
506
- const cleanJsonContent = rawContent.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
507
- const parsed = JSON.parse(cleanJsonContent);
508
- const baseUrl = parsed.compilerOptions?.baseUrl || ".";
509
- const paths = parsed.compilerOptions?.paths || {};
510
- return { baseUrl, paths };
511
- } catch {
512
- }
475
+ const candidates = [join7(cwd, "tsconfig.json"), join7(cwd, "jsconfig.json")];
476
+ for (const configPath of candidates) {
477
+ if (!existsSync4(configPath)) continue;
478
+ try {
479
+ const raw = await fs2.readFile(configPath, "utf-8");
480
+ const clean = raw.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
481
+ const parsed = JSON.parse(clean);
482
+ return {
483
+ baseUrl: parsed.compilerOptions?.baseUrl ?? ".",
484
+ paths: parsed.compilerOptions?.paths ?? {}
485
+ };
486
+ } catch {
513
487
  }
514
488
  }
515
489
  return { baseUrl: ".", paths: {} };
516
490
  }
491
+ function deriveBlockAlias(tsPaths, blocksPhysicalPath) {
492
+ const normalizedBlocksPath = blocksPhysicalPath.replace(/^\.\//, "").replace(/\\/g, "/");
493
+ for (const [aliasKey, targets] of Object.entries(tsPaths)) {
494
+ if (!Array.isArray(targets) || targets.length === 0) continue;
495
+ const rawTarget = targets[0].replace(/\\/g, "/");
496
+ const targetRoot = rawTarget.replace(/\*$/, "").replace(/\/$/, "").replace(/^\.\//, "");
497
+ const aliasRoot = aliasKey.replace(/\/\*$/, "").replace(/\*$/, "");
498
+ if (targetRoot === "") {
499
+ return `${aliasRoot}/${normalizedBlocksPath}`.replace(/\/+/g, "/");
500
+ }
501
+ if (normalizedBlocksPath === targetRoot || normalizedBlocksPath.startsWith(`${targetRoot}/`)) {
502
+ const suffix = normalizedBlocksPath.slice(targetRoot.length).replace(/^\//, "");
503
+ const alias = suffix ? `${aliasRoot}/${suffix}` : aliasRoot;
504
+ return alias.replace(/\/+/g, "/");
505
+ }
506
+ }
507
+ return void 0;
508
+ }
517
509
  function outputInitError(json, message) {
518
510
  if (json) {
519
511
  process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
@@ -543,7 +535,7 @@ async function initCommand(options = {}) {
543
535
  action = "overwrite";
544
536
  } else {
545
537
  const actionPrompt = await select2({
546
- message: "A blockend.json configuration already exists. What would you like to do?",
538
+ message: "A blockend.json already exists. What would you like to do?",
547
539
  options: [
548
540
  { value: "keep", label: "Keep existing (cancel setup)" },
549
541
  { value: "overwrite", label: "Overwrite current configuration" },
@@ -609,7 +601,7 @@ async function initCommand(options = {}) {
609
601
  placeholder: defaultDir,
610
602
  initialValue: defaultDir,
611
603
  validate(value) {
612
- if (value?.trim().length === 0) return "Directory path cannot be empty.";
604
+ if (!value || value.trim().length === 0) return "Directory path cannot be empty.";
613
605
  }
614
606
  });
615
607
  if (isCancel2(directoryPrompt)) {
@@ -618,37 +610,14 @@ async function initCommand(options = {}) {
618
610
  }
619
611
  rawPhysicalInput = String(directoryPrompt).trim();
620
612
  }
621
- const relativeBlocksPath = path2.relative(cwd, path2.resolve(cwd, rawPhysicalInput));
622
- let finalPath = relativeBlocksPath.replace(/\\/g, "/");
623
- if (!finalPath.startsWith(".") && !finalPath.startsWith("/")) {
624
- finalPath = `./${finalPath}`;
625
- }
626
- let finalBlockAlias = "@/blocks";
627
- const configuredAliases = Object.keys(tsConfig.paths);
628
- if (configuredAliases.length > 0) {
629
- const matchedAliasKey = configuredAliases.find((alias) => {
630
- const targets = tsConfig.paths[alias];
631
- if (Array.isArray(targets) && targets.length > 0) {
632
- const targetSubPath = targets[0].replace(/\*$/, "").replace(/\\/g, "/");
633
- return finalPath.replace(/^\.\//, "").startsWith(targetSubPath);
634
- }
635
- return false;
636
- });
637
- if (matchedAliasKey) {
638
- const cleanKey = matchedAliasKey.replace(/\*$/, "");
639
- const targets = tsConfig.paths[matchedAliasKey];
640
- const targetSubPath = targets[0].replace(/\*$/, "").replace(/\\/g, "/");
641
- const relativeSuffix = finalPath.replace(/^\.\//, "").replace(targetSubPath, "");
642
- finalBlockAlias = `${cleanKey}${relativeSuffix}`.replace(/\/$/, "");
643
- } else {
644
- const primaryAlias = configuredAliases.find((k) => k.startsWith("@")) || configuredAliases[0];
645
- const inferredBaseAlias = primaryAlias.replace(/\*$/, "");
646
- const folderSegmentName = path2.basename(finalPath);
647
- finalBlockAlias = `${inferredBaseAlias}${folderSegmentName}`;
648
- }
649
- } else {
650
- const folderSegmentName = path2.basename(finalPath);
651
- finalBlockAlias = `@/${folderSegmentName}`;
613
+ const relativeBlocksPath = path2.relative(cwd, path2.resolve(cwd, rawPhysicalInput)).replace(/\\/g, "/");
614
+ const finalPath = relativeBlocksPath.startsWith(".") ? relativeBlocksPath : `./${relativeBlocksPath}`;
615
+ const hasTsPaths = Object.keys(tsConfig.paths).length > 0;
616
+ const finalBlockAlias = hasTsPaths ? deriveBlockAlias(tsConfig.paths, relativeBlocksPath) : void 0;
617
+ if (!json) {
618
+ log2.info(
619
+ `Import alias: ${theme.state.info(finalBlockAlias === void 0 ? " relative imports" : finalBlockAlias)}`
620
+ );
652
621
  }
653
622
  let includeRedis = false;
654
623
  if (context.hasRedis) {
@@ -665,15 +634,16 @@ async function initCommand(options = {}) {
665
634
  }
666
635
  }
667
636
  const configPayload = {
668
- $schema: "https://blockend.dev/schema.json",
637
+ $schema: "https://blockend.noorulhassan.com/schema.json",
669
638
  environment: framework || "express",
670
639
  language: context.language || "typescript",
671
640
  includeRedis,
672
641
  aliases: {
673
- blocks: finalBlockAlias
642
+ ...finalBlockAlias ? { blocks: finalBlockAlias } : {}
674
643
  },
675
644
  paths: {
676
645
  blocks: finalPath
646
+ // e.g. "./src/blocks" — always a real physical path
677
647
  }
678
648
  };
679
649
  if (!json) s.start("Writing configuration...");
@@ -682,7 +652,7 @@ async function initCommand(options = {}) {
682
652
  if (!json) s.stop("Configuration saved.");
683
653
  outputInitResult(json, {
684
654
  success: true,
685
- message: "Blockend initialized successfully! Run: npx blockend add <block>",
655
+ message: "Blockend initialized! Run: npx blockend add <block>",
686
656
  config: configPayload
687
657
  });
688
658
  } catch {
@@ -690,7 +660,7 @@ async function initCommand(options = {}) {
690
660
  s.stop("Failed");
691
661
  outputInitError(false, "Failed to write configuration file.");
692
662
  } else {
693
- outputInitError(true, "Failed to write architectural layout configuration map.");
663
+ outputInitError(true, "Failed to write configuration file.");
694
664
  }
695
665
  }
696
666
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blockend-cli",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "CLI for installing production-ready backend blocks into Next.js, Express, Hono, NestJS, and other Node.js applications.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",