blockend-cli 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/dist/block-install/index.d.ts +2 -1
- package/dist/block-install/recipes.d.ts +6 -0
- package/dist/block-install/types.d.ts +3 -1
- package/dist/commands/add.d.ts +1 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/index.js +214 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,26 +48,35 @@ Detects the framework, language, package manager, Redis, and import alias, then
|
|
|
48
48
|
npx blockend-cli add [block] [options]
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
| Option
|
|
52
|
-
|
|
|
53
|
-
| `[block]`
|
|
54
|
-
| `--yes`
|
|
55
|
-
| `--json`
|
|
56
|
-
| `--multi`
|
|
51
|
+
| Option | Alias | Description |
|
|
52
|
+
| ---------- | ----- | ------------------------------------------------------------ |
|
|
53
|
+
| `[block]` | | Registry key, such as `rate-limiter` or `logger`. |
|
|
54
|
+
| `--yes` | `-y` | Skip prompts, install dependencies, and overwrite conflicts. |
|
|
55
|
+
| `--json` | | Write machine-readable output. |
|
|
56
|
+
| `--multi` | `-m` | Select multiple blocks. |
|
|
57
|
+
| `--recipe` | | Add every block in a published recipe, in integration order. |
|
|
57
58
|
|
|
58
59
|
Without a block name, the CLI opens a selector. For blocks with storage options, it asks for a variant. In interactive mode, `includeRedis: true` selects the Redis variant when one exists. With `--yes`, it selects memory when available, otherwise the first variant.
|
|
59
60
|
|
|
60
61
|
`add --yes` without a block name is an error.
|
|
61
62
|
|
|
63
|
+
`--recipe` installs a curated, ordered stack from the registry in one command:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx blockend-cli add --recipe authenticated-api
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
It installs the recipe's required blocks in integration order. Optional blocks are offered interactively and skipped with `--yes` or `--json`. `--recipe` cannot be combined with `--multi` or a positional block name.
|
|
70
|
+
|
|
62
71
|
Dependency installation runs `pnpm add` when `packageManager` is `pnpm`, and `npm install` for every other value. Yarn and bun projects should decline the install prompt and add the listed packages themselves.
|
|
63
72
|
|
|
64
73
|
### `list`
|
|
65
74
|
|
|
66
75
|
```bash
|
|
67
|
-
npx blockend-cli list [--json]
|
|
76
|
+
npx blockend-cli list [--json] [--recipes]
|
|
68
77
|
```
|
|
69
78
|
|
|
70
|
-
Lists blocks compatible with the framework in `blockend.json`.
|
|
79
|
+
Lists blocks compatible with the framework in `blockend.json`. With `--recipes`, it lists published recipes instead and does not require `blockend.json`.
|
|
71
80
|
|
|
72
81
|
### `detect`
|
|
73
82
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export type { AdapterConfig, AppliedBlock, ApplyBlockOptions, AssetMapping, AvailableBlock, BlockPlan, BlockPlanError, BlockPlanErrorCode, BlockPlanResult, BlockManifest, BlockendConfig, EnvironmentConfig, FrameworkName, InstalledBlockRecord, PlanBlockOptions, PreviewBlockOptions, PreviewFile, RegistryManifest, RegistrySource } from "./types.js";
|
|
1
|
+
export type { AdapterConfig, AppliedBlock, ApplyBlockOptions, AssetMapping, AvailableBlock, BlockPlan, BlockPlanError, BlockPlanErrorCode, BlockPlanResult, BlockManifest, BlockendConfig, EnvironmentConfig, FrameworkName, InstalledBlockRecord, PlanBlockOptions, PreviewBlockOptions, PreviewFile, Recipe, RecipeCollection, RegistryManifest, RegistrySource } from "./types.js";
|
|
2
2
|
export { createGitHubRegistrySource, createStaticRegistrySource, loadRegistry, type GitHubRegistrySourceOptions } from "./registry.js";
|
|
3
3
|
export { listAvailableBlocks, planBlock } from "./plan.js";
|
|
4
4
|
export { previewBlock } from "./preview.js";
|
|
5
5
|
export { applyBlock } from "./apply.js";
|
|
6
6
|
export { findUp } from "./config.js";
|
|
7
|
+
export { findRecipe, loadRecipeCollection, orderRecipeBlocks } from "./recipes.js";
|
|
7
8
|
export { resolvePreferredVariant, selectVariant } from "./variants.js";
|
|
8
9
|
export { getMissingDependencies, getVersionConflicts, parseDepSpec, semverSatisfies } from "./deps.js";
|
|
9
10
|
export { rewriteFileImports } from "./imports.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Recipe, type RecipeCollection } from "@blockend/registry";
|
|
2
|
+
import type { RegistrySource } from "./types.js";
|
|
3
|
+
export declare const RECIPE_COLLECTION_PATH = "registry/recipes.json";
|
|
4
|
+
export declare function loadRecipeCollection(source: RegistrySource): Promise<RecipeCollection>;
|
|
5
|
+
export declare function findRecipe(collection: RecipeCollection, slug: string): Recipe | undefined;
|
|
6
|
+
export declare function orderRecipeBlocks(recipe: Recipe, selectedOptional?: string[]): string[];
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { AdapterConfig as RegistryAdapterConfig, AssetMapping as RegistryAssetMapping, BlockManifest as RegistryBlockManifest, EnvironmentConfig as RegistryEnvironmentConfig, FrameworkName as RegistryFrameworkName, RegistryManifest as RegistryManifestType } from "@blockend/registry";
|
|
1
|
+
import type { AdapterConfig as RegistryAdapterConfig, AssetMapping as RegistryAssetMapping, BlockManifest as RegistryBlockManifest, EnvironmentConfig as RegistryEnvironmentConfig, FrameworkName as RegistryFrameworkName, Recipe as RegistryRecipe, RecipeCollection as RegistryRecipeCollection, RegistryManifest as RegistryManifestType } from "@blockend/registry";
|
|
2
2
|
export type AssetMapping = RegistryAssetMapping;
|
|
3
3
|
export type AdapterConfig = RegistryAdapterConfig;
|
|
4
4
|
export type EnvironmentConfig = RegistryEnvironmentConfig;
|
|
5
5
|
export type FrameworkName = RegistryFrameworkName;
|
|
6
6
|
export type BlockManifest = RegistryBlockManifest;
|
|
7
7
|
export type RegistryManifest = RegistryManifestType;
|
|
8
|
+
export type Recipe = RegistryRecipe;
|
|
9
|
+
export type RecipeCollection = RegistryRecipeCollection;
|
|
8
10
|
export interface RegistrySource {
|
|
9
11
|
fetchRegistry(): Promise<RegistryManifest>;
|
|
10
12
|
fetchFile(sourcePath: string): Promise<string>;
|
package/dist/commands/add.d.ts
CHANGED
package/dist/commands/list.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -195,6 +195,17 @@ ${formatIssues(result.error.issues)}`,
|
|
|
195
195
|
}
|
|
196
196
|
return result.data;
|
|
197
197
|
}
|
|
198
|
+
function parseRecipeCollection(input) {
|
|
199
|
+
const result = RecipeCollectionSchema.safeParse(input);
|
|
200
|
+
if (!result.success) {
|
|
201
|
+
throw new RegistryParseError(
|
|
202
|
+
`Invalid recipe collection:
|
|
203
|
+
${formatIssues(result.error.issues)}`,
|
|
204
|
+
result.error.issues
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return result.data;
|
|
208
|
+
}
|
|
198
209
|
|
|
199
210
|
// src/block-install/registry.ts
|
|
200
211
|
var DEFAULT_REPO_OWNER = "codewithnuh";
|
|
@@ -534,6 +545,32 @@ async function applyBlock(plan, options) {
|
|
|
534
545
|
};
|
|
535
546
|
}
|
|
536
547
|
|
|
548
|
+
// src/block-install/recipes.ts
|
|
549
|
+
var RECIPE_COLLECTION_PATH = "registry/recipes.json";
|
|
550
|
+
async function loadRecipeCollection(source) {
|
|
551
|
+
const raw = await source.fetchFile(RECIPE_COLLECTION_PATH);
|
|
552
|
+
try {
|
|
553
|
+
return parseRecipeCollection(JSON.parse(raw));
|
|
554
|
+
} catch (error) {
|
|
555
|
+
if (error instanceof SyntaxError) {
|
|
556
|
+
throw new Error(`Recipe collection is not valid JSON: ${error.message}`);
|
|
557
|
+
}
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function findRecipe(collection, slug) {
|
|
562
|
+
return collection.recipes.find((recipe) => recipe.slug === slug);
|
|
563
|
+
}
|
|
564
|
+
function orderRecipeBlocks(recipe, selectedOptional = []) {
|
|
565
|
+
const selected = /* @__PURE__ */ new Set([...recipe.includedBlocks, ...selectedOptional]);
|
|
566
|
+
const ordered = recipe.integrationOrder.filter((slug) => selected.has(slug));
|
|
567
|
+
const placed = new Set(ordered);
|
|
568
|
+
const missing = [...recipe.includedBlocks, ...recipe.optionalBlocks].filter(
|
|
569
|
+
(slug) => selected.has(slug) && !placed.has(slug)
|
|
570
|
+
);
|
|
571
|
+
return [...ordered, ...missing];
|
|
572
|
+
}
|
|
573
|
+
|
|
537
574
|
// src/block-install/deps.ts
|
|
538
575
|
function parseDepSpec(dep) {
|
|
539
576
|
const atIndex = dep.lastIndexOf("@");
|
|
@@ -620,7 +657,7 @@ function failureMessage(code, blockName) {
|
|
|
620
657
|
if (code === "no-adapter") return `No adapter for "${blockName}"`;
|
|
621
658
|
return `No variants for "${blockName}"`;
|
|
622
659
|
}
|
|
623
|
-
async function addSingleBlock(targetBlock, context) {
|
|
660
|
+
async function addSingleBlock(targetBlock, context, options = {}) {
|
|
624
661
|
const { yes = false, json = false } = context.options;
|
|
625
662
|
const {
|
|
626
663
|
registry,
|
|
@@ -635,9 +672,12 @@ async function addSingleBlock(targetBlock, context) {
|
|
|
635
672
|
rewriteStrategy
|
|
636
673
|
} = context;
|
|
637
674
|
const s = spinner();
|
|
675
|
+
const reportError = (message) => {
|
|
676
|
+
if (!options.suppressErrorOutput) outputError(json, message);
|
|
677
|
+
};
|
|
638
678
|
const initialPlan = await planBlock(registry, targetBlock, envKey);
|
|
639
679
|
if (!initialPlan.ok) {
|
|
640
|
-
|
|
680
|
+
reportError(initialPlan.error.message);
|
|
641
681
|
return {
|
|
642
682
|
success: false,
|
|
643
683
|
filesWritten: [],
|
|
@@ -666,7 +706,7 @@ async function addSingleBlock(targetBlock, context) {
|
|
|
666
706
|
variant: selectedVariant
|
|
667
707
|
});
|
|
668
708
|
if (!selectedPlan.ok) {
|
|
669
|
-
|
|
709
|
+
reportError(selectedPlan.error.message);
|
|
670
710
|
return {
|
|
671
711
|
success: false,
|
|
672
712
|
filesWritten: [],
|
|
@@ -678,7 +718,7 @@ async function addSingleBlock(targetBlock, context) {
|
|
|
678
718
|
const targetFolder = path5.resolve(blocksRootAbsolute, targetBlock);
|
|
679
719
|
const packageJsonPath = await findUp("package.json", rootDir);
|
|
680
720
|
if (!packageJsonPath) {
|
|
681
|
-
|
|
721
|
+
reportError("Could not locate package.json.");
|
|
682
722
|
return { success: false, filesWritten: [], message: "package.json not found" };
|
|
683
723
|
}
|
|
684
724
|
const packageJsonDir = dirname4(packageJsonPath);
|
|
@@ -686,7 +726,7 @@ async function addSingleBlock(targetBlock, context) {
|
|
|
686
726
|
try {
|
|
687
727
|
packageJson = JSON.parse(await fs5.readFile(packageJsonPath, "utf-8"));
|
|
688
728
|
} catch (err) {
|
|
689
|
-
|
|
729
|
+
reportError("Failed to parse package.json.");
|
|
690
730
|
if (!json) log.error(pc.dim(String(err)));
|
|
691
731
|
return { success: false, filesWritten: [], message: "Failed to parse package.json" };
|
|
692
732
|
}
|
|
@@ -748,7 +788,7 @@ async function addSingleBlock(targetBlock, context) {
|
|
|
748
788
|
if (!json) {
|
|
749
789
|
s.stop("Installation failed.");
|
|
750
790
|
log.error(pc.dim(String(err)));
|
|
751
|
-
} else
|
|
791
|
+
} else reportError("Dependency installation failed.");
|
|
752
792
|
return { success: false, filesWritten: [], message: "Dependency installation failed" };
|
|
753
793
|
}
|
|
754
794
|
}
|
|
@@ -791,12 +831,64 @@ async function addSingleBlock(targetBlock, context) {
|
|
|
791
831
|
if (!json) {
|
|
792
832
|
s.stop("Failed.");
|
|
793
833
|
log.error(pc.dim(String(err)));
|
|
794
|
-
} else
|
|
834
|
+
} else reportError("Fatal error while writing block files.");
|
|
795
835
|
return { success: false, filesWritten: [], message: `Failed to add ${targetBlock}` };
|
|
796
836
|
}
|
|
797
837
|
}
|
|
838
|
+
async function addRecipe(recipeSlug, context) {
|
|
839
|
+
const { yes = false, json = false } = context.options;
|
|
840
|
+
const { source } = context;
|
|
841
|
+
let recipe;
|
|
842
|
+
try {
|
|
843
|
+
const collection = await loadRecipeCollection(source);
|
|
844
|
+
recipe = findRecipe(collection, recipeSlug);
|
|
845
|
+
} catch (err) {
|
|
846
|
+
outputError(json, "Failed to load the recipe collection from the registry.");
|
|
847
|
+
if (!json) log.error(pc.dim(String(err)));
|
|
848
|
+
process.exitCode = 1;
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (!recipe) {
|
|
852
|
+
outputError(json, `Recipe "${recipeSlug}" not found in the registry.`);
|
|
853
|
+
process.exitCode = 1;
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
let selectedOptional = [];
|
|
857
|
+
if (recipe.optionalBlocks.length > 0 && !yes && !json) {
|
|
858
|
+
const picked = await multiselect({
|
|
859
|
+
message: `Optional blocks to include with ${pc.cyan(recipeSlug)}:`,
|
|
860
|
+
options: recipe.optionalBlocks.map((block) => ({ value: block, label: block })),
|
|
861
|
+
required: false
|
|
862
|
+
});
|
|
863
|
+
handleCancel(picked);
|
|
864
|
+
selectedOptional = picked;
|
|
865
|
+
}
|
|
866
|
+
const blocks = orderRecipeBlocks(recipe, selectedOptional);
|
|
867
|
+
if (!json) log.info(`
|
|
868
|
+
Adding ${blocks.length} block(s) from recipe "${recipe.title}"...
|
|
869
|
+
`);
|
|
870
|
+
const added = [];
|
|
871
|
+
const failed = [];
|
|
872
|
+
for (const block of blocks) {
|
|
873
|
+
const result = await addSingleBlock(block, context, { suppressErrorOutput: json });
|
|
874
|
+
if (result.success) added.push(block);
|
|
875
|
+
else failed.push({ block, message: result.message });
|
|
876
|
+
}
|
|
877
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
878
|
+
if (json) {
|
|
879
|
+
process.stdout.write(
|
|
880
|
+
JSON.stringify({ success: failed.length === 0, recipe: recipeSlug, blocks: added, failed }) + "\n"
|
|
881
|
+
);
|
|
882
|
+
} else {
|
|
883
|
+
outro(
|
|
884
|
+
pc.green(`
|
|
885
|
+
\u2728 ${added.length} of ${blocks.length} block(s) added from "${recipe.title}"`) + (failed.length ? `
|
|
886
|
+
${pc.yellow(`Skipped: ${failed.map((entry) => entry.block).join(", ")}`)}` : "")
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
798
890
|
async function addCommand(blockName, options = {}) {
|
|
799
|
-
const { yes = false, json = false, multi = false } = options;
|
|
891
|
+
const { yes = false, json = false, multi = false, recipe } = options;
|
|
800
892
|
if (!json) {
|
|
801
893
|
console.log("");
|
|
802
894
|
intro(`${pc.bgCyan(pc.black(" blockend "))} ${pc.dim("add")}`);
|
|
@@ -852,10 +944,22 @@ async function addCommand(blockName, options = {}) {
|
|
|
852
944
|
rewriteStrategy,
|
|
853
945
|
options
|
|
854
946
|
};
|
|
855
|
-
if (yes && !blockName) {
|
|
947
|
+
if (yes && !blockName && !recipe) {
|
|
856
948
|
outputError(json, "Block name required with --yes. Usage: blockend add <block> --yes");
|
|
857
949
|
return;
|
|
858
950
|
}
|
|
951
|
+
if (recipe && multi) {
|
|
952
|
+
outputError(json, "Use either --recipe or --multi, not both.");
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (recipe && blockName) {
|
|
956
|
+
outputError(json, "Provide either a block name or --recipe, not both.");
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (recipe) {
|
|
960
|
+
await addRecipe(recipe, context);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
859
963
|
if (multi && !blockName) {
|
|
860
964
|
const available = await listAvailableBlocks(registry, envKey);
|
|
861
965
|
if (available.length === 0) {
|
|
@@ -1428,8 +1532,60 @@ import fs7 from "fs/promises";
|
|
|
1428
1532
|
import pc5 from "picocolors";
|
|
1429
1533
|
import { outro as outro4, spinner as spinner3 } from "@clack/prompts";
|
|
1430
1534
|
async function listCommand(options = {}) {
|
|
1431
|
-
const { json = false } = options;
|
|
1535
|
+
const { json = false, recipes: listRecipes = false } = options;
|
|
1432
1536
|
const cwd = process.cwd();
|
|
1537
|
+
const source = createGitHubRegistrySource();
|
|
1538
|
+
const s = spinner3();
|
|
1539
|
+
if (listRecipes) {
|
|
1540
|
+
if (!json) s.start("Fetching published recipes...");
|
|
1541
|
+
try {
|
|
1542
|
+
const collection = await loadRecipeCollection(source);
|
|
1543
|
+
if (!json) s.stop(pc5.green("\u2714 Synced published recipes."));
|
|
1544
|
+
const recipes = collection.recipes.map((recipe) => ({
|
|
1545
|
+
slug: recipe.slug,
|
|
1546
|
+
title: recipe.title,
|
|
1547
|
+
outcome: recipe.outcome,
|
|
1548
|
+
includedBlocks: recipe.includedBlocks,
|
|
1549
|
+
optionalBlocks: recipe.optionalBlocks
|
|
1550
|
+
}));
|
|
1551
|
+
if (json) {
|
|
1552
|
+
process.stdout.write(JSON.stringify({ success: true, recipes }) + "\n");
|
|
1553
|
+
} else {
|
|
1554
|
+
console.log(`
|
|
1555
|
+
Published recipes:`);
|
|
1556
|
+
if (recipes.length === 0) {
|
|
1557
|
+
console.log(pc5.dim(" No recipes published yet."));
|
|
1558
|
+
} else {
|
|
1559
|
+
recipes.forEach((recipe) => {
|
|
1560
|
+
console.log(`
|
|
1561
|
+
${pc5.cyan(recipe.slug)} ${pc5.dim(`\u2014 ${recipe.title}`)}`);
|
|
1562
|
+
console.log(` ${pc5.dim(recipe.outcome)}`);
|
|
1563
|
+
console.log(
|
|
1564
|
+
` Blocks: ${recipe.includedBlocks.map((block) => pc5.yellow(block)).join(", ")}`
|
|
1565
|
+
);
|
|
1566
|
+
if (recipe.optionalBlocks.length > 0) {
|
|
1567
|
+
console.log(
|
|
1568
|
+
` Optional: ${recipe.optionalBlocks.map((block) => pc5.yellow(block)).join(", ")}`
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
console.log(`
|
|
1573
|
+
${pc5.dim(" Add one with: blockend add --recipe <slug>")}
|
|
1574
|
+
`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
if (json) {
|
|
1579
|
+
process.stdout.write(
|
|
1580
|
+
JSON.stringify({ success: false, error: "Failed to fetch recipes from GitHub." }) + "\n"
|
|
1581
|
+
);
|
|
1582
|
+
} else {
|
|
1583
|
+
s.stop(pc5.red("\u2716 Failed to fetch the recipe collection from GitHub."));
|
|
1584
|
+
console.error(pc5.dim(String(error)));
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1433
1589
|
const configPath = await findUp("blockend.json", cwd);
|
|
1434
1590
|
if (!configPath) {
|
|
1435
1591
|
if (json) {
|
|
@@ -1456,8 +1612,6 @@ async function listCommand(options = {}) {
|
|
|
1456
1612
|
return;
|
|
1457
1613
|
}
|
|
1458
1614
|
const envKey = String(config.environment);
|
|
1459
|
-
const source = createGitHubRegistrySource();
|
|
1460
|
-
const s = spinner3();
|
|
1461
1615
|
if (!json) {
|
|
1462
1616
|
s.start("Syncing registry metadata blocks...");
|
|
1463
1617
|
}
|
|
@@ -1557,7 +1711,7 @@ async function mcpStartCommand() {
|
|
|
1557
1711
|
const server = new McpServer({
|
|
1558
1712
|
name: "blockend",
|
|
1559
1713
|
version: "0.1.0",
|
|
1560
|
-
description: "Blockend MCP server \u2014 install production-ready backend blocks into any project"
|
|
1714
|
+
description: "Blockend MCP server \u2014 install production-ready backend blocks or whole recipes into any project"
|
|
1561
1715
|
});
|
|
1562
1716
|
async function safeExec(command, args, options) {
|
|
1563
1717
|
try {
|
|
@@ -1578,6 +1732,21 @@ async function mcpStartCommand() {
|
|
|
1578
1732
|
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
1579
1733
|
}
|
|
1580
1734
|
);
|
|
1735
|
+
server.tool(
|
|
1736
|
+
"list_recipes",
|
|
1737
|
+
"Lists published Blockend recipes: curated, ordered stacks of blocks with the blocks each one installs",
|
|
1738
|
+
{},
|
|
1739
|
+
async () => {
|
|
1740
|
+
const result = await safeExec("npx", [
|
|
1741
|
+
"--no-install",
|
|
1742
|
+
"blockend",
|
|
1743
|
+
"list",
|
|
1744
|
+
"--recipes",
|
|
1745
|
+
"--json"
|
|
1746
|
+
]);
|
|
1747
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
1748
|
+
}
|
|
1749
|
+
);
|
|
1581
1750
|
server.tool(
|
|
1582
1751
|
"add_block",
|
|
1583
1752
|
"Installs a Blockend block into a project.",
|
|
@@ -1687,6 +1856,22 @@ async function mcpStartCommand() {
|
|
|
1687
1856
|
};
|
|
1688
1857
|
}
|
|
1689
1858
|
);
|
|
1859
|
+
server.tool(
|
|
1860
|
+
"add_recipe",
|
|
1861
|
+
"Installs every block in a published recipe, in the recipe's integration order. Optional blocks are skipped in non-interactive mode. Use list_recipes to discover recipe slugs.",
|
|
1862
|
+
{
|
|
1863
|
+
recipe: z2.string().describe("Recipe slug, e.g. production-api-foundation, authenticated-api"),
|
|
1864
|
+
projectPath: z2.string().describe("Absolute path to the project root where blockend should install")
|
|
1865
|
+
},
|
|
1866
|
+
async ({ recipe, projectPath }) => {
|
|
1867
|
+
const result = await safeExec(
|
|
1868
|
+
"npx",
|
|
1869
|
+
["--no-install", "blockend", "add", "--recipe", recipe, "--yes", "--json"],
|
|
1870
|
+
{ cwd: projectPath }
|
|
1871
|
+
);
|
|
1872
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
1873
|
+
}
|
|
1874
|
+
);
|
|
1690
1875
|
const transport = new StdioServerTransport();
|
|
1691
1876
|
await server.connect(transport);
|
|
1692
1877
|
}
|
|
@@ -2491,10 +2676,15 @@ var list = defineCommand({
|
|
|
2491
2676
|
type: "boolean",
|
|
2492
2677
|
default: false,
|
|
2493
2678
|
description: "Output machine-readable blocks configuration array stream"
|
|
2679
|
+
},
|
|
2680
|
+
recipes: {
|
|
2681
|
+
type: "boolean",
|
|
2682
|
+
default: false,
|
|
2683
|
+
description: "List published recipes instead of blocks"
|
|
2494
2684
|
}
|
|
2495
2685
|
},
|
|
2496
2686
|
async run({ args }) {
|
|
2497
|
-
await listCommand({ json: args.json });
|
|
2687
|
+
await listCommand({ json: args.json, recipes: args.recipes });
|
|
2498
2688
|
}
|
|
2499
2689
|
});
|
|
2500
2690
|
var add = defineCommand({
|
|
@@ -2524,10 +2714,19 @@ var add = defineCommand({
|
|
|
2524
2714
|
alias: "m",
|
|
2525
2715
|
default: false,
|
|
2526
2716
|
description: "Select and add multiple blocks at once via interactive multiselect"
|
|
2717
|
+
},
|
|
2718
|
+
recipe: {
|
|
2719
|
+
type: "string",
|
|
2720
|
+
description: "Add every block in a published recipe, in its integration order (see 'blockend list --recipes')"
|
|
2527
2721
|
}
|
|
2528
2722
|
},
|
|
2529
2723
|
async run({ args }) {
|
|
2530
|
-
await addCommand(args.block, {
|
|
2724
|
+
await addCommand(args.block, {
|
|
2725
|
+
yes: args.yes,
|
|
2726
|
+
json: args.json,
|
|
2727
|
+
multi: args.multi,
|
|
2728
|
+
recipe: args.recipe
|
|
2729
|
+
});
|
|
2531
2730
|
}
|
|
2532
2731
|
});
|
|
2533
2732
|
var detect = defineCommand({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blockend-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Blockend CLI — shadcn/ui-style backend blocks for TypeScript. Generate production-ready rate limiting, validation, logging, and error handling as source code into Express, Fastify, Hono, or Next.js projects. You own the code. Ships an MCP server for AI coding assistants.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|