blockend-cli 1.2.0 → 1.3.1
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 +102 -146
- package/package.json +6 -3
- package/dist/index.mjs +0 -49
- package/dist/templates/auth/index.hbs +0 -13
- package/dist/templates/auth/meta.json +0 -12
- package/dist/templates/rate-limiter/index.hbs +0 -38
- package/dist/templates/rate-limiter/meta.json +0 -22
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";
|
|
@@ -140,9 +48,9 @@ var format = {
|
|
|
140
48
|
// src/commands/init.ts
|
|
141
49
|
async function initCommand() {
|
|
142
50
|
const cwd = process.cwd();
|
|
143
|
-
const configPath =
|
|
51
|
+
const configPath = join(cwd, "blockend.json");
|
|
144
52
|
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
|
|
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
|
|
146
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
|
|
147
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
|
|
148
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
|
|
@@ -150,7 +58,7 @@ async function initCommand() {
|
|
|
150
58
|
\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
59
|
`);
|
|
152
60
|
intro(format.title("Blockend \xB7 Intelligent Backend Blocks Setup"));
|
|
153
|
-
if (
|
|
61
|
+
if (existsSync(configPath)) {
|
|
154
62
|
const action = await select({
|
|
155
63
|
message: "blockend.json already exists. What do you want to do?",
|
|
156
64
|
options: [
|
|
@@ -197,18 +105,6 @@ async function initCommand() {
|
|
|
197
105
|
outro(format.muted("Initialization cancelled."));
|
|
198
106
|
return;
|
|
199
107
|
}
|
|
200
|
-
const language = await select({
|
|
201
|
-
message: "Confirm primary language",
|
|
202
|
-
initialValue: context.language === "typescript" ? "typescript" : "javascript",
|
|
203
|
-
options: [
|
|
204
|
-
{ value: "typescript", label: "TypeScript" },
|
|
205
|
-
{ value: "javascript", label: "JavaScript" }
|
|
206
|
-
]
|
|
207
|
-
});
|
|
208
|
-
if (isCancel(language)) {
|
|
209
|
-
outro(format.muted("Initialization cancelled."));
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
108
|
const availableAliases = Object.keys(context.aliasMap || {});
|
|
213
109
|
const baseAliasToken = availableAliases.length > 0 ? availableAliases[0] : "@/";
|
|
214
110
|
const blockAliasInput = await text({
|
|
@@ -224,7 +120,7 @@ async function initCommand() {
|
|
|
224
120
|
const aliasPrefix = availableAliases.find((a) => blockAlias.startsWith(a)) || "";
|
|
225
121
|
const physicalPrefix = aliasPrefix ? context.aliasMap[aliasPrefix] : "./";
|
|
226
122
|
const resolvedSubDir = blockAlias.replace(aliasPrefix, "");
|
|
227
|
-
const assumedPhysicalDir =
|
|
123
|
+
const assumedPhysicalDir = join(physicalPrefix, resolvedSubDir);
|
|
228
124
|
const relativeBlocksPath = path.relative(cwd, path.resolve(cwd, assumedPhysicalDir));
|
|
229
125
|
const normalizedPath = relativeBlocksPath.replace(/\\/g, "/");
|
|
230
126
|
const finalPath = normalizedPath.startsWith(".") ? normalizedPath : `./${normalizedPath}`;
|
|
@@ -241,7 +137,7 @@ async function initCommand() {
|
|
|
241
137
|
const configPayload = {
|
|
242
138
|
$schema: "https://blockend.dev/schema.json",
|
|
243
139
|
environment: framework,
|
|
244
|
-
language,
|
|
140
|
+
language: "typescript",
|
|
245
141
|
includeRedis,
|
|
246
142
|
aliases: {
|
|
247
143
|
blocks: blockAlias
|
|
@@ -262,7 +158,7 @@ async function initCommand() {
|
|
|
262
158
|
}
|
|
263
159
|
|
|
264
160
|
// src/commands/add.ts
|
|
265
|
-
import path2, { join as
|
|
161
|
+
import path2, { join as join2, dirname } from "path";
|
|
266
162
|
import fs2 from "fs/promises";
|
|
267
163
|
import { exec } from "child_process";
|
|
268
164
|
import { intro as intro2, outro as outro2, select as select2, spinner as spinner2, confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
@@ -270,7 +166,6 @@ import pc2 from "picocolors";
|
|
|
270
166
|
var REPO_OWNER = "codewithnuh";
|
|
271
167
|
var REPO_NAME = "blockend";
|
|
272
168
|
var BRANCH = "master";
|
|
273
|
-
var LOCAL_DEV_URL = "http://localhost:5000";
|
|
274
169
|
var RAW_CDN_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}`;
|
|
275
170
|
var MANIFEST_URL = `${RAW_CDN_BASE}/registry/index.json`;
|
|
276
171
|
function handleCancel(value) {
|
|
@@ -279,16 +174,47 @@ function handleCancel(value) {
|
|
|
279
174
|
process.exit(0);
|
|
280
175
|
}
|
|
281
176
|
}
|
|
177
|
+
async function findUp(filename, startDir) {
|
|
178
|
+
let dir = startDir;
|
|
179
|
+
while (true) {
|
|
180
|
+
const checkPath = join2(dir, filename);
|
|
181
|
+
try {
|
|
182
|
+
await fs2.access(checkPath);
|
|
183
|
+
return checkPath;
|
|
184
|
+
} catch {
|
|
185
|
+
const parent = dirname(dir);
|
|
186
|
+
if (parent === dir) break;
|
|
187
|
+
dir = parent;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
282
192
|
async function addCommand(blockName) {
|
|
283
193
|
intro2(pc2.bgBlack(pc2.magenta(" Blockend Component Ingestion ")));
|
|
284
194
|
const cwd = process.cwd();
|
|
285
|
-
const configPath =
|
|
195
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
196
|
+
if (!configPath) {
|
|
197
|
+
outro2(pc2.red("\u2716 blockend.json not found. Run 'npx blockend init' first."));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const rootDir = dirname(configPath);
|
|
286
201
|
let config;
|
|
287
202
|
try {
|
|
288
203
|
const configFile = await fs2.readFile(configPath, "utf-8");
|
|
289
204
|
config = JSON.parse(configFile);
|
|
290
205
|
} catch {
|
|
291
|
-
outro2(pc2.red("\u2716
|
|
206
|
+
outro2(pc2.red("\u2716 Failed to parse blockend.json layout configuration."));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (config.language !== "typescript") {
|
|
210
|
+
outro2(
|
|
211
|
+
pc2.yellow(
|
|
212
|
+
`
|
|
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.`
|
|
216
|
+
)
|
|
217
|
+
);
|
|
292
218
|
return;
|
|
293
219
|
}
|
|
294
220
|
const s = spinner2();
|
|
@@ -303,14 +229,19 @@ async function addCommand(blockName) {
|
|
|
303
229
|
s.stop(pc2.red("\u2716 Failed to fetch the component registry from GitHub network paths."));
|
|
304
230
|
return;
|
|
305
231
|
}
|
|
232
|
+
const envKey = String(config.environment);
|
|
306
233
|
let targetBlock = blockName;
|
|
307
234
|
if (!targetBlock) {
|
|
308
|
-
const availableOptions = Object.keys(registry).map((key) => ({
|
|
235
|
+
const availableOptions = Object.keys(registry).filter((key) => registry[key].environments[envKey] !== void 0).map((key) => ({
|
|
309
236
|
value: key,
|
|
310
237
|
label: `${key} - ${pc2.dim(registry[key].description)}`
|
|
311
238
|
}));
|
|
312
239
|
if (availableOptions.length === 0) {
|
|
313
|
-
outro2(
|
|
240
|
+
outro2(
|
|
241
|
+
pc2.yellow(
|
|
242
|
+
`\u26A0 No backend blocks are currently available for your framework layer: [${envKey}].`
|
|
243
|
+
)
|
|
244
|
+
);
|
|
314
245
|
return;
|
|
315
246
|
}
|
|
316
247
|
const selectBlockPrompt = await select2({
|
|
@@ -325,7 +256,6 @@ async function addCommand(blockName) {
|
|
|
325
256
|
outro2(pc2.red(`\u2716 Block "${targetBlock}" does not exist in the remote registry.`));
|
|
326
257
|
return;
|
|
327
258
|
}
|
|
328
|
-
const envKey = String(config.environment);
|
|
329
259
|
const envConfig = blockMeta.environments[envKey];
|
|
330
260
|
if (!envConfig) {
|
|
331
261
|
outro2(
|
|
@@ -352,13 +282,17 @@ async function addCommand(blockName) {
|
|
|
352
282
|
}
|
|
353
283
|
variantMeta = envConfig.variants[selectedVariant];
|
|
354
284
|
}
|
|
285
|
+
const packageJsonPath = await findUp("package.json", rootDir);
|
|
286
|
+
if (!packageJsonPath) {
|
|
287
|
+
outro2(pc2.red("\u2716 Could not locate package.json in your current directory layout hierarchy."));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const packageJsonDir = dirname(packageJsonPath);
|
|
355
291
|
let packageJson;
|
|
356
292
|
try {
|
|
357
|
-
packageJson = JSON.parse(
|
|
358
|
-
await fs2.readFile(join7(cwd, "package.json"), "utf-8")
|
|
359
|
-
);
|
|
293
|
+
packageJson = JSON.parse(await fs2.readFile(packageJsonPath, "utf-8"));
|
|
360
294
|
} catch {
|
|
361
|
-
outro2(pc2.red("\u2716
|
|
295
|
+
outro2(pc2.red("\u2716 Failed parsing file data configurations from target package.json location."));
|
|
362
296
|
return;
|
|
363
297
|
}
|
|
364
298
|
const installedDeps = {
|
|
@@ -378,14 +312,14 @@ async function addCommand(blockName) {
|
|
|
378
312
|
});
|
|
379
313
|
handleCancel(shouldInstallPrompt);
|
|
380
314
|
if (shouldInstallPrompt) {
|
|
381
|
-
const packageManager = await fs2.access(
|
|
315
|
+
const packageManager = await fs2.access(join2(packageJsonDir, "pnpm-lock.yaml")).then(() => "pnpm").catch(() => "npm");
|
|
382
316
|
s.start(`Preparing native workspace via ${packageManager}...`);
|
|
383
317
|
try {
|
|
384
318
|
const installCmd = packageManager === "pnpm" ? `pnpm add ${missingDeps.join(" ")}` : `npm install ${missingDeps.join(" ")}`;
|
|
385
319
|
s.stop(pc2.cyan(`Executing: ${installCmd}
|
|
386
320
|
`));
|
|
387
321
|
await new Promise((resolve, reject) => {
|
|
388
|
-
const child = exec(installCmd, { cwd });
|
|
322
|
+
const child = exec(installCmd, { cwd: packageJsonDir });
|
|
389
323
|
child.stdout?.on("data", (data) => {
|
|
390
324
|
process.stdout.write(pc2.dim(data));
|
|
391
325
|
});
|
|
@@ -407,36 +341,56 @@ async function addCommand(blockName) {
|
|
|
407
341
|
}
|
|
408
342
|
s.start(`Downloading clean production template block [${targetBlock}]...`);
|
|
409
343
|
try {
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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);
|
|
415
350
|
}
|
|
416
|
-
const coreCodeTemplate = await coreFetchResponse.text();
|
|
417
351
|
let physicalPath = config.paths.blocks;
|
|
418
352
|
if (physicalPath.startsWith("@")) {
|
|
419
353
|
physicalPath = "./src/blocks";
|
|
420
354
|
}
|
|
421
|
-
let targetFolder = path2.resolve(
|
|
355
|
+
let targetFolder = path2.resolve(rootDir, physicalPath);
|
|
422
356
|
if (variantMeta && selectedVariant) {
|
|
423
|
-
targetFolder =
|
|
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 {
|
|
368
|
+
}
|
|
369
|
+
}
|
|
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}]...`);
|
|
424
382
|
}
|
|
425
383
|
await fs2.mkdir(targetFolder, { recursive: true });
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const variantFetchResponse = await fetch(variantFileUrl);
|
|
432
|
-
if (!variantFetchResponse.ok) {
|
|
433
|
-
throw new Error(
|
|
434
|
-
`Failed downloading storage variant file: ${variantFetchResponse.statusText}`
|
|
435
|
-
);
|
|
384
|
+
for (const remoteFilePath of remoteFilesToDownload) {
|
|
385
|
+
const fileUrl = `${RAW_CDN_BASE}/${remoteFilePath}`;
|
|
386
|
+
const response = await fetch(fileUrl);
|
|
387
|
+
if (!response.ok) {
|
|
388
|
+
throw new Error(`Failed downloading file: ${remoteFilePath} (${response.statusText})`);
|
|
436
389
|
}
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
|
|
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");
|
|
440
394
|
}
|
|
441
395
|
const cleanDisplayPath = variantMeta && selectedVariant ? `${physicalPath.replace(/\\/g, "/")}/${targetBlock}` : physicalPath.replace(/\\/g, "/");
|
|
442
396
|
s.stop(pc2.green("\u2714 Component isolation structures written smoothly."));
|
|
@@ -445,9 +399,11 @@ async function addCommand(blockName) {
|
|
|
445
399
|
`\u2728 Source blocks written to ${cleanDisplayPath}/ layout. Code ownership transferred!`
|
|
446
400
|
)
|
|
447
401
|
);
|
|
402
|
+
process.exit(0);
|
|
448
403
|
} catch (error) {
|
|
449
404
|
s.stop(pc2.red("\u2716 Fatal crash occurred while downloading or transferring file layouts."));
|
|
450
405
|
console.error(error);
|
|
406
|
+
process.exit(1);
|
|
451
407
|
}
|
|
452
408
|
}
|
|
453
409
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blockend-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Intelligent, modular backend blocks right inside your terminal",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"readme.md"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "tsup src/index.ts --format esm --dts --out-dir dist"
|
|
15
|
+
"build": "tsup src/index.ts --format esm --dts --out-dir dist",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"cli",
|
|
@@ -25,11 +26,13 @@
|
|
|
25
26
|
"license": "ISC",
|
|
26
27
|
"packageManager": "pnpm@10.33.2",
|
|
27
28
|
"dependencies": {
|
|
29
|
+
"@blockend/detector": "workspace:*",
|
|
28
30
|
"@clack/prompts": "^1.5.1",
|
|
29
31
|
"commander": "^15.0.0",
|
|
30
32
|
"picocolors": "^1.1.1"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
33
|
-
"@types/node": "^25.9.3"
|
|
35
|
+
"@types/node": "^25.9.3",
|
|
36
|
+
"tsup": "^8.5.1"
|
|
34
37
|
}
|
|
35
38
|
}
|
package/dist/index.mjs
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/index.ts
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import { intro, outro, select, text } from "@clack/prompts";
|
|
6
|
-
import pc from "picocolors";
|
|
7
|
-
var program = new Command();
|
|
8
|
-
program.name("blockend").description("Zero-dependency production backend blocks CLI").version("0.1.0");
|
|
9
|
-
program.command("init").description("Initialize blockend configuration in your project").action(async () => {
|
|
10
|
-
intro(pc.bgBlack(pc.cyan(" Blockend Initialization ")));
|
|
11
|
-
const environment = await select({
|
|
12
|
-
message: "Select your backend framework environment:",
|
|
13
|
-
options: [
|
|
14
|
-
{ value: "express", label: "Express.js" },
|
|
15
|
-
{ value: "fastify", label: "Fastify" },
|
|
16
|
-
{ value: "nextjs", label: "Next.js (App Router)" }
|
|
17
|
-
]
|
|
18
|
-
});
|
|
19
|
-
const blockPath = await text({
|
|
20
|
-
message: "Where should we save your backend blocks?",
|
|
21
|
-
initialValue: "./src/blocks",
|
|
22
|
-
placeholder: "./src/blocks"
|
|
23
|
-
});
|
|
24
|
-
console.log("\nWriting configuration target data...");
|
|
25
|
-
console.log(
|
|
26
|
-
pc.green(`\u2714 Targets set: Environment -> ${String(environment)}, Path -> ${String(blockPath)}`)
|
|
27
|
-
);
|
|
28
|
-
outro(pc.cyan("blockend.json configured mock-successfully."));
|
|
29
|
-
});
|
|
30
|
-
program.command("add [block]").description("Add a backend block to your project").action(async (block) => {
|
|
31
|
-
intro(pc.bgBlack(pc.magenta(" Blockend Add Tool ")));
|
|
32
|
-
if (!block) {
|
|
33
|
-
const selectedBlock = await select({
|
|
34
|
-
message: "Which backend block would you like to add?",
|
|
35
|
-
options: [
|
|
36
|
-
{ value: "rate-limiter", label: "rate-limiter (Redis/Memory)" },
|
|
37
|
-
{ value: "auth-handler", label: "auth-handler (JWT/Session)" },
|
|
38
|
-
{ value: "error-handler", label: "global-error-handler" }
|
|
39
|
-
]
|
|
40
|
-
});
|
|
41
|
-
block = selectedBlock;
|
|
42
|
-
}
|
|
43
|
-
console.log(pc.yellow(`
|
|
44
|
-
[Mock Run]: Fetching metadata manifest for "${block}"...`));
|
|
45
|
-
console.log(pc.green(`\u2714 [Mock Run]: Injected missing dependencies into your package context.`));
|
|
46
|
-
console.log(pc.green(`\u2714 [Mock Run]: Written file cleanly into your target blocks path.`));
|
|
47
|
-
outro(pc.magenta(`Successfully added ${block} block!`));
|
|
48
|
-
});
|
|
49
|
-
program.parse(process.argv);
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export interface SessionUser {
|
|
2
|
-
id: string;
|
|
3
|
-
email: string;
|
|
4
|
-
roles: string[];
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function requireRole(user: SessionUser | null, role: string): SessionUser {
|
|
8
|
-
if (!user || !user.roles.includes(role)) {
|
|
9
|
-
throw new Error("Unauthorized");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
return user;
|
|
13
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export interface RateLimitOptions {
|
|
2
|
-
limit: number;
|
|
3
|
-
windowMs: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface RateLimitResult {
|
|
7
|
-
allowed: boolean;
|
|
8
|
-
remaining: number;
|
|
9
|
-
resetAt: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const buckets = new Map<string, { count: number; resetAt: number }>();
|
|
13
|
-
|
|
14
|
-
export function createRateLimiter(options: RateLimitOptions) {
|
|
15
|
-
return function checkRateLimit(key: string): RateLimitResult {
|
|
16
|
-
const now = Date.now();
|
|
17
|
-
const current = buckets.get(key);
|
|
18
|
-
|
|
19
|
-
if (!current || current.resetAt <= now) {
|
|
20
|
-
const resetAt = now + options.windowMs;
|
|
21
|
-
buckets.set(key, { count: 1, resetAt });
|
|
22
|
-
return { allowed: true, remaining: options.limit - 1, resetAt };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (current.count >= options.limit) {
|
|
26
|
-
return { allowed: false, remaining: 0, resetAt: current.resetAt };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
current.count += 1;
|
|
30
|
-
return {
|
|
31
|
-
allowed: true,
|
|
32
|
-
remaining: Math.max(options.limit - current.count, 0),
|
|
33
|
-
resetAt: current.resetAt
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const rateLimitDriver = "{{driver}}";
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "rate-limiter",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"dependencies": [],
|
|
5
|
-
"prompts": [
|
|
6
|
-
{
|
|
7
|
-
"id": "driver",
|
|
8
|
-
"type": "select",
|
|
9
|
-
"message": "Select your storage backend:",
|
|
10
|
-
"options": [
|
|
11
|
-
{ "value": "in-memory", "label": "In-Memory (Zero dependencies)" },
|
|
12
|
-
{ "value": "redis", "label": "Redis (Scalable cluster)" }
|
|
13
|
-
]
|
|
14
|
-
}
|
|
15
|
-
],
|
|
16
|
-
"files": [
|
|
17
|
-
{
|
|
18
|
-
"template": "index.hbs",
|
|
19
|
-
"output": "src/lib/blocks/{{name}}.ts"
|
|
20
|
-
}
|
|
21
|
-
]
|
|
22
|
-
}
|