blockend-cli 1.3.1 → 1.4.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/LICENSE +21 -0
- package/dist/index.js +974 -298
- package/package.json +62 -17
- package/readme.md +53 -10
package/dist/index.js
CHANGED
|
@@ -1,37 +1,395 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
4
|
+
import { defineCommand, runMain } from "citty";
|
|
5
5
|
|
|
6
|
-
// src/commands/
|
|
7
|
-
import path, { join } from "path";
|
|
6
|
+
// src/commands/add.ts
|
|
7
|
+
import path, { join, dirname } from "path";
|
|
8
8
|
import fs from "fs/promises";
|
|
9
|
+
import { exec } from "child_process";
|
|
10
|
+
import { intro, outro, select, spinner, confirm, isCancel } from "@clack/prompts";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
var REPO_OWNER = "codewithnuh";
|
|
13
|
+
var REPO_NAME = "blockend";
|
|
14
|
+
var BRANCH = "master";
|
|
15
|
+
var RAW_CDN_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}`;
|
|
16
|
+
var MANIFEST_URL = `${RAW_CDN_BASE}/registry/index.json`;
|
|
17
|
+
function outputError(json, message) {
|
|
18
|
+
if (json) {
|
|
19
|
+
process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
|
|
20
|
+
} else {
|
|
21
|
+
outro(pc.red(`\u2716 ${message}`));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function outputResult(json, result) {
|
|
25
|
+
if (json) {
|
|
26
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
27
|
+
} else {
|
|
28
|
+
if (result.success) {
|
|
29
|
+
outro(pc.cyan(`\u2728 ${result.message}`));
|
|
30
|
+
} else {
|
|
31
|
+
outro(pc.yellow(`\u2139 ${result.message}`));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function handleCancel(value) {
|
|
36
|
+
if (isCancel(value)) {
|
|
37
|
+
outro(pc.yellow("\u26A0 Operation cancelled. Exiting Blockend CLI cleanly."));
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function findUp(filename, startDir) {
|
|
42
|
+
let dir = startDir;
|
|
43
|
+
while (true) {
|
|
44
|
+
const checkPath = join(dir, filename);
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(checkPath);
|
|
47
|
+
return checkPath;
|
|
48
|
+
} catch {
|
|
49
|
+
const parent = dirname(dir);
|
|
50
|
+
if (parent === dir) break;
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
async function addCommand(blockName, options = {}) {
|
|
57
|
+
const { yes = false, json = false } = options;
|
|
58
|
+
if (!json) {
|
|
59
|
+
intro(pc.bgBlack(pc.magenta(" Blockend Component Ingestion ")));
|
|
60
|
+
}
|
|
61
|
+
const cwd = process.cwd();
|
|
62
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
63
|
+
if (!configPath) {
|
|
64
|
+
outputError(json, "blockend.json not found. Run 'npx blockend init' first.");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const rootDir = dirname(configPath);
|
|
68
|
+
let config;
|
|
69
|
+
try {
|
|
70
|
+
const configFile = await fs.readFile(configPath, "utf-8");
|
|
71
|
+
config = JSON.parse(configFile);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
outputError(json, "Failed to parse blockend.json layout configuration.");
|
|
74
|
+
if (!json) console.error(pc.dim(String(error)));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (config.language !== "typescript") {
|
|
78
|
+
if (json) {
|
|
79
|
+
outputError(
|
|
80
|
+
json,
|
|
81
|
+
"Blockend forces modern architectural standards. Registry exclusively supports TypeScript."
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
outro(
|
|
85
|
+
pc.yellow(
|
|
86
|
+
`
|
|
87
|
+
\u2139 Blockend forces modern architectural standards. Registry exclusively supports TypeScript.`
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const s = spinner();
|
|
94
|
+
if (!json) {
|
|
95
|
+
s.start("Connecting to remote Blockend Registry manifest...");
|
|
96
|
+
}
|
|
97
|
+
let registry;
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetch(MANIFEST_URL);
|
|
100
|
+
if (!response.ok) throw new Error(`HTTP Error Status: ${response.status}`);
|
|
101
|
+
registry = await response.json();
|
|
102
|
+
if (!json) s.stop(pc.green("\u2714 Remote manifest synchronization complete."));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (!json) {
|
|
105
|
+
s.stop(pc.red("\u2716 Failed to fetch the component registry from GitHub network paths."));
|
|
106
|
+
console.error(pc.dim(String(error)));
|
|
107
|
+
} else {
|
|
108
|
+
outputError(json, "Failed to fetch the component registry from GitHub network paths.");
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let blockMap = {};
|
|
113
|
+
if (registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks)) {
|
|
114
|
+
blockMap = registry.blocks;
|
|
115
|
+
} else {
|
|
116
|
+
blockMap = registry;
|
|
117
|
+
}
|
|
118
|
+
const envKey = String(config.environment);
|
|
119
|
+
let targetBlock = blockName;
|
|
120
|
+
if (yes && !targetBlock) {
|
|
121
|
+
outputError(
|
|
122
|
+
json,
|
|
123
|
+
"Block name required when using --yes flag. Usage: blockend add <block> --yes"
|
|
124
|
+
);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!targetBlock) {
|
|
128
|
+
const availableOptions = Object.entries(blockMap).filter(([_, block]) => {
|
|
129
|
+
if (!block) return false;
|
|
130
|
+
const hasAdapter = block.adapters?.[envKey] !== void 0;
|
|
131
|
+
const hasEnvironment = block.environments?.[envKey] !== void 0;
|
|
132
|
+
return hasAdapter || hasEnvironment;
|
|
133
|
+
}).map(([key, block]) => ({
|
|
134
|
+
value: key,
|
|
135
|
+
label: `${key} - ${pc.dim(block.description)}`
|
|
136
|
+
}));
|
|
137
|
+
if (availableOptions.length === 0) {
|
|
138
|
+
if (json) {
|
|
139
|
+
outputError(
|
|
140
|
+
json,
|
|
141
|
+
`No backend blocks are currently available for your framework layer: [${envKey}].`
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
outro(
|
|
145
|
+
pc.yellow(
|
|
146
|
+
`\u26A0 No backend blocks are currently available for your framework layer: [${envKey}].`
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const selectBlockPrompt = await select({
|
|
153
|
+
message: "Which backend block would you like to inject?",
|
|
154
|
+
options: availableOptions
|
|
155
|
+
});
|
|
156
|
+
handleCancel(selectBlockPrompt);
|
|
157
|
+
targetBlock = selectBlockPrompt;
|
|
158
|
+
}
|
|
159
|
+
const blockMeta = blockMap[targetBlock];
|
|
160
|
+
if (!blockMeta) {
|
|
161
|
+
outputError(json, `Block "${targetBlock}" does not exist in the remote registry.`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const adapterContext = blockMeta.adapters?.[envKey] ?? blockMeta.environments?.[envKey];
|
|
165
|
+
if (!adapterContext) {
|
|
166
|
+
outputError(
|
|
167
|
+
json,
|
|
168
|
+
`The block "${targetBlock}" does not support your environment layout: ${envKey}`
|
|
169
|
+
);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
let selectedVariant;
|
|
173
|
+
const variantKeys = Object.keys(adapterContext.variants || {});
|
|
174
|
+
if (variantKeys.length === 0) {
|
|
175
|
+
outputError(
|
|
176
|
+
json,
|
|
177
|
+
`No architecture storage layout variants found for block framework: ${envKey}`
|
|
178
|
+
);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (yes) {
|
|
182
|
+
selectedVariant = variantKeys.includes("memory") ? "memory" : variantKeys[0];
|
|
183
|
+
} else if (variantKeys.includes("redis") && config.redisEnabled) {
|
|
184
|
+
selectedVariant = "redis";
|
|
185
|
+
} else if (variantKeys.length === 1) {
|
|
186
|
+
selectedVariant = variantKeys[0];
|
|
187
|
+
} else {
|
|
188
|
+
const selectVariantPrompt = await select({
|
|
189
|
+
message: "Which architectural storage variant do you want to back this block?",
|
|
190
|
+
options: variantKeys.map((vKey) => ({
|
|
191
|
+
value: vKey,
|
|
192
|
+
label: vKey.toUpperCase()
|
|
193
|
+
}))
|
|
194
|
+
});
|
|
195
|
+
handleCancel(selectVariantPrompt);
|
|
196
|
+
selectedVariant = selectVariantPrompt;
|
|
197
|
+
}
|
|
198
|
+
const variantMeta = adapterContext.variants[selectedVariant];
|
|
199
|
+
let physicalPath = config.paths.blocks;
|
|
200
|
+
if (physicalPath.startsWith("@")) {
|
|
201
|
+
physicalPath = "./src/blocks";
|
|
202
|
+
}
|
|
203
|
+
const targetFolder = path.resolve(rootDir, physicalPath, targetBlock);
|
|
204
|
+
const packageJsonPath = await findUp("package.json", rootDir);
|
|
205
|
+
if (!packageJsonPath) {
|
|
206
|
+
outputError(json, "Could not locate package.json in your current directory layout hierarchy.");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const packageJsonDir = dirname(packageJsonPath);
|
|
210
|
+
let packageJson;
|
|
211
|
+
try {
|
|
212
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
|
|
213
|
+
packageJson = JSON.parse(packageJsonContent);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
outputError(json, "Failed parsing file data configurations from target package.json location.");
|
|
216
|
+
if (!json) console.error(pc.dim(String(error)));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const installedDeps = {
|
|
220
|
+
...packageJson.dependencies ?? {},
|
|
221
|
+
...packageJson.devDependencies ?? {}
|
|
222
|
+
};
|
|
223
|
+
const missingProdDeps = (adapterContext.dependencies ?? []).concat(variantMeta?.dependencies ?? []).filter((dep) => !(dep in installedDeps));
|
|
224
|
+
const missingDevDeps = (adapterContext.devDependencies ?? []).concat(variantMeta?.devDependencies ?? []).filter((dep) => !(dep in installedDeps));
|
|
225
|
+
const hasMissingDeps = missingProdDeps.length > 0 || missingDevDeps.length > 0;
|
|
226
|
+
if (hasMissingDeps) {
|
|
227
|
+
const allMissingNames = [...missingProdDeps, ...missingDevDeps];
|
|
228
|
+
if (!json) {
|
|
229
|
+
console.log(
|
|
230
|
+
pc.yellow(`
|
|
231
|
+
\u26A0\uFE0F Missing required infrastructure packages: ${allMissingNames.join(", ")}`)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const shouldInstallPrompt = yes ? true : await confirm({
|
|
235
|
+
message: "Would you like the CLI to automatically install these dependencies?",
|
|
236
|
+
initialValue: true
|
|
237
|
+
});
|
|
238
|
+
if (!yes) handleCancel(shouldInstallPrompt);
|
|
239
|
+
if (shouldInstallPrompt) {
|
|
240
|
+
const packageManager = await fs.access(join(packageJsonDir, "pnpm-lock.yaml")).then(() => "pnpm").catch(() => "npm");
|
|
241
|
+
if (!json) s.start(`Preparing native workspace via ${packageManager}...`);
|
|
242
|
+
try {
|
|
243
|
+
const installTasks = [];
|
|
244
|
+
if (missingProdDeps.length > 0) {
|
|
245
|
+
installTasks.push(
|
|
246
|
+
packageManager === "pnpm" ? `pnpm add ${missingProdDeps.join(" ")}` : `npm install ${missingProdDeps.join(" ")}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (missingDevDeps.length > 0) {
|
|
250
|
+
installTasks.push(
|
|
251
|
+
packageManager === "pnpm" ? `pnpm add -D ${missingDevDeps.join(" ")}` : `npm install --save-dev ${missingDevDeps.join(" ")}`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
for (const installCmd of installTasks) {
|
|
255
|
+
if (!json) s.stop(pc.cyan(`Executing: ${installCmd}
|
|
256
|
+
`));
|
|
257
|
+
await new Promise((resolve, reject) => {
|
|
258
|
+
const child = exec(installCmd, { cwd: packageJsonDir });
|
|
259
|
+
child.stdout?.on("data", (data) => {
|
|
260
|
+
if (!json) process.stdout.write(pc.dim(data));
|
|
261
|
+
});
|
|
262
|
+
child.stderr?.on("data", (data) => {
|
|
263
|
+
if (!json) process.stdout.write(pc.dim(pc.red(data)));
|
|
264
|
+
});
|
|
265
|
+
child.on(
|
|
266
|
+
"close",
|
|
267
|
+
(code) => code === 0 ? resolve() : reject(new Error(`Exit code ${code}`))
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (!json) {
|
|
272
|
+
s.start(pc.green("\u2714 All dependencies synchronized successfully."));
|
|
273
|
+
s.stop();
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (!json) {
|
|
277
|
+
s.stop(pc.red("\u2716 Automated dependency installation failed. Please run setup manually."));
|
|
278
|
+
console.error(pc.dim(String(error)));
|
|
279
|
+
} else {
|
|
280
|
+
outputError(json, "Automated dependency installation failed.");
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const filesToDownload = [];
|
|
287
|
+
if (blockMeta.baseFiles) {
|
|
288
|
+
filesToDownload.push(...blockMeta.baseFiles);
|
|
289
|
+
}
|
|
290
|
+
if (adapterContext.core) {
|
|
291
|
+
filesToDownload.push({
|
|
292
|
+
source: adapterContext.core,
|
|
293
|
+
target: path.basename(adapterContext.core, ".txt")
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
297
|
+
for (const fileEntry of variantMeta.files) {
|
|
298
|
+
if (typeof fileEntry === "string") {
|
|
299
|
+
filesToDownload.push({
|
|
300
|
+
source: fileEntry,
|
|
301
|
+
target: path.basename(fileEntry, ".txt")
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
filesToDownload.push(fileEntry);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
let fileExistsConflict = false;
|
|
309
|
+
for (const fileMap of filesToDownload) {
|
|
310
|
+
const destinationPath = join(targetFolder, fileMap.target);
|
|
311
|
+
try {
|
|
312
|
+
await fs.access(destinationPath);
|
|
313
|
+
fileExistsConflict = true;
|
|
314
|
+
break;
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (fileExistsConflict) {
|
|
319
|
+
const overwritePrompt = yes ? true : await confirm({
|
|
320
|
+
message: `\u26A0 Components in [${targetBlock}] already exist. Overwrite custom revisions?`,
|
|
321
|
+
initialValue: false
|
|
322
|
+
});
|
|
323
|
+
if (!yes) handleCancel(overwritePrompt);
|
|
324
|
+
if (!overwritePrompt) {
|
|
325
|
+
outputResult(json, {
|
|
326
|
+
success: false,
|
|
327
|
+
reason: "aborted",
|
|
328
|
+
message: "Local modifications preserved."
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!json) {
|
|
334
|
+
s.start(`Downloading and building template adapter blocks [${targetBlock}]...`);
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
for (const fileMap of filesToDownload) {
|
|
338
|
+
const fileUrl = `${RAW_CDN_BASE}/${fileMap.source}`;
|
|
339
|
+
const response = await fetch(fileUrl);
|
|
340
|
+
if (!response.ok) throw new Error(`Download failed for: ${fileMap.source}`);
|
|
341
|
+
const fileContent = await response.text();
|
|
342
|
+
const localWriteLocation = join(targetFolder, fileMap.target);
|
|
343
|
+
await fs.mkdir(dirname(localWriteLocation), { recursive: true });
|
|
344
|
+
await fs.writeFile(localWriteLocation, fileContent, "utf-8");
|
|
345
|
+
}
|
|
346
|
+
if (!json) s.stop(pc.green("\u2714 Component isolation structures written smoothly."));
|
|
347
|
+
outputResult(json, {
|
|
348
|
+
success: true,
|
|
349
|
+
block: targetBlock,
|
|
350
|
+
filesWritten: filesToDownload.map((f) => join(targetFolder, f.target)),
|
|
351
|
+
dependenciesInstalled: [...missingProdDeps, ...missingDevDeps],
|
|
352
|
+
message: `Source blocks written to ${physicalPath}/${targetBlock}/. Code ownership transferred!`
|
|
353
|
+
});
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (!json) {
|
|
356
|
+
s.stop(pc.red("\u2716 Fatal error occurred while assembling file mappings."));
|
|
357
|
+
console.error(pc.dim(String(error)));
|
|
358
|
+
} else {
|
|
359
|
+
outputError(json, "Fatal error occurred while assembling file mappings.");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/commands/init.ts
|
|
365
|
+
import path2, { join as join2 } from "path";
|
|
366
|
+
import fs2 from "fs/promises";
|
|
9
367
|
import { existsSync } from "fs";
|
|
10
|
-
import { intro, outro, select, text, confirm, spinner, isCancel } from "@clack/prompts";
|
|
368
|
+
import { intro as intro2, outro as outro2, select as select2, text, confirm as confirm2, spinner as spinner2, isCancel as isCancel2 } from "@clack/prompts";
|
|
11
369
|
import { detectProject } from "@blockend/detector";
|
|
12
370
|
|
|
13
371
|
// src/ui/theme.ts
|
|
14
|
-
import
|
|
372
|
+
import pc2 from "picocolors";
|
|
15
373
|
var theme = {
|
|
16
374
|
brand: {
|
|
17
|
-
primary:
|
|
18
|
-
title:
|
|
19
|
-
logo:
|
|
375
|
+
primary: pc2.blue,
|
|
376
|
+
title: pc2.bold,
|
|
377
|
+
logo: pc2.white
|
|
20
378
|
},
|
|
21
379
|
text: {
|
|
22
|
-
normal:
|
|
23
|
-
muted:
|
|
24
|
-
subtle:
|
|
380
|
+
normal: pc2.white,
|
|
381
|
+
muted: pc2.dim,
|
|
382
|
+
subtle: pc2.gray
|
|
25
383
|
},
|
|
26
384
|
state: {
|
|
27
|
-
success:
|
|
28
|
-
warning:
|
|
29
|
-
error:
|
|
30
|
-
info:
|
|
385
|
+
success: pc2.green,
|
|
386
|
+
warning: pc2.yellow,
|
|
387
|
+
error: pc2.red,
|
|
388
|
+
info: pc2.blue
|
|
31
389
|
},
|
|
32
390
|
emphasis: {
|
|
33
|
-
strong:
|
|
34
|
-
dim:
|
|
391
|
+
strong: pc2.bold,
|
|
392
|
+
dim: pc2.dim
|
|
35
393
|
}
|
|
36
394
|
};
|
|
37
395
|
|
|
@@ -46,372 +404,690 @@ var format = {
|
|
|
46
404
|
};
|
|
47
405
|
|
|
48
406
|
// src/commands/init.ts
|
|
49
|
-
async function
|
|
407
|
+
async function resolveTsConfigPaths(cwd) {
|
|
408
|
+
const possiblePaths = [join2(cwd, "tsconfig.json"), join2(cwd, "jsconfig.json")];
|
|
409
|
+
for (const configPath of possiblePaths) {
|
|
410
|
+
if (existsSync(configPath)) {
|
|
411
|
+
try {
|
|
412
|
+
const rawContent = await fs2.readFile(configPath, "utf-8");
|
|
413
|
+
const cleanJsonContent = rawContent.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
|
|
414
|
+
const parsed = JSON.parse(cleanJsonContent);
|
|
415
|
+
const baseUrl = parsed.compilerOptions?.baseUrl || ".";
|
|
416
|
+
const paths = parsed.compilerOptions?.paths || {};
|
|
417
|
+
return { baseUrl, paths };
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { baseUrl: ".", paths: {} };
|
|
423
|
+
}
|
|
424
|
+
function outputInitError(json, message) {
|
|
425
|
+
if (json) {
|
|
426
|
+
process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
|
|
427
|
+
} else {
|
|
428
|
+
outro2(format.error(message));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function outputInitResult(json, result) {
|
|
432
|
+
if (json) {
|
|
433
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
434
|
+
} else {
|
|
435
|
+
outro2(theme.state.success(result.message));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function initCommand(options = {}) {
|
|
439
|
+
const { yes = false, json = false } = options;
|
|
50
440
|
const cwd = process.cwd();
|
|
51
|
-
const configPath =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
\u2588\u2588\
|
|
55
|
-
\u2588\u2588\
|
|
56
|
-
\u2588\u2588\
|
|
441
|
+
const configPath = join2(cwd, "blockend.json");
|
|
442
|
+
if (!json) {
|
|
443
|
+
console.log(`
|
|
444
|
+
\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
|
|
445
|
+
\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
|
|
446
|
+
\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
|
|
447
|
+
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
57
448
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
|
|
58
449
|
\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D
|
|
59
450
|
`);
|
|
60
|
-
|
|
451
|
+
intro2(theme.brand.primary(" Blockend \xB7 Intelligent Backend Blocks Setup "));
|
|
452
|
+
}
|
|
61
453
|
if (existsSync(configPath)) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
454
|
+
let action;
|
|
455
|
+
if (yes) {
|
|
456
|
+
action = "overwrite";
|
|
457
|
+
} else {
|
|
458
|
+
const actionPrompt = await select2({
|
|
459
|
+
message: "blockend.json already exists. What do you want to do?",
|
|
460
|
+
options: [
|
|
461
|
+
{ value: "keep", label: "Keep existing config (cancel init)" },
|
|
462
|
+
{ value: "overwrite", label: "Overwrite config" },
|
|
463
|
+
{ value: "regenerate", label: "Delete and regenerate" }
|
|
464
|
+
]
|
|
465
|
+
});
|
|
466
|
+
if (isCancel2(actionPrompt) || actionPrompt === "keep") {
|
|
467
|
+
if (json) {
|
|
468
|
+
outputInitResult(json, {
|
|
469
|
+
success: false,
|
|
470
|
+
message: "Initialization cancelled. Existing config preserved."
|
|
471
|
+
});
|
|
472
|
+
} else {
|
|
473
|
+
outro2(format.muted("Initialization cancelled. Existing config preserved."));
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
action = actionPrompt;
|
|
73
478
|
}
|
|
74
479
|
if (action === "regenerate") {
|
|
75
|
-
await
|
|
76
|
-
console.log(format.error("Existing config deleted"));
|
|
480
|
+
await fs2.unlink(configPath);
|
|
481
|
+
if (!json) console.log(format.error("Existing config deleted"));
|
|
77
482
|
}
|
|
78
483
|
}
|
|
79
|
-
const s =
|
|
80
|
-
s.start("Scanning project
|
|
484
|
+
const s = spinner2();
|
|
485
|
+
if (!json) s.start("Scanning project layout...");
|
|
81
486
|
const context = await detectProject(cwd);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
487
|
+
const hasSrcDir = existsSync(join2(cwd, "src"));
|
|
488
|
+
const tsConfig = await resolveTsConfigPaths(cwd);
|
|
489
|
+
if (!json) s.stop(format.success("Project architecture scanned"));
|
|
490
|
+
let framework = context.framework;
|
|
491
|
+
if (!framework) {
|
|
492
|
+
if (yes) {
|
|
493
|
+
framework = "express";
|
|
494
|
+
} else {
|
|
495
|
+
const frameworkSelect = await select2({
|
|
496
|
+
message: "Framework could not be auto-detected. Select framework environment manually:",
|
|
497
|
+
options: [
|
|
498
|
+
{ value: "express", label: "Express.js" },
|
|
499
|
+
{ value: "fastify", label: "Fastify" },
|
|
500
|
+
{ value: "next", label: "Next.js (App Router)" },
|
|
501
|
+
{ value: "hono", label: "Hono" }
|
|
502
|
+
]
|
|
503
|
+
});
|
|
504
|
+
if (isCancel2(frameworkSelect)) {
|
|
505
|
+
if (json) {
|
|
506
|
+
outputInitResult(json, { success: false, message: "Initialization cancelled." });
|
|
507
|
+
} else {
|
|
508
|
+
outro2(format.muted("Initialization cancelled."));
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
framework = frameworkSelect;
|
|
513
|
+
}
|
|
514
|
+
} else if (!json) {
|
|
515
|
+
console.log(
|
|
516
|
+
`${format.success("\u2714")} Framework environment detected: ${theme.state.info(framework)}`
|
|
517
|
+
);
|
|
107
518
|
}
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
519
|
+
const defaultDir = hasSrcDir ? "src/blocks" : "blocks";
|
|
520
|
+
let rawPhysicalInput = defaultDir;
|
|
521
|
+
if (!yes) {
|
|
522
|
+
const directoryPrompt = await text({
|
|
523
|
+
message: "Configure the targeted physical directory destination for blocks:",
|
|
524
|
+
placeholder: defaultDir,
|
|
525
|
+
initialValue: defaultDir,
|
|
526
|
+
validate(value) {
|
|
527
|
+
if (value?.trim().length === 0) return "Physical path location directory cannot be empty.";
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
if (isCancel2(directoryPrompt)) {
|
|
531
|
+
outro2(format.muted("Initialization cancelled."));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
rawPhysicalInput = String(directoryPrompt).trim();
|
|
535
|
+
}
|
|
536
|
+
const relativeBlocksPath = path2.relative(cwd, path2.resolve(cwd, rawPhysicalInput));
|
|
537
|
+
let finalPath = relativeBlocksPath.replace(/\\/g, "/");
|
|
538
|
+
if (!finalPath.startsWith(".") && !finalPath.startsWith("/")) {
|
|
539
|
+
finalPath = `./${finalPath}`;
|
|
540
|
+
}
|
|
541
|
+
let finalBlockAlias = "@/blocks";
|
|
542
|
+
const configuredAliases = Object.keys(tsConfig.paths);
|
|
543
|
+
if (configuredAliases.length > 0) {
|
|
544
|
+
const matchedAliasKey = configuredAliases.find((alias) => {
|
|
545
|
+
const targets = tsConfig.paths[alias];
|
|
546
|
+
if (Array.isArray(targets) && targets.length > 0) {
|
|
547
|
+
const targetSubPath = targets[0].replace(/\*$/, "").replace(/\\/g, "/");
|
|
548
|
+
return finalPath.replace(/^\.\//, "").startsWith(targetSubPath);
|
|
549
|
+
}
|
|
550
|
+
return false;
|
|
551
|
+
});
|
|
552
|
+
if (matchedAliasKey) {
|
|
553
|
+
const cleanKey = matchedAliasKey.replace(/\*$/, "");
|
|
554
|
+
const targets = tsConfig.paths[matchedAliasKey];
|
|
555
|
+
const targetSubPath = targets[0].replace(/\*$/, "").replace(/\\/g, "/");
|
|
556
|
+
const relativeSuffix = finalPath.replace(/^\.\//, "").replace(targetSubPath, "");
|
|
557
|
+
finalBlockAlias = `${cleanKey}${relativeSuffix}`.replace(/\/$/, "");
|
|
558
|
+
} else {
|
|
559
|
+
const primaryAlias = configuredAliases.find((k) => k.startsWith("@")) || configuredAliases[0];
|
|
560
|
+
const inferredBaseAlias = primaryAlias.replace(/\*$/, "");
|
|
561
|
+
const folderSegmentName = path2.basename(finalPath);
|
|
562
|
+
finalBlockAlias = `${inferredBaseAlias}${folderSegmentName}`;
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
const folderSegmentName = path2.basename(finalPath);
|
|
566
|
+
finalBlockAlias = `@/${folderSegmentName}`;
|
|
118
567
|
}
|
|
119
|
-
const blockAlias = String(blockAliasInput);
|
|
120
|
-
const aliasPrefix = availableAliases.find((a) => blockAlias.startsWith(a)) || "";
|
|
121
|
-
const physicalPrefix = aliasPrefix ? context.aliasMap[aliasPrefix] : "./";
|
|
122
|
-
const resolvedSubDir = blockAlias.replace(aliasPrefix, "");
|
|
123
|
-
const assumedPhysicalDir = join(physicalPrefix, resolvedSubDir);
|
|
124
|
-
const relativeBlocksPath = path.relative(cwd, path.resolve(cwd, assumedPhysicalDir));
|
|
125
|
-
const normalizedPath = relativeBlocksPath.replace(/\\/g, "/");
|
|
126
|
-
const finalPath = normalizedPath.startsWith(".") ? normalizedPath : `./${normalizedPath}`;
|
|
127
568
|
let includeRedis = false;
|
|
128
569
|
if (context.hasRedis) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
570
|
+
if (yes) {
|
|
571
|
+
includeRedis = true;
|
|
572
|
+
} else {
|
|
573
|
+
const redisConfirm = await confirm2({
|
|
574
|
+
message: "Redis detected. Enable Redis-backed block variants automatically?",
|
|
575
|
+
initialValue: true
|
|
576
|
+
});
|
|
577
|
+
if (!isCancel2(redisConfirm)) {
|
|
578
|
+
includeRedis = Boolean(redisConfirm);
|
|
579
|
+
}
|
|
135
580
|
}
|
|
136
581
|
}
|
|
137
582
|
const configPayload = {
|
|
138
583
|
$schema: "https://blockend.dev/schema.json",
|
|
139
|
-
environment: framework,
|
|
140
|
-
language: "typescript",
|
|
584
|
+
environment: framework || "express",
|
|
585
|
+
language: context.language || "typescript",
|
|
141
586
|
includeRedis,
|
|
142
587
|
aliases: {
|
|
143
|
-
blocks:
|
|
588
|
+
blocks: finalBlockAlias
|
|
144
589
|
},
|
|
145
590
|
paths: {
|
|
146
591
|
blocks: finalPath
|
|
147
592
|
}
|
|
148
593
|
};
|
|
149
|
-
|
|
150
|
-
writeSpinner.start("Finalizing configuration...");
|
|
594
|
+
if (!json) s.start("Finalizing configuration...");
|
|
151
595
|
try {
|
|
152
|
-
await
|
|
153
|
-
|
|
154
|
-
|
|
596
|
+
await fs2.writeFile(configPath, JSON.stringify(configPayload, null, 2), "utf-8");
|
|
597
|
+
if (!json) s.stop(format.success("blockend.json ready"));
|
|
598
|
+
outputInitResult(json, {
|
|
599
|
+
success: true,
|
|
600
|
+
message: "\u2728 Blockend initialized successfully. Run: npx blockend add <block>",
|
|
601
|
+
config: configPayload
|
|
602
|
+
});
|
|
155
603
|
} catch {
|
|
156
|
-
|
|
604
|
+
if (!json) {
|
|
605
|
+
s.stop(format.error("Failed to write configuration"));
|
|
606
|
+
} else {
|
|
607
|
+
outputInitError(json, "Failed to write architectural layout configuration map.");
|
|
608
|
+
}
|
|
157
609
|
}
|
|
158
610
|
}
|
|
159
611
|
|
|
160
|
-
// src/commands/
|
|
161
|
-
import
|
|
162
|
-
import
|
|
163
|
-
import
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
612
|
+
// src/commands/detect.ts
|
|
613
|
+
import { detectProject as detectProject2 } from "@blockend/detector";
|
|
614
|
+
import { outro as outro3 } from "@clack/prompts";
|
|
615
|
+
import pc3 from "picocolors";
|
|
616
|
+
async function detectCommand(options = {}) {
|
|
617
|
+
const { json = false } = options;
|
|
618
|
+
try {
|
|
619
|
+
const context = await detectProject2(process.cwd());
|
|
620
|
+
if (json) {
|
|
621
|
+
process.stdout.write(JSON.stringify(context, null, 2) + "\n");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
console.log();
|
|
625
|
+
console.log(pc3.bold(" Detected project configuration:"));
|
|
626
|
+
console.log();
|
|
627
|
+
console.log(` Framework: ${pc3.cyan(context.framework)}`);
|
|
628
|
+
console.log(` Language: ${pc3.cyan(context.language)}`);
|
|
629
|
+
console.log(` Package manager: ${pc3.cyan(context.packageManager)}`);
|
|
630
|
+
console.log(` Source dir: ${pc3.dim(context.srcDir)}`);
|
|
631
|
+
console.log(
|
|
632
|
+
` Redis: ${context.hasRedis ? pc3.green("detected") : pc3.dim("not found")}`
|
|
633
|
+
);
|
|
634
|
+
console.log(
|
|
635
|
+
` Prisma: ${context.hasPrisma ? pc3.green("detected") : pc3.dim("not found")}`
|
|
636
|
+
);
|
|
637
|
+
console.log(
|
|
638
|
+
` Drizzle: ${context.hasDrizzle ? pc3.green("detected") : pc3.dim("not found")}`
|
|
639
|
+
);
|
|
640
|
+
console.log();
|
|
641
|
+
} catch (error) {
|
|
642
|
+
if (json) {
|
|
643
|
+
process.stdout.write(JSON.stringify({ success: false, error: String(error) }) + "\n");
|
|
644
|
+
} else {
|
|
645
|
+
outro3(pc3.red(`\u2716 Detection failed: ${String(error)}`));
|
|
646
|
+
}
|
|
647
|
+
process.exit(1);
|
|
175
648
|
}
|
|
176
649
|
}
|
|
177
|
-
|
|
650
|
+
|
|
651
|
+
// src/commands/list.ts
|
|
652
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
653
|
+
import fs3 from "fs/promises";
|
|
654
|
+
import pc4 from "picocolors";
|
|
655
|
+
import { outro as outro4, spinner as spinner3 } from "@clack/prompts";
|
|
656
|
+
var REPO_OWNER2 = "codewithnuh";
|
|
657
|
+
var REPO_NAME2 = "blockend";
|
|
658
|
+
var BRANCH2 = "master";
|
|
659
|
+
var MANIFEST_URL2 = `https://raw.githubusercontent.com/${REPO_OWNER2}/${REPO_NAME2}/${BRANCH2}/registry/index.json`;
|
|
660
|
+
async function findUp2(filename, startDir) {
|
|
178
661
|
let dir = startDir;
|
|
179
662
|
while (true) {
|
|
180
|
-
const checkPath =
|
|
663
|
+
const checkPath = join3(dir, filename);
|
|
181
664
|
try {
|
|
182
|
-
await
|
|
665
|
+
await fs3.access(checkPath);
|
|
183
666
|
return checkPath;
|
|
184
667
|
} catch {
|
|
185
|
-
const parent =
|
|
668
|
+
const parent = dirname2(dir);
|
|
186
669
|
if (parent === dir) break;
|
|
187
670
|
dir = parent;
|
|
188
671
|
}
|
|
189
672
|
}
|
|
190
673
|
return null;
|
|
191
674
|
}
|
|
192
|
-
async function
|
|
193
|
-
|
|
675
|
+
async function listCommand(options = {}) {
|
|
676
|
+
const { json = false } = options;
|
|
194
677
|
const cwd = process.cwd();
|
|
195
|
-
const configPath = await
|
|
678
|
+
const configPath = await findUp2("blockend.json", cwd);
|
|
196
679
|
if (!configPath) {
|
|
197
|
-
|
|
680
|
+
if (json) {
|
|
681
|
+
process.stdout.write(
|
|
682
|
+
JSON.stringify({ success: false, error: "blockend.json not found." }) + "\n"
|
|
683
|
+
);
|
|
684
|
+
} else {
|
|
685
|
+
outro4(pc4.red("\u2716 blockend.json not found. Run 'npx blockend init' first."));
|
|
686
|
+
}
|
|
198
687
|
return;
|
|
199
688
|
}
|
|
200
|
-
const rootDir = dirname(configPath);
|
|
201
689
|
let config;
|
|
202
690
|
try {
|
|
203
|
-
const configFile = await
|
|
691
|
+
const configFile = await fs3.readFile(configPath, "utf-8");
|
|
204
692
|
config = JSON.parse(configFile);
|
|
205
693
|
} catch {
|
|
206
|
-
|
|
694
|
+
if (json) {
|
|
695
|
+
process.stdout.write(
|
|
696
|
+
JSON.stringify({ success: false, error: "Failed to parse configuration matrix." }) + "\n"
|
|
697
|
+
);
|
|
698
|
+
} else {
|
|
699
|
+
outro4(pc4.red("\u2716 Failed to parse blockend.json layout configuration."));
|
|
700
|
+
}
|
|
207
701
|
return;
|
|
208
702
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
);
|
|
218
|
-
return;
|
|
703
|
+
const envKey = String(config.environment);
|
|
704
|
+
const s = spinner3();
|
|
705
|
+
if (!json) {
|
|
706
|
+
s.start("Syncing registry metadata blocks...");
|
|
219
707
|
}
|
|
220
|
-
const s = spinner2();
|
|
221
|
-
s.start("Connecting to remote Blockend Registry manifest...");
|
|
222
708
|
let registry;
|
|
223
709
|
try {
|
|
224
|
-
const response = await fetch(
|
|
225
|
-
if (!response.ok) throw new Error(`HTTP
|
|
710
|
+
const response = await fetch(MANIFEST_URL2);
|
|
711
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
226
712
|
registry = await response.json();
|
|
227
|
-
s.stop(
|
|
713
|
+
if (!json) s.stop(pc4.green("\u2714 Synced available block registries."));
|
|
228
714
|
} catch {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const envKey = String(config.environment);
|
|
233
|
-
let targetBlock = blockName;
|
|
234
|
-
if (!targetBlock) {
|
|
235
|
-
const availableOptions = Object.keys(registry).filter((key) => registry[key].environments[envKey] !== void 0).map((key) => ({
|
|
236
|
-
value: key,
|
|
237
|
-
label: `${key} - ${pc2.dim(registry[key].description)}`
|
|
238
|
-
}));
|
|
239
|
-
if (availableOptions.length === 0) {
|
|
240
|
-
outro2(
|
|
241
|
-
pc2.yellow(
|
|
242
|
-
`\u26A0 No backend blocks are currently available for your framework layer: [${envKey}].`
|
|
243
|
-
)
|
|
715
|
+
if (json) {
|
|
716
|
+
process.stdout.write(
|
|
717
|
+
JSON.stringify({ success: false, error: "Network sync failure." }) + "\n"
|
|
244
718
|
);
|
|
245
|
-
|
|
719
|
+
} else {
|
|
720
|
+
s.stop(pc4.red("\u2716 Failed to fetch the component registry from GitHub network paths."));
|
|
246
721
|
}
|
|
247
|
-
const selectBlockPrompt = await select2({
|
|
248
|
-
message: "Which backend block would you like to inject?",
|
|
249
|
-
options: availableOptions
|
|
250
|
-
});
|
|
251
|
-
handleCancel(selectBlockPrompt);
|
|
252
|
-
targetBlock = selectBlockPrompt;
|
|
253
|
-
}
|
|
254
|
-
const blockMeta = registry[targetBlock];
|
|
255
|
-
if (!blockMeta) {
|
|
256
|
-
outro2(pc2.red(`\u2716 Block "${targetBlock}" does not exist in the remote registry.`));
|
|
257
722
|
return;
|
|
258
723
|
}
|
|
259
|
-
|
|
260
|
-
if (!
|
|
261
|
-
|
|
262
|
-
|
|
724
|
+
let blockMap = {};
|
|
725
|
+
if (registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks)) {
|
|
726
|
+
blockMap = registry.blocks;
|
|
727
|
+
} else {
|
|
728
|
+
blockMap = registry;
|
|
729
|
+
}
|
|
730
|
+
const blocksForEnv = Object.entries(blockMap).filter(([_, block]) => {
|
|
731
|
+
if (!block) return false;
|
|
732
|
+
const hasAdapter = block.adapters?.[envKey] !== void 0;
|
|
733
|
+
const hasEnvironment = block.environments?.[envKey] !== void 0;
|
|
734
|
+
return hasAdapter || hasEnvironment;
|
|
735
|
+
}).map(([key, block]) => {
|
|
736
|
+
const context = block.adapters?.[envKey] ?? block.environments?.[envKey];
|
|
737
|
+
return {
|
|
738
|
+
name: key,
|
|
739
|
+
description: block.description,
|
|
740
|
+
variants: Object.keys(context?.variants || {})
|
|
741
|
+
};
|
|
742
|
+
});
|
|
743
|
+
if (json) {
|
|
744
|
+
process.stdout.write(
|
|
745
|
+
JSON.stringify({ success: true, environment: envKey, blocks: blocksForEnv }) + "\n"
|
|
263
746
|
);
|
|
264
|
-
|
|
747
|
+
} else {
|
|
748
|
+
console.log(`
|
|
749
|
+
Available backend blocks for ${pc4.magenta(envKey)}:`);
|
|
750
|
+
if (blocksForEnv.length === 0) {
|
|
751
|
+
console.log(pc4.dim(" No blocks found for this framework."));
|
|
752
|
+
} else {
|
|
753
|
+
blocksForEnv.forEach((b) => {
|
|
754
|
+
console.log(`
|
|
755
|
+
${pc4.cyan(b.name)}`);
|
|
756
|
+
console.log(` ${pc4.dim(b.description)}`);
|
|
757
|
+
console.log(` Storage Variants: ${b.variants.map((v) => pc4.yellow(v)).join(", ")}`);
|
|
758
|
+
});
|
|
759
|
+
console.log();
|
|
760
|
+
}
|
|
265
761
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/commands/mcp.ts
|
|
765
|
+
import path3, { join as join4 } from "path";
|
|
766
|
+
import fs4 from "fs/promises";
|
|
767
|
+
import pc5 from "picocolors";
|
|
768
|
+
import { outro as outro5, spinner as spinner4, select as select3, confirm as confirm3 } from "@clack/prompts";
|
|
769
|
+
var CLIENT_MATRIX = {
|
|
770
|
+
claude: {
|
|
771
|
+
label: "Claude Code",
|
|
772
|
+
relativePath: ".mcp.json",
|
|
773
|
+
format: "json"
|
|
774
|
+
},
|
|
775
|
+
codex: {
|
|
776
|
+
label: "Codex CLI",
|
|
777
|
+
relativePath: ".codex/config.toml",
|
|
778
|
+
format: "toml"
|
|
779
|
+
},
|
|
780
|
+
cursor: {
|
|
781
|
+
label: "Cursor",
|
|
782
|
+
relativePath: ".cursor/mcp.json",
|
|
783
|
+
format: "json"
|
|
784
|
+
},
|
|
785
|
+
vscode: {
|
|
786
|
+
label: "VS Code",
|
|
787
|
+
relativePath: ".vscode/settings.json",
|
|
788
|
+
format: "json",
|
|
789
|
+
isCustomSchema: true
|
|
790
|
+
},
|
|
791
|
+
windsurf: {
|
|
792
|
+
label: "Windsurf",
|
|
793
|
+
relativePath: ".windsurf/mcp.json",
|
|
794
|
+
format: "json"
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
var BLOCKEND_SERVER = {
|
|
798
|
+
command: "npx",
|
|
799
|
+
args: ["-y", "blockend-cli", "mcp"]
|
|
800
|
+
};
|
|
801
|
+
function generateToml(server) {
|
|
802
|
+
const args = server.args.map((arg) => `"${arg}"`).join(", ");
|
|
803
|
+
return `
|
|
804
|
+
[mcp_servers.blockend]
|
|
805
|
+
command = "${server.command}"
|
|
806
|
+
args = [${args}]
|
|
807
|
+
`.trimStart();
|
|
808
|
+
}
|
|
809
|
+
async function mcpStartCommand() {
|
|
810
|
+
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
811
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
812
|
+
const { z } = await import("zod");
|
|
813
|
+
const { execa } = await import("execa");
|
|
814
|
+
const server = new McpServer({
|
|
815
|
+
name: "blockend",
|
|
816
|
+
version: "0.1.0",
|
|
817
|
+
description: "Blockend MCP server \u2014 install production-ready backend blocks into any project"
|
|
818
|
+
});
|
|
819
|
+
async function safeExec(command, args, options) {
|
|
820
|
+
try {
|
|
821
|
+
const { stdout } = await execa(command, args, options);
|
|
822
|
+
return { text: String(stdout), isError: false };
|
|
823
|
+
} catch (error) {
|
|
824
|
+
const err = error;
|
|
825
|
+
const rawError = err.stderr || err.stdout || err.message || "Unknown execution error";
|
|
826
|
+
return { text: `Error executing command: ${String(rawError)}`, isError: true };
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
server.tool(
|
|
830
|
+
"list_blocks",
|
|
831
|
+
"Lists all available Blockend blocks with names, descriptions, and tags",
|
|
832
|
+
{},
|
|
833
|
+
async () => {
|
|
834
|
+
const result = await safeExec("npx", ["--no-install", "blockend", "list", "--json"]);
|
|
835
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
836
|
+
}
|
|
837
|
+
);
|
|
838
|
+
server.tool(
|
|
839
|
+
"add_block",
|
|
840
|
+
"Installs a Blockend block into a project.",
|
|
841
|
+
{
|
|
842
|
+
blockName: z.string().describe("Block name e.g. rate-limit, error-handler, logger"),
|
|
843
|
+
projectPath: z.string().describe("Absolute path to the project root where blockend should install")
|
|
844
|
+
},
|
|
845
|
+
async ({ blockName, projectPath }) => {
|
|
846
|
+
const result = await safeExec(
|
|
847
|
+
"npx",
|
|
848
|
+
["--no-install", "blockend", "add", blockName, "--yes", "--json"],
|
|
849
|
+
{ cwd: projectPath }
|
|
850
|
+
);
|
|
851
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
server.tool(
|
|
855
|
+
"detect_project",
|
|
856
|
+
"Analyzes a project context.",
|
|
857
|
+
{
|
|
858
|
+
projectPath: z.string().describe("Absolute path to the project root")
|
|
859
|
+
},
|
|
860
|
+
async ({ projectPath }) => {
|
|
861
|
+
const result = await safeExec("npx", ["--no-install", "blockend", "detect", "--json"], {
|
|
862
|
+
cwd: projectPath
|
|
863
|
+
});
|
|
864
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
865
|
+
}
|
|
866
|
+
);
|
|
867
|
+
const transport = new StdioServerTransport();
|
|
868
|
+
await server.connect(transport);
|
|
869
|
+
}
|
|
870
|
+
async function mcpInitCommand(options) {
|
|
871
|
+
const cwd = process.cwd();
|
|
872
|
+
let chosenClient = options.client;
|
|
873
|
+
if (!chosenClient) {
|
|
874
|
+
if (options.yes) {
|
|
875
|
+
chosenClient = "claude";
|
|
272
876
|
} else {
|
|
273
|
-
const
|
|
274
|
-
message: "
|
|
275
|
-
options:
|
|
276
|
-
value:
|
|
277
|
-
label:
|
|
877
|
+
const selection = await select3({
|
|
878
|
+
message: "Select target AI client layout profile:",
|
|
879
|
+
options: Object.entries(CLIENT_MATRIX).map(([key, value]) => ({
|
|
880
|
+
value: key,
|
|
881
|
+
label: value.label
|
|
278
882
|
}))
|
|
279
883
|
});
|
|
280
|
-
|
|
281
|
-
|
|
884
|
+
if (typeof selection === "symbol") return;
|
|
885
|
+
chosenClient = selection;
|
|
282
886
|
}
|
|
283
|
-
variantMeta = envConfig.variants[selectedVariant];
|
|
284
887
|
}
|
|
285
|
-
const
|
|
286
|
-
if (!
|
|
287
|
-
|
|
888
|
+
const meta = CLIENT_MATRIX[chosenClient];
|
|
889
|
+
if (!meta) {
|
|
890
|
+
outro5(pc5.red(`\u2716 Unsupported client identifier profile: ${options.client}`));
|
|
288
891
|
return;
|
|
289
892
|
}
|
|
290
|
-
const
|
|
291
|
-
|
|
893
|
+
const absoluteConfigTarget = join4(cwd, meta.relativePath);
|
|
894
|
+
if (options.dryRun) {
|
|
895
|
+
console.log(
|
|
896
|
+
pc5.cyan(`
|
|
897
|
+
\u2139 Dry-run mode active. Intended path location: ${pc5.dim(meta.relativePath)}`)
|
|
898
|
+
);
|
|
899
|
+
if (meta.format === "json") {
|
|
900
|
+
const jsonConfig = meta.isCustomSchema ? { "mcp.mcpServers": { blockend: BLOCKEND_SERVER } } : { mcpServers: { blockend: BLOCKEND_SERVER } };
|
|
901
|
+
console.log(pc5.gray(JSON.stringify(jsonConfig, null, 2)));
|
|
902
|
+
} else {
|
|
903
|
+
console.log(pc5.gray(generateToml(BLOCKEND_SERVER)));
|
|
904
|
+
}
|
|
905
|
+
outro5(pc5.green("\u2714 Dry run evaluation complete."));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
let fileConflict = false;
|
|
292
909
|
try {
|
|
293
|
-
|
|
910
|
+
await fs4.access(absoluteConfigTarget);
|
|
911
|
+
fileConflict = true;
|
|
294
912
|
} catch {
|
|
295
|
-
outro2(pc2.red("\u2716 Failed parsing file data configurations from target package.json location."));
|
|
296
|
-
return;
|
|
297
913
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const allRequiredDeps = [...envConfig.dependencies ?? [], ...variantMeta?.dependencies ?? []];
|
|
303
|
-
const missingDeps = allRequiredDeps.filter((dep) => !(dep in installedDeps));
|
|
304
|
-
if (missingDeps.length > 0) {
|
|
305
|
-
console.log(
|
|
306
|
-
pc2.yellow(`
|
|
307
|
-
\u26A0\uFE0F Missing required infrastructure packages: ${missingDeps.join(", ")}`)
|
|
308
|
-
);
|
|
309
|
-
const shouldInstallPrompt = await confirm2({
|
|
310
|
-
message: "Would you like the CLI to automatically install these dependencies?",
|
|
311
|
-
initialValue: true
|
|
914
|
+
if (fileConflict && !options.force && !options.yes) {
|
|
915
|
+
const shouldOverwrite = await confirm3({
|
|
916
|
+
message: `\u26A0 Local configuration at [${meta.relativePath}] already exists. Overwrite?`,
|
|
917
|
+
initialValue: false
|
|
312
918
|
});
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
s.start(`Preparing native workspace via ${packageManager}...`);
|
|
317
|
-
try {
|
|
318
|
-
const installCmd = packageManager === "pnpm" ? `pnpm add ${missingDeps.join(" ")}` : `npm install ${missingDeps.join(" ")}`;
|
|
319
|
-
s.stop(pc2.cyan(`Executing: ${installCmd}
|
|
320
|
-
`));
|
|
321
|
-
await new Promise((resolve, reject) => {
|
|
322
|
-
const child = exec(installCmd, { cwd: packageJsonDir });
|
|
323
|
-
child.stdout?.on("data", (data) => {
|
|
324
|
-
process.stdout.write(pc2.dim(data));
|
|
325
|
-
});
|
|
326
|
-
child.stderr?.on("data", (data) => {
|
|
327
|
-
process.stdout.write(pc2.dim(pc2.red(data)));
|
|
328
|
-
});
|
|
329
|
-
child.on("close", (code) => {
|
|
330
|
-
if (code === 0) resolve();
|
|
331
|
-
else reject(new Error(`Exit code ${code}`));
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
console.log("");
|
|
335
|
-
s.start(pc2.green("\u2714 Dependencies installed cleanly. Continuing components build..."));
|
|
336
|
-
s.stop();
|
|
337
|
-
} catch {
|
|
338
|
-
s.stop(pc2.red("\u2716 Automated dependency installation failed. Please run setup manually."));
|
|
339
|
-
}
|
|
919
|
+
if (!shouldOverwrite || typeof shouldOverwrite === "symbol") {
|
|
920
|
+
outro5(pc5.yellow("\u2139 Operations halted. Project layout revisions preserved."));
|
|
921
|
+
return;
|
|
340
922
|
}
|
|
341
923
|
}
|
|
342
|
-
s
|
|
924
|
+
const s = spinner4();
|
|
925
|
+
s.start(`Writing localized project configurations inside ${pc5.dim(meta.relativePath)}...`);
|
|
343
926
|
try {
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
let targetFolder = path2.resolve(rootDir, physicalPath);
|
|
356
|
-
if (variantMeta && selectedVariant) {
|
|
357
|
-
targetFolder = join2(targetFolder, targetBlock);
|
|
358
|
-
}
|
|
359
|
-
let fileExistsConflict = false;
|
|
360
|
-
for (const remoteFile of remoteFilesToDownload) {
|
|
361
|
-
const parsedFilename = path2.basename(remoteFile, ".txt");
|
|
362
|
-
const checkFileLocation = join2(targetFolder, parsedFilename);
|
|
363
|
-
try {
|
|
364
|
-
await fs2.access(checkFileLocation);
|
|
365
|
-
fileExistsConflict = true;
|
|
366
|
-
break;
|
|
367
|
-
} catch {
|
|
927
|
+
await fs4.mkdir(path3.dirname(absoluteConfigTarget), { recursive: true });
|
|
928
|
+
if (meta.format === "json") {
|
|
929
|
+
let workingConfig = {};
|
|
930
|
+
if (fileConflict) {
|
|
931
|
+
try {
|
|
932
|
+
const rawContent = await fs4.readFile(absoluteConfigTarget, "utf-8");
|
|
933
|
+
workingConfig = JSON.parse(rawContent);
|
|
934
|
+
} catch {
|
|
935
|
+
workingConfig = {};
|
|
936
|
+
}
|
|
368
937
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
});
|
|
376
|
-
handleCancel(overwritePrompt);
|
|
377
|
-
if (!overwritePrompt) {
|
|
378
|
-
outro2(pc2.yellow("\u2139 Operation aborted safely. Local code modifications preserved."));
|
|
379
|
-
return;
|
|
938
|
+
if (meta.isCustomSchema) {
|
|
939
|
+
const existingServers = workingConfig["mcp.mcpServers"] || {};
|
|
940
|
+
workingConfig["mcp.mcpServers"] = { ...existingServers, blockend: BLOCKEND_SERVER };
|
|
941
|
+
} else {
|
|
942
|
+
const existingServers = workingConfig.mcpServers || {};
|
|
943
|
+
workingConfig.mcpServers = { ...existingServers, blockend: BLOCKEND_SERVER };
|
|
380
944
|
}
|
|
381
|
-
|
|
945
|
+
await fs4.writeFile(absoluteConfigTarget, JSON.stringify(workingConfig, null, 2), "utf-8");
|
|
946
|
+
} else {
|
|
947
|
+
await fs4.writeFile(absoluteConfigTarget, generateToml(BLOCKEND_SERVER), "utf-8");
|
|
382
948
|
}
|
|
383
|
-
|
|
384
|
-
|
|
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})`);
|
|
389
|
-
}
|
|
390
|
-
const rawCodeTemplate = await response.text();
|
|
391
|
-
const targetFilename = path2.basename(remoteFilePath, ".txt");
|
|
392
|
-
const localWriteLocation = join2(targetFolder, targetFilename);
|
|
393
|
-
await fs2.writeFile(localWriteLocation, rawCodeTemplate, "utf-8");
|
|
394
|
-
}
|
|
395
|
-
const cleanDisplayPath = variantMeta && selectedVariant ? `${physicalPath.replace(/\\/g, "/")}/${targetBlock}` : physicalPath.replace(/\\/g, "/");
|
|
396
|
-
s.stop(pc2.green("\u2714 Component isolation structures written smoothly."));
|
|
397
|
-
outro2(
|
|
398
|
-
pc2.cyan(
|
|
399
|
-
`\u2728 Source blocks written to ${cleanDisplayPath}/ layout. Code ownership transferred!`
|
|
400
|
-
)
|
|
949
|
+
s.stop(
|
|
950
|
+
pc5.green(`\u2714 Local workspace integration fully complete! File target: ${meta.relativePath}`)
|
|
401
951
|
);
|
|
402
|
-
process.exit(0);
|
|
403
952
|
} catch (error) {
|
|
404
|
-
s.stop(
|
|
405
|
-
console.error(error);
|
|
406
|
-
process.exit(1);
|
|
953
|
+
s.stop(pc5.red("\u2716 Fatal crash reading or creating local configuration maps."));
|
|
954
|
+
console.error(pc5.dim(String(error)));
|
|
407
955
|
}
|
|
408
956
|
}
|
|
409
957
|
|
|
410
958
|
// src/index.ts
|
|
411
|
-
var
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
959
|
+
var mcpInit = defineCommand({
|
|
960
|
+
meta: {
|
|
961
|
+
name: "init",
|
|
962
|
+
description: "Generate appropriate project-local MCP infrastructure configurations"
|
|
963
|
+
},
|
|
964
|
+
args: {
|
|
965
|
+
client: {
|
|
966
|
+
type: "string",
|
|
967
|
+
description: "Target specific integration client profiles: claude | codex | cursor | vscode | windsurf"
|
|
968
|
+
},
|
|
969
|
+
force: {
|
|
970
|
+
type: "boolean",
|
|
971
|
+
default: false,
|
|
972
|
+
description: "Force override existing asset layouts"
|
|
973
|
+
},
|
|
974
|
+
"dry-run": {
|
|
975
|
+
type: "boolean",
|
|
976
|
+
default: false,
|
|
977
|
+
description: "Output planned workspace updates without disk mutation logs"
|
|
978
|
+
},
|
|
979
|
+
yes: { type: "boolean", default: false, description: "Bypass verification prompt iterations" }
|
|
980
|
+
},
|
|
981
|
+
async run({ args }) {
|
|
982
|
+
await mcpInitCommand({
|
|
983
|
+
client: args.client,
|
|
984
|
+
force: args.force,
|
|
985
|
+
dryRun: args["dry-run"],
|
|
986
|
+
yes: args.yes
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
var mcp = defineCommand({
|
|
991
|
+
meta: {
|
|
992
|
+
name: "mcp",
|
|
993
|
+
description: "Connect to context protocol channels or configure localized project setups"
|
|
994
|
+
},
|
|
995
|
+
subCommands: {
|
|
996
|
+
init: mcpInit
|
|
997
|
+
},
|
|
998
|
+
async run({ rawArgs }) {
|
|
999
|
+
if (rawArgs.length === 0) {
|
|
1000
|
+
await mcpStartCommand();
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
var init = defineCommand({
|
|
1005
|
+
meta: {
|
|
1006
|
+
name: "init",
|
|
1007
|
+
description: "Initialize Blockend configuration profile (blockend.json)"
|
|
1008
|
+
},
|
|
1009
|
+
args: {
|
|
1010
|
+
yes: {
|
|
1011
|
+
type: "boolean",
|
|
1012
|
+
alias: "y",
|
|
1013
|
+
default: false,
|
|
1014
|
+
description: "Skip initialization questions and auto-enforce layout defaults"
|
|
1015
|
+
},
|
|
1016
|
+
json: {
|
|
1017
|
+
type: "boolean",
|
|
1018
|
+
default: false,
|
|
1019
|
+
description: "Output machine-readable configuration write states"
|
|
1020
|
+
}
|
|
1021
|
+
},
|
|
1022
|
+
async run({ args }) {
|
|
1023
|
+
await initCommand({ yes: args.yes, json: args.json });
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
var list = defineCommand({
|
|
1027
|
+
meta: {
|
|
1028
|
+
name: "list",
|
|
1029
|
+
description: "List available component blocks matching local project runtime environment context"
|
|
1030
|
+
},
|
|
1031
|
+
args: {
|
|
1032
|
+
json: {
|
|
1033
|
+
type: "boolean",
|
|
1034
|
+
default: false,
|
|
1035
|
+
description: "Output machine-readable blocks configuration array stream"
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
async run({ args }) {
|
|
1039
|
+
await listCommand({ json: args.json });
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
var add = defineCommand({
|
|
1043
|
+
meta: {
|
|
1044
|
+
name: "add",
|
|
1045
|
+
description: "Inject a production-grade component block straight into your codebase"
|
|
1046
|
+
},
|
|
1047
|
+
args: {
|
|
1048
|
+
block: {
|
|
1049
|
+
type: "positional",
|
|
1050
|
+
required: false,
|
|
1051
|
+
description: "Target block key name from the remote repository registry"
|
|
1052
|
+
},
|
|
1053
|
+
yes: {
|
|
1054
|
+
type: "boolean",
|
|
1055
|
+
alias: "y",
|
|
1056
|
+
default: false,
|
|
1057
|
+
description: "Skip structural modification confirmations and force dependency downloads"
|
|
1058
|
+
},
|
|
1059
|
+
json: {
|
|
1060
|
+
type: "boolean",
|
|
1061
|
+
default: false,
|
|
1062
|
+
description: "Output streaming machine-readable JSON payloads for automation systems"
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
async run({ args }) {
|
|
1066
|
+
await addCommand(args.block, { yes: args.yes, json: args.json });
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
var detect = defineCommand({
|
|
1070
|
+
meta: {
|
|
1071
|
+
name: "detect",
|
|
1072
|
+
description: "Scan runtime directory frameworks, engines, and workspaces architectures"
|
|
1073
|
+
},
|
|
1074
|
+
args: {
|
|
1075
|
+
json: {
|
|
1076
|
+
type: "boolean",
|
|
1077
|
+
default: false,
|
|
1078
|
+
description: "Output full detected project workspace context delta maps directly as JSON"
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
async run({ args }) {
|
|
1082
|
+
await detectCommand({ json: args.json });
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
var main = defineCommand({
|
|
1086
|
+
meta: {
|
|
1087
|
+
name: "blockend",
|
|
1088
|
+
version: "0.1.0",
|
|
1089
|
+
description: "Blockend CLI - Core architectural blocks straight into your repository layout primitives"
|
|
1090
|
+
},
|
|
1091
|
+
subCommands: { init, add, detect, list, mcp }
|
|
416
1092
|
});
|
|
417
|
-
|
|
1093
|
+
runMain(main);
|