blockend-cli 1.0.2
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.d.ts +1 -0
- package/dist/index.js +399 -0
- package/dist/index.mjs +49 -0
- package/dist/templates/auth/index.hbs +13 -0
- package/dist/templates/auth/meta.json +12 -0
- package/dist/templates/rate-limiter/index.hbs +38 -0
- package/dist/templates/rate-limiter/meta.json +22 -0
- package/package.json +35 -0
- package/readme.md +30 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import path, { join as join6 } from "path";
|
|
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
|
+
import { existsSync } from "fs";
|
|
18
|
+
import { join as join2 } from "path";
|
|
19
|
+
import { existsSync as existsSync2 } from "fs";
|
|
20
|
+
import { join as join3 } from "path";
|
|
21
|
+
import { existsSync as existsSync3 } from "fs";
|
|
22
|
+
import { join as join4 } from "path";
|
|
23
|
+
async function readPackageJson(cwd) {
|
|
24
|
+
const pkgPath = join(cwd, "package.json");
|
|
25
|
+
try {
|
|
26
|
+
const content = await readFile(pkgPath, "utf-8");
|
|
27
|
+
return JSON.parse(content);
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error(`No package.json found at ${pkgPath}. Run blockend from your project root.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function readTsConfig(cwd) {
|
|
33
|
+
const tsconfigPath = join2(cwd, "tsconfig.json");
|
|
34
|
+
if (!existsSync(tsconfigPath)) return null;
|
|
35
|
+
try {
|
|
36
|
+
const content = await readFile2(tsconfigPath, "utf-8");
|
|
37
|
+
const cleanLines = content.split(/\r?\n/).filter((line) => {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
return !trimmed.startsWith("//") && !trimmed.startsWith("/*");
|
|
40
|
+
}).join("\n").replace(/,(\s*[}\]])/g, "$1");
|
|
41
|
+
return JSON.parse(cleanLines);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function inferSrcDir(cwd) {
|
|
47
|
+
const candidates = ["src", "app", "lib"];
|
|
48
|
+
for (const dir of candidates) {
|
|
49
|
+
if (existsSync2(join3(cwd, dir))) {
|
|
50
|
+
return join3(cwd, dir);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return cwd;
|
|
54
|
+
}
|
|
55
|
+
function detectFramework(deps) {
|
|
56
|
+
if ("fastify" in deps) return "fastify";
|
|
57
|
+
if ("hono" in deps) return "hono";
|
|
58
|
+
if ("express" in deps) return "express";
|
|
59
|
+
if ("next" in deps) return "next";
|
|
60
|
+
return "none";
|
|
61
|
+
}
|
|
62
|
+
function detectRuntime(deps) {
|
|
63
|
+
if ("@types/bun" in deps || "bun-types" in deps) return "bun";
|
|
64
|
+
return "node";
|
|
65
|
+
}
|
|
66
|
+
function detectPackageManager(cwd) {
|
|
67
|
+
if (existsSync3(join4(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
68
|
+
if (existsSync3(join4(cwd, "bun.lockb"))) return "bun";
|
|
69
|
+
if (existsSync3(join4(cwd, "yarn.lock"))) return "yarn";
|
|
70
|
+
return "npm";
|
|
71
|
+
}
|
|
72
|
+
async function detectProject(cwd) {
|
|
73
|
+
const [pkg, tsConfig] = await Promise.all([readPackageJson(cwd), readTsConfig(cwd)]);
|
|
74
|
+
const allDeps = {
|
|
75
|
+
...pkg.dependencies,
|
|
76
|
+
...pkg.devDependencies,
|
|
77
|
+
...pkg.peerDependencies
|
|
78
|
+
};
|
|
79
|
+
const srcDir = await inferSrcDir(cwd);
|
|
80
|
+
return {
|
|
81
|
+
root: cwd,
|
|
82
|
+
language: tsConfig !== null ? "typescript" : "javascript",
|
|
83
|
+
runtime: detectRuntime(allDeps),
|
|
84
|
+
framework: detectFramework(allDeps),
|
|
85
|
+
packageManager: detectPackageManager(cwd),
|
|
86
|
+
hasRedis: "ioredis" in allDeps || "redis" in allDeps,
|
|
87
|
+
hasPrisma: "@prisma/client" in allDeps,
|
|
88
|
+
hasDrizzle: "drizzle-orm" in allDeps,
|
|
89
|
+
aliasMap: tsConfig?.compilerOptions?.paths ? flattenTsPaths(tsConfig.compilerOptions.paths) : {},
|
|
90
|
+
srcDir,
|
|
91
|
+
// Default blocks directory: srcDir/lib/blocks
|
|
92
|
+
blocksDir: join5(srcDir, "lib", "blocks")
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function flattenTsPaths(paths) {
|
|
96
|
+
const result = {};
|
|
97
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
98
|
+
if (targets[0]) {
|
|
99
|
+
result[alias.replace("/*", "/")] = targets[0].replace("/*", "/");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/ui/theme.ts
|
|
106
|
+
import pc from "picocolors";
|
|
107
|
+
var theme = {
|
|
108
|
+
brand: {
|
|
109
|
+
primary: pc.blue,
|
|
110
|
+
title: pc.bold,
|
|
111
|
+
logo: pc.white
|
|
112
|
+
},
|
|
113
|
+
text: {
|
|
114
|
+
normal: pc.white,
|
|
115
|
+
muted: pc.dim,
|
|
116
|
+
subtle: pc.gray
|
|
117
|
+
},
|
|
118
|
+
state: {
|
|
119
|
+
success: pc.green,
|
|
120
|
+
warning: pc.yellow,
|
|
121
|
+
error: pc.red,
|
|
122
|
+
info: pc.blue
|
|
123
|
+
},
|
|
124
|
+
emphasis: {
|
|
125
|
+
strong: pc.bold,
|
|
126
|
+
dim: pc.dim
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/ui/format.ts
|
|
131
|
+
var format = {
|
|
132
|
+
title: (text2) => theme.brand.primary(theme.brand.title(text2)),
|
|
133
|
+
success: (text2) => theme.state.success(`\u2714 ${text2}`),
|
|
134
|
+
error: (text2) => theme.state.error(`\u2716 ${text2}`),
|
|
135
|
+
warning: (text2) => theme.state.warning(text2),
|
|
136
|
+
muted: (text2) => theme.text.muted(text2),
|
|
137
|
+
info: (text2) => theme.state.info(text2)
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/commands/init.ts
|
|
141
|
+
async function initCommand() {
|
|
142
|
+
const cwd = process.cwd();
|
|
143
|
+
const configPath = join6(cwd, "blockend.json");
|
|
144
|
+
console.log(`
|
|
145
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
146
|
+
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
147
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
148
|
+
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
149
|
+
\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
|
+
\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
|
+
`);
|
|
152
|
+
intro(format.title("Blockend \xB7 Intelligent Backend Blocks Setup"));
|
|
153
|
+
if (existsSync4(configPath)) {
|
|
154
|
+
const action = await select({
|
|
155
|
+
message: "blockend.json already exists. What do you want to do?",
|
|
156
|
+
options: [
|
|
157
|
+
{ value: "keep", label: "Keep existing config (cancel init)" },
|
|
158
|
+
{ value: "overwrite", label: "Overwrite config" },
|
|
159
|
+
{ value: "regenerate", label: "Delete and regenerate" }
|
|
160
|
+
]
|
|
161
|
+
});
|
|
162
|
+
if (isCancel(action) || action === "keep") {
|
|
163
|
+
outro(format.muted("Initialization cancelled. Existing config preserved."));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (action === "regenerate") {
|
|
167
|
+
await fs.unlink(configPath);
|
|
168
|
+
console.log(format.error("Existing config deleted"));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const s = spinner();
|
|
172
|
+
s.start("Scanning project structure...");
|
|
173
|
+
const context = await detectProject(cwd);
|
|
174
|
+
s.stop(format.success("Project structure detected"));
|
|
175
|
+
s.start("Analyzing framework signals...");
|
|
176
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
177
|
+
s.stop(format.success(`Detected ${context.framework || "unknown"} environment`));
|
|
178
|
+
s.start("Resolving alias mappings...");
|
|
179
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
180
|
+
s.stop(format.success("Import strategy mapped"));
|
|
181
|
+
console.log(
|
|
182
|
+
format.muted(
|
|
183
|
+
`System: ${context.framework || "unknown"} \xB7 ${context.language || "ts"} \xB7 aliases=${Object.keys(context.aliasMap || {}).length}`
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
const framework = await select({
|
|
187
|
+
message: "Confirm framework environment",
|
|
188
|
+
initialValue: context.framework || "express",
|
|
189
|
+
options: [
|
|
190
|
+
{ value: "express", label: "Express.js" },
|
|
191
|
+
{ value: "fastify", label: "Fastify" },
|
|
192
|
+
{ value: "next", label: "Next.js (App Router)" },
|
|
193
|
+
{ value: "hono", label: "Hono" }
|
|
194
|
+
]
|
|
195
|
+
});
|
|
196
|
+
if (isCancel(framework)) {
|
|
197
|
+
outro(format.muted("Initialization cancelled."));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
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
|
+
const availableAliases = Object.keys(context.aliasMap || {});
|
|
213
|
+
const baseAliasToken = availableAliases.length > 0 ? availableAliases[0] : "@/";
|
|
214
|
+
const blockAliasInput = await text({
|
|
215
|
+
message: "Configure blocks import alias",
|
|
216
|
+
initialValue: `${baseAliasToken}blocks`,
|
|
217
|
+
placeholder: `${baseAliasToken}blocks`
|
|
218
|
+
});
|
|
219
|
+
if (isCancel(blockAliasInput)) {
|
|
220
|
+
outro(format.muted("Initialization cancelled."));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const blockAlias = String(blockAliasInput);
|
|
224
|
+
const aliasPrefix = availableAliases.find((a) => blockAlias.startsWith(a)) || "";
|
|
225
|
+
const physicalPrefix = aliasPrefix ? context.aliasMap[aliasPrefix] : "./";
|
|
226
|
+
const resolvedSubDir = blockAlias.replace(aliasPrefix, "");
|
|
227
|
+
const assumedPhysicalDir = join6(physicalPrefix, resolvedSubDir);
|
|
228
|
+
const relativeBlocksPath = path.relative(cwd, path.resolve(cwd, assumedPhysicalDir));
|
|
229
|
+
const normalizedPath = relativeBlocksPath.replace(/\\/g, "/");
|
|
230
|
+
const finalPath = normalizedPath.startsWith(".") ? normalizedPath : `./${normalizedPath}`;
|
|
231
|
+
let includeRedis = false;
|
|
232
|
+
if (context.hasRedis) {
|
|
233
|
+
const redisConfirm = await confirm({
|
|
234
|
+
message: "Redis detected. Enable Redis-backed block variants automatically?",
|
|
235
|
+
initialValue: true
|
|
236
|
+
});
|
|
237
|
+
if (!isCancel(redisConfirm)) {
|
|
238
|
+
includeRedis = Boolean(redisConfirm);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const configPayload = {
|
|
242
|
+
$schema: "https://blockend.dev/schema.json",
|
|
243
|
+
environment: framework,
|
|
244
|
+
language,
|
|
245
|
+
includeRedis,
|
|
246
|
+
aliases: {
|
|
247
|
+
blocks: blockAlias
|
|
248
|
+
},
|
|
249
|
+
paths: {
|
|
250
|
+
blocks: finalPath
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const writeSpinner = spinner();
|
|
254
|
+
writeSpinner.start("Finalizing configuration...");
|
|
255
|
+
try {
|
|
256
|
+
await fs.writeFile(configPath, JSON.stringify(configPayload, null, 2), "utf-8");
|
|
257
|
+
writeSpinner.stop(format.success("blockend.json ready"));
|
|
258
|
+
outro(format.title("Blockend initialized successfully. Run: npx blockend add <block>"));
|
|
259
|
+
} catch {
|
|
260
|
+
writeSpinner.stop(format.error("Failed to write configuration"));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/commands/add.ts
|
|
265
|
+
import path2, { join as join7 } from "path";
|
|
266
|
+
import fs2 from "fs/promises";
|
|
267
|
+
import { execSync } from "child_process";
|
|
268
|
+
import { intro as intro2, outro as outro2, select as select2, spinner as spinner2, confirm as confirm2 } from "@clack/prompts";
|
|
269
|
+
import pc2 from "picocolors";
|
|
270
|
+
var REPO_OWNER = "codewitnuh";
|
|
271
|
+
var REPO_NAME = "blockend";
|
|
272
|
+
var BRANCH = "master";
|
|
273
|
+
var RAW_CDN_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}`;
|
|
274
|
+
var MANIFEST_URL = `${RAW_CDN_BASE}/registry/index.json`;
|
|
275
|
+
async function addCommand(blockName) {
|
|
276
|
+
intro2(pc2.bgBlack(pc2.magenta(" Blockend Component Ingestion ")));
|
|
277
|
+
const cwd = process.cwd();
|
|
278
|
+
const configPath = join7(cwd, "blockend.json");
|
|
279
|
+
let config;
|
|
280
|
+
try {
|
|
281
|
+
const configFile = await fs2.readFile(configPath, "utf-8");
|
|
282
|
+
config = JSON.parse(configFile);
|
|
283
|
+
} catch {
|
|
284
|
+
outro2(pc2.red("\u2716 blockend.json not found. Run 'npx blockend init' first."));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const s = spinner2();
|
|
288
|
+
s.start("Connecting to remote Blockend Registry manifest...");
|
|
289
|
+
let registry;
|
|
290
|
+
try {
|
|
291
|
+
const response = await fetch(MANIFEST_URL);
|
|
292
|
+
if (!response.ok) throw new Error(`HTTP Error Status: ${response.status}`);
|
|
293
|
+
registry = await response.json();
|
|
294
|
+
s.stop(pc2.green("\u2714 Remote manifest synchronization complete."));
|
|
295
|
+
} catch {
|
|
296
|
+
s.stop(pc2.red("\u2716 Failed to fetch the component registry from GitHub network paths."));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
let targetBlock = blockName;
|
|
300
|
+
if (!targetBlock) {
|
|
301
|
+
const availableOptions = Object.keys(registry).map((key) => ({
|
|
302
|
+
value: key,
|
|
303
|
+
label: `${key} - pc.dim(${registry[key].description})`
|
|
304
|
+
}));
|
|
305
|
+
if (availableOptions.length === 0) {
|
|
306
|
+
outro2(pc2.yellow("\u26A0 The remote block registry is currently empty."));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
targetBlock = await select2({
|
|
310
|
+
message: "Which backend block would you like to inject?",
|
|
311
|
+
options: availableOptions
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const blockMeta = registry[targetBlock];
|
|
315
|
+
if (!blockMeta) {
|
|
316
|
+
outro2(pc2.red(`\u2716 Block "${targetBlock}" does not exist in the remote registry.`));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const envKey = String(config.environment);
|
|
320
|
+
const envConfig = blockMeta.environments[envKey];
|
|
321
|
+
if (!envConfig) {
|
|
322
|
+
outro2(
|
|
323
|
+
pc2.red(
|
|
324
|
+
`\u2716 The block "${targetBlock}" does not support your environment layout: ${String(config.environment)}`
|
|
325
|
+
)
|
|
326
|
+
);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
let packageJson;
|
|
330
|
+
try {
|
|
331
|
+
packageJson = JSON.parse(await fs2.readFile(join7(cwd, "package.json"), "utf-8"));
|
|
332
|
+
} catch {
|
|
333
|
+
outro2(pc2.red("\u2716 Could not locate package.json in your current workspace directory."));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const installedDeps = {
|
|
337
|
+
...packageJson.dependencies ?? {},
|
|
338
|
+
...packageJson.devDependencies ?? {}
|
|
339
|
+
};
|
|
340
|
+
const missingDeps = envConfig.dependencies.filter((dep) => !(dep in installedDeps));
|
|
341
|
+
if (missingDeps.length > 0) {
|
|
342
|
+
console.log(
|
|
343
|
+
pc2.yellow(`
|
|
344
|
+
\u26A0\uFE0F Missing required infrastructure packages: ${missingDeps.join(", ")}`)
|
|
345
|
+
);
|
|
346
|
+
const shouldInstall = await confirm2({
|
|
347
|
+
message: "Would you like the CLI to automatically install these dependencies?",
|
|
348
|
+
initialValue: true
|
|
349
|
+
});
|
|
350
|
+
if (shouldInstall) {
|
|
351
|
+
s.start(`Installing dependencies via your native package manager...`);
|
|
352
|
+
try {
|
|
353
|
+
const lockfileCheck = await fs2.access(join7(cwd, "pnpm-lock.yaml")).then(() => "pnpm").catch(() => "npm");
|
|
354
|
+
const installCmd = lockfileCheck === "pnpm" ? `pnpm add ${missingDeps.join(" ")}` : `npm install ${missingDeps.join(" ")}`;
|
|
355
|
+
execSync(installCmd, { stdio: "ignore", cwd });
|
|
356
|
+
s.stop(pc2.green("\u2714 Dependencies installed cleanly."));
|
|
357
|
+
} catch {
|
|
358
|
+
s.stop(pc2.red("\u2716 Automated dependency installation failed. Please run setup manually."));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
s.start(`Downloading clean production template block [${targetBlock}]...`);
|
|
363
|
+
try {
|
|
364
|
+
const targetFileUrl = `${RAW_CDN_BASE}/${envConfig.path}`;
|
|
365
|
+
const codeFetchResponse = await fetch(targetFileUrl);
|
|
366
|
+
if (!codeFetchResponse.ok) {
|
|
367
|
+
throw new Error(`Failed downloading component source file: ${codeFetchResponse.statusText}`);
|
|
368
|
+
}
|
|
369
|
+
const targetCodeTemplate = await codeFetchResponse.text();
|
|
370
|
+
let physicalPath = config.paths.blocks;
|
|
371
|
+
if (physicalPath.startsWith("@")) {
|
|
372
|
+
physicalPath = "./src/blocks";
|
|
373
|
+
}
|
|
374
|
+
const targetFolder = path2.resolve(cwd, physicalPath);
|
|
375
|
+
await fs2.mkdir(targetFolder, { recursive: true });
|
|
376
|
+
const fileExtension = config.language === "typescript" ? "ts" : "js";
|
|
377
|
+
const outputFilename = `${targetBlock}.${fileExtension}`;
|
|
378
|
+
await fs2.writeFile(join7(targetFolder, outputFilename), targetCodeTemplate, "utf-8");
|
|
379
|
+
const cleanDisplayPath = physicalPath.replace(/\\/g, "/");
|
|
380
|
+
s.stop(pc2.green(`\u2714 ${outputFilename} successfully injected into codebase.`));
|
|
381
|
+
outro2(
|
|
382
|
+
pc2.cyan(
|
|
383
|
+
`\u2728 Source blocks written to ${cleanDisplayPath}/${outputFilename}. Code ownership transferred!`
|
|
384
|
+
)
|
|
385
|
+
);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
s.stop(pc2.red("\u2716 Fatal crash occurred while downloading or transferring file layouts."));
|
|
388
|
+
console.error(error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/index.ts
|
|
393
|
+
var program = new Command();
|
|
394
|
+
program.name("blockend").description("Zero-dependency production backend blocks CLI").version("0.1.0");
|
|
395
|
+
program.command("init").description("Initialize blockend configuration in your project").action(async () => await initCommand());
|
|
396
|
+
program.command("add [block]").description("Inject unstyled, clean backend blocks directly into code paths").action(async (block) => {
|
|
397
|
+
await addCommand(block);
|
|
398
|
+
});
|
|
399
|
+
program.parse(process.argv);
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
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);
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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}}";
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blockend-cli",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Intelligent, modular backend blocks right inside your terminal",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"blockend": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"readme.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup src/index.ts --format esm --dts --out-dir dist"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cli",
|
|
19
|
+
"backend",
|
|
20
|
+
"express",
|
|
21
|
+
"nextjs",
|
|
22
|
+
"architecture"
|
|
23
|
+
],
|
|
24
|
+
"author": "codewithnuh",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"packageManager": "pnpm@10.33.2",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@clack/prompts": "^1.5.1",
|
|
29
|
+
"commander": "^15.0.0",
|
|
30
|
+
"picocolors": "^1.1.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.9.3"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# blockend-cli 🚀
|
|
2
|
+
|
|
3
|
+
Generate reusable backend code blocks directly from your terminal. Instead of rewriting common middleware, utilities, and configuration files, add them to your project with a single command.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Initialize Blockend in your project:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx blockend-cli init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Add Code Blocks
|
|
14
|
+
|
|
15
|
+
Add backend components to your project:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx blockend-cli add
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **No installation required:** Run it anytime with `npx`.
|
|
24
|
+
- **Works with your project:** Generates code that matches your framework (Express, Next.js, Hono, or Fastify).
|
|
25
|
+
- **Handles dependencies:** Installs required packages when needed.
|
|
26
|
+
- **Your code stays yours:** Writes plain source files directly into your project with no hidden runtime.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
ISC
|