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.
- package/dist/index.js +236 -249
- 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
|
|
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 {
|
|
19
|
-
import {
|
|
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 =
|
|
68
|
+
const configPath = join(cwd, "blockend.json");
|
|
144
69
|
console.log(`
|
|
145
|
-
\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557
|
|
146
|
-
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551
|
|
147
|
-
\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551
|
|
148
|
-
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\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(
|
|
153
|
-
if (
|
|
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
|
|
97
|
+
s.start("Scanning project layout...");
|
|
173
98
|
const context = await detectProject(cwd);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
initialValue:
|
|
205
|
-
|
|
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(
|
|
132
|
+
if (isCancel(directoryPrompt)) {
|
|
208
133
|
outro(format.muted("Initialization cancelled."));
|
|
209
134
|
return;
|
|
210
135
|
}
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
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:
|
|
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(
|
|
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
|
|
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 =
|
|
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.
|
|
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(
|
|
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 =
|
|
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
|
|
352
|
-
if (!
|
|
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
|
-
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
386
|
-
|
|
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
|
|
395
|
-
const
|
|
396
|
-
|
|
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: ${
|
|
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(
|
|
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
|
|
411
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
process.stdout.write(pc2.dim(data));
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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(
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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 ${
|
|
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
|
|
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
|
+
"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
|
}
|