blockend-cli 2.1.0 → 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 +94 -601
- package/dist/block-install/apply.d.ts +2 -0
- package/dist/block-install/config.d.ts +3 -0
- package/dist/block-install/deps.d.ts +7 -0
- package/dist/block-install/imports.d.ts +1 -0
- package/dist/block-install/index.d.ts +10 -0
- package/dist/block-install/plan.d.ts +3 -0
- package/dist/block-install/preview.d.ts +2 -0
- package/dist/block-install/recipes.d.ts +6 -0
- package/dist/block-install/registry.d.ts +11 -0
- package/dist/block-install/types.d.ts +96 -0
- package/dist/block-install/variants.d.ts +2 -0
- package/dist/commands/add.d.ts +1 -41
- package/dist/commands/init.d.ts +0 -23
- package/dist/commands/list.d.ts +1 -0
- package/dist/detectors/alias.d.ts +2 -0
- package/dist/detectors/index.d.ts +4 -1
- package/dist/detectors/readers/tsconfig.d.ts +5 -1
- package/dist/index.js +1138 -717
- package/dist/types/types.d.ts +1 -0
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -4,22 +4,243 @@
|
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
5
|
|
|
6
6
|
// src/commands/add.ts
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import path5, { dirname as dirname4, join as join3 } from "path";
|
|
8
|
+
import fs5 from "fs/promises";
|
|
9
9
|
import { exec } from "child_process";
|
|
10
10
|
import { intro, outro, select, multiselect, spinner, confirm, isCancel, log } from "@clack/prompts";
|
|
11
11
|
import pc from "picocolors";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
var
|
|
16
|
-
var
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
|
|
13
|
+
// ../registry/dist/index.js
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
var FRAMEWORK_KEYS = ["express", "fastify", "hono", "next", "*"];
|
|
16
|
+
var FRAMEWORK_NAMES = ["express", "fastify", "hono", "next"];
|
|
17
|
+
var BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
18
|
+
var DEPENDENCY_PATTERN = /^[a-z@][a-z0-9._/-]*(@[~^>=<]*[0-9]+(\.[0-9]+)*)?$/;
|
|
19
|
+
var RELEASED_AT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
20
|
+
var FrameworkKeySchema = z.enum(FRAMEWORK_KEYS);
|
|
21
|
+
var FrameworkNameSchema = z.enum(FRAMEWORK_NAMES);
|
|
22
|
+
var nonEmptyString = z.string().min(1, "must not be empty");
|
|
23
|
+
function isValidCalendarDate(value) {
|
|
24
|
+
if (!RELEASED_AT_PATTERN.test(value)) return false;
|
|
25
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
26
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
27
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
28
|
+
}
|
|
29
|
+
var AssetMappingSchema = z.object({
|
|
30
|
+
source: z.string().min(1, "source path must not be empty").describe("Repo-relative source path."),
|
|
31
|
+
target: z.string().min(1, "target path must not be empty").describe("Block-folder-relative destination path.")
|
|
32
|
+
}).strict();
|
|
33
|
+
var AssetReferenceSchema = z.union([z.string().min(1), AssetMappingSchema]);
|
|
34
|
+
var VariantSchema = z.object({
|
|
35
|
+
dependencies: z.array(z.string()).describe("Variant-specific production dependencies.").optional(),
|
|
36
|
+
devDependencies: z.array(z.string()).describe("Variant-specific development dependencies.").optional(),
|
|
37
|
+
files: z.array(AssetReferenceSchema).describe("Files copied for this variant.")
|
|
38
|
+
}).strict();
|
|
39
|
+
var FrameworkMappingSchema = z.object({
|
|
40
|
+
core: z.string().describe("Optional fallback template.").optional(),
|
|
41
|
+
dependencies: z.array(z.string()).describe("Dependencies for this framework mapping.").optional(),
|
|
42
|
+
devDependencies: z.array(z.string()).describe("Development dependencies for this framework mapping.").optional(),
|
|
43
|
+
variants: z.record(z.string(), VariantSchema).describe("Storage or implementation variants.")
|
|
44
|
+
}).strict();
|
|
45
|
+
function blockSlugArray(label) {
|
|
46
|
+
return z.array(
|
|
47
|
+
z.string().regex(BLOCK_SLUG_PATTERN, `${label} entries must be valid registry slugs`).min(1, `${label} entries must not be empty`)
|
|
48
|
+
).meta({ uniqueItems: true }).optional();
|
|
49
|
+
}
|
|
50
|
+
var BlockSchema = z.object({
|
|
51
|
+
name: z.string().min(1, "block name must not be empty").describe("Human-readable block name."),
|
|
52
|
+
description: z.string().min(1, "block description must not be empty").describe("Short description of the block."),
|
|
53
|
+
version: z.string().describe('Semantic version of this block (e.g. "1.0.0"). Used for update tracking.').optional(),
|
|
54
|
+
frameworks: z.array(FrameworkKeySchema).meta({ uniqueItems: true }).refine(
|
|
55
|
+
(frameworks) => new Set(frameworks).size === frameworks.length,
|
|
56
|
+
"frameworks must not contain duplicates"
|
|
57
|
+
).describe(
|
|
58
|
+
'Explicit framework support list. Named keys mean the block only supports that environment; "*" means framework-agnostic.'
|
|
59
|
+
).optional(),
|
|
60
|
+
releasedAt: z.string().regex(RELEASED_AT_PATTERN, "releasedAt must be a valid ISO date (YYYY-MM-DD)").refine(isValidCalendarDate, "releasedAt must be a valid ISO date (YYYY-MM-DD)").describe("ISO calendar date (YYYY-MM-DD) when this block was first released.").optional(),
|
|
61
|
+
runtimes: z.record(z.enum(["node"]), z.string().min(1, "runtime version range must not be empty")).describe('Supported runtime engine ranges, e.g. {"node": ">=20"}.').optional(),
|
|
62
|
+
requires: blockSlugArray("requires").describe(
|
|
63
|
+
"Registry slugs that must be installed alongside this block."
|
|
64
|
+
),
|
|
65
|
+
related: blockSlugArray("related").describe("Registry slugs that pair well with this block."),
|
|
66
|
+
conflicts: blockSlugArray("conflicts").describe(
|
|
67
|
+
"Registry slugs that must not be installed together with this block."
|
|
68
|
+
),
|
|
69
|
+
dependencies: z.array(z.string().regex(DEPENDENCY_PATTERN, "invalid dependency format")).describe("Production dependencies, optionally with version ranges.").optional(),
|
|
70
|
+
devDependencies: z.array(z.string().regex(DEPENDENCY_PATTERN, "invalid devDependency format")).describe("Development dependencies, optionally with version ranges.").optional(),
|
|
71
|
+
baseFiles: z.array(AssetMappingSchema).describe("Files copied for every install of this block.").optional(),
|
|
72
|
+
adapters: z.partialRecord(FrameworkKeySchema, FrameworkMappingSchema).describe("Framework adapter mappings.").optional(),
|
|
73
|
+
environments: z.partialRecord(FrameworkKeySchema, FrameworkMappingSchema).describe("Legacy framework mappings.").optional()
|
|
74
|
+
}).strict().refine(
|
|
75
|
+
(block) => block.baseFiles || block.adapters || block.environments,
|
|
76
|
+
"block must define at least one of: baseFiles, adapters, or environments"
|
|
77
|
+
);
|
|
78
|
+
var BlockDeclarationSchema = BlockSchema.safeExtend({
|
|
79
|
+
$schema: z.string().optional().describe("JSON Schema reference for editor support.")
|
|
80
|
+
});
|
|
81
|
+
var RegistrySchema = z.object({
|
|
82
|
+
$schema: z.string().optional(),
|
|
83
|
+
version: z.string().min(1, "registry version must not be empty").describe("Semantic version of the registry schema layout."),
|
|
84
|
+
blocks: z.record(z.string().min(1, "block key must not be empty"), BlockSchema).describe("Dictionary lookup table of all available ingestible component blocks.")
|
|
85
|
+
}).strict();
|
|
86
|
+
var blockSlug = z.string().regex(BLOCK_SLUG_PATTERN, "must be a valid registry block slug");
|
|
87
|
+
var uniqueBlockSlugs = z.array(blockSlug).meta({ uniqueItems: true }).refine(
|
|
88
|
+
(slugs) => new Set(slugs).size === slugs.length,
|
|
89
|
+
"must not contain duplicate block slugs"
|
|
90
|
+
);
|
|
91
|
+
var requiredBlockSlugs = z.array(blockSlug).min(1).meta({ uniqueItems: true }).refine(
|
|
92
|
+
(slugs) => new Set(slugs).size === slugs.length,
|
|
93
|
+
"must not contain duplicate block slugs"
|
|
94
|
+
);
|
|
95
|
+
var RecipeSchema = z.object({
|
|
96
|
+
slug: blockSlug.describe("Stable URL-safe recipe identifier."),
|
|
97
|
+
title: nonEmptyString,
|
|
98
|
+
outcome: nonEmptyString.describe("What a developer will have after following the recipe."),
|
|
99
|
+
includedBlocks: requiredBlockSlugs,
|
|
100
|
+
optionalBlocks: uniqueBlockSlugs,
|
|
101
|
+
integrationOrder: uniqueBlockSlugs.describe(
|
|
102
|
+
"A complete ordering of included and optional blocks, respecting registry requirements."
|
|
103
|
+
),
|
|
104
|
+
architecture: z.object({
|
|
105
|
+
overview: nonEmptyString,
|
|
106
|
+
components: z.array(
|
|
107
|
+
z.object({
|
|
108
|
+
name: nonEmptyString,
|
|
109
|
+
description: nonEmptyString,
|
|
110
|
+
block: blockSlug.optional()
|
|
111
|
+
}).strict()
|
|
112
|
+
)
|
|
113
|
+
}).strict(),
|
|
114
|
+
assumptions: z.array(nonEmptyString),
|
|
115
|
+
configuration: z.array(
|
|
116
|
+
z.object({
|
|
117
|
+
name: nonEmptyString,
|
|
118
|
+
description: nonEmptyString,
|
|
119
|
+
required: z.boolean(),
|
|
120
|
+
default: z.string().optional()
|
|
121
|
+
}).strict()
|
|
122
|
+
),
|
|
123
|
+
tradeoffs: z.array(
|
|
124
|
+
z.object({
|
|
125
|
+
choice: nonEmptyString,
|
|
126
|
+
tradeoff: nonEmptyString
|
|
127
|
+
}).strict()
|
|
128
|
+
),
|
|
129
|
+
alternatives: z.array(
|
|
130
|
+
z.object({
|
|
131
|
+
title: nonEmptyString,
|
|
132
|
+
description: nonEmptyString
|
|
133
|
+
}).strict()
|
|
134
|
+
).optional(),
|
|
135
|
+
failureBehavior: z.array(
|
|
136
|
+
z.object({
|
|
137
|
+
scenario: nonEmptyString,
|
|
138
|
+
behavior: nonEmptyString
|
|
139
|
+
}).strict()
|
|
140
|
+
),
|
|
141
|
+
commands: z.array(
|
|
142
|
+
z.object({
|
|
143
|
+
label: nonEmptyString,
|
|
144
|
+
command: nonEmptyString
|
|
145
|
+
}).strict()
|
|
146
|
+
),
|
|
147
|
+
relatedContent: z.array(
|
|
148
|
+
z.object({
|
|
149
|
+
title: nonEmptyString,
|
|
150
|
+
href: z.string().regex(/^(https:\/\/|\/)/, "must be an HTTPS URL or site-relative path")
|
|
151
|
+
}).strict()
|
|
152
|
+
)
|
|
153
|
+
}).strict();
|
|
154
|
+
var RecipeCollectionSchema = z.object({
|
|
155
|
+
$schema: z.string().optional(),
|
|
156
|
+
version: nonEmptyString.describe("Version of the recipe collection format."),
|
|
157
|
+
recipes: z.array(RecipeSchema).describe("Production-ready backend recipes published by Blockend.")
|
|
158
|
+
}).strict();
|
|
159
|
+
var RegistryParseError = class extends Error {
|
|
160
|
+
constructor(message, issues) {
|
|
161
|
+
super(message);
|
|
162
|
+
this.issues = issues;
|
|
163
|
+
this.name = "RegistryParseError";
|
|
164
|
+
}
|
|
165
|
+
issues;
|
|
166
|
+
};
|
|
167
|
+
function formatIssues(issues) {
|
|
168
|
+
return issues.map((issue) => {
|
|
169
|
+
const path22 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
170
|
+
return `${path22}${issue.message}`;
|
|
171
|
+
}).join("\n");
|
|
172
|
+
}
|
|
173
|
+
function isRecord(value) {
|
|
174
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
175
|
+
}
|
|
176
|
+
function isLegacyRegistry(input) {
|
|
177
|
+
if (!isRecord(input)) return false;
|
|
178
|
+
const blocks = input.blocks;
|
|
179
|
+
if (!isRecord(blocks)) {
|
|
180
|
+
return typeof input.version !== "string";
|
|
181
|
+
}
|
|
182
|
+
return "name" in blocks || "description" in blocks;
|
|
183
|
+
}
|
|
184
|
+
function parseRegistry(input, options = {}) {
|
|
185
|
+
let result = RegistrySchema.safeParse(input);
|
|
186
|
+
if (!result.success && options.allowLegacy && isLegacyRegistry(input)) {
|
|
187
|
+
result = RegistrySchema.safeParse({ version: "0.0.0", blocks: input });
|
|
188
|
+
}
|
|
189
|
+
if (!result.success) {
|
|
190
|
+
throw new RegistryParseError(
|
|
191
|
+
`Invalid registry:
|
|
192
|
+
${formatIssues(result.error.issues)}`,
|
|
193
|
+
result.error.issues
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return result.data;
|
|
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
|
+
);
|
|
20
206
|
}
|
|
21
|
-
return
|
|
207
|
+
return result.data;
|
|
22
208
|
}
|
|
209
|
+
|
|
210
|
+
// src/block-install/registry.ts
|
|
211
|
+
var DEFAULT_REPO_OWNER = "codewithnuh";
|
|
212
|
+
var DEFAULT_REPO_NAME = "blockend";
|
|
213
|
+
var DEFAULT_BRANCH = "master";
|
|
214
|
+
function createGitHubRegistrySource(options = {}) {
|
|
215
|
+
const baseUrl = options.baseUrl ?? `https://raw.githubusercontent.com/${options.owner ?? DEFAULT_REPO_OWNER}/${options.repo ?? DEFAULT_REPO_NAME}/${options.branch ?? DEFAULT_BRANCH}`;
|
|
216
|
+
return {
|
|
217
|
+
async fetchRegistry() {
|
|
218
|
+
const response = await fetch(`${baseUrl}/registry/index.json`);
|
|
219
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
220
|
+
return parseRegistry(await response.json(), { allowLegacy: true });
|
|
221
|
+
},
|
|
222
|
+
async fetchFile(sourcePath) {
|
|
223
|
+
const response = await fetch(`${baseUrl}/${sourcePath}`);
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(`Download failed: ${sourcePath} (HTTP ${response.status})`);
|
|
226
|
+
}
|
|
227
|
+
return response.text();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function loadRegistry(source) {
|
|
232
|
+
return source.fetchRegistry();
|
|
233
|
+
}
|
|
234
|
+
function getBlockMap(registry) {
|
|
235
|
+
return registry.blocks;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/block-install/plan.ts
|
|
239
|
+
import path from "path";
|
|
240
|
+
|
|
241
|
+
// src/block-install/variants.ts
|
|
242
|
+
import { join } from "path";
|
|
243
|
+
import fs from "fs/promises";
|
|
23
244
|
function selectVariant(variantKeys, preferredVariant) {
|
|
24
245
|
if (preferredVariant && variantKeys.includes(preferredVariant)) {
|
|
25
246
|
return preferredVariant;
|
|
@@ -48,6 +269,309 @@ async function resolvePreferredVariant(variantKeys, recordedVariant, targetFolde
|
|
|
48
269
|
}
|
|
49
270
|
return void 0;
|
|
50
271
|
}
|
|
272
|
+
|
|
273
|
+
// src/block-install/plan.ts
|
|
274
|
+
function blockSupportsEnv(block, environment) {
|
|
275
|
+
if (block.frameworks && block.frameworks.length > 0) {
|
|
276
|
+
return block.frameworks.includes("*") || block.frameworks.includes(environment);
|
|
277
|
+
}
|
|
278
|
+
const framework = environment;
|
|
279
|
+
return block.adapters?.[framework] !== void 0 || block.environments?.[framework] !== void 0;
|
|
280
|
+
}
|
|
281
|
+
function resolveAdapterContext(block, environment) {
|
|
282
|
+
const framework = environment;
|
|
283
|
+
return block.adapters?.[framework] ?? block.adapters?.["*"] ?? block.environments?.[framework] ?? block.environments?.["*"] ?? null;
|
|
284
|
+
}
|
|
285
|
+
function normalizeFiles(files) {
|
|
286
|
+
if (!files) return [];
|
|
287
|
+
return files.map(
|
|
288
|
+
(entry) => typeof entry === "string" ? { source: entry, target: path.basename(entry) } : entry
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
function planError(code, blockName, environment) {
|
|
292
|
+
if (code === "not-found") {
|
|
293
|
+
return {
|
|
294
|
+
code,
|
|
295
|
+
blockName,
|
|
296
|
+
environment,
|
|
297
|
+
message: `Block "${blockName}" not found in the registry.`
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (code === "unsupported-environment") {
|
|
301
|
+
return {
|
|
302
|
+
code,
|
|
303
|
+
blockName,
|
|
304
|
+
environment,
|
|
305
|
+
message: `Block "${blockName}" does not support environment: ${environment}`
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (code === "no-adapter") {
|
|
309
|
+
return {
|
|
310
|
+
code,
|
|
311
|
+
blockName,
|
|
312
|
+
environment,
|
|
313
|
+
message: `Block "${blockName}" has no adapter or environment entry for: ${environment}. This is a registry configuration issue \u2014 please open an issue.`
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
code,
|
|
318
|
+
blockName,
|
|
319
|
+
environment,
|
|
320
|
+
message: `No variants found for "${blockName}" / "${environment}".`
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
async function planBlock(registry, blockName, environment, options = {}) {
|
|
324
|
+
const block = getBlockMap(registry)[blockName];
|
|
325
|
+
if (!block) {
|
|
326
|
+
return { ok: false, error: planError("not-found", blockName, environment) };
|
|
327
|
+
}
|
|
328
|
+
if (!blockSupportsEnv(block, environment)) {
|
|
329
|
+
return { ok: false, error: planError("unsupported-environment", blockName, environment) };
|
|
330
|
+
}
|
|
331
|
+
const adapterContext = resolveAdapterContext(block, environment);
|
|
332
|
+
if (!adapterContext) {
|
|
333
|
+
return { ok: false, error: planError("no-adapter", blockName, environment) };
|
|
334
|
+
}
|
|
335
|
+
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
336
|
+
if (variantKeys.length === 0) {
|
|
337
|
+
return { ok: false, error: planError("no-variants", blockName, environment) };
|
|
338
|
+
}
|
|
339
|
+
let preferred = options.variant ?? options.recordedVariant;
|
|
340
|
+
if (options.targetFolder && (!preferred || !variantKeys.includes(preferred))) {
|
|
341
|
+
preferred = await resolvePreferredVariant(variantKeys, preferred, options.targetFolder) ?? preferred;
|
|
342
|
+
}
|
|
343
|
+
const selectedVariant = selectVariant(variantKeys, preferred);
|
|
344
|
+
const variantMeta = adapterContext.variants[selectedVariant];
|
|
345
|
+
const files = [];
|
|
346
|
+
if (block.baseFiles?.length) files.push(...block.baseFiles);
|
|
347
|
+
if (adapterContext.core) {
|
|
348
|
+
files.push({ source: adapterContext.core, target: path.basename(adapterContext.core) });
|
|
349
|
+
}
|
|
350
|
+
files.push(...normalizeFiles(variantMeta?.files));
|
|
351
|
+
const plan = {
|
|
352
|
+
blockName,
|
|
353
|
+
block,
|
|
354
|
+
environment,
|
|
355
|
+
variant: selectedVariant,
|
|
356
|
+
variantKeys,
|
|
357
|
+
files,
|
|
358
|
+
dependencies: [
|
|
359
|
+
...block.dependencies ?? [],
|
|
360
|
+
...adapterContext.dependencies ?? [],
|
|
361
|
+
...variantMeta?.dependencies ?? []
|
|
362
|
+
],
|
|
363
|
+
devDependencies: [
|
|
364
|
+
...block.devDependencies ?? [],
|
|
365
|
+
...adapterContext.devDependencies ?? [],
|
|
366
|
+
...variantMeta?.devDependencies ?? []
|
|
367
|
+
]
|
|
368
|
+
};
|
|
369
|
+
return { ok: true, plan };
|
|
370
|
+
}
|
|
371
|
+
async function listAvailableBlocks(registry, environment) {
|
|
372
|
+
const available = [];
|
|
373
|
+
for (const [name, block] of Object.entries(getBlockMap(registry))) {
|
|
374
|
+
if (!block) continue;
|
|
375
|
+
const result = await planBlock(registry, name, environment);
|
|
376
|
+
if (!result.ok) continue;
|
|
377
|
+
available.push({
|
|
378
|
+
name,
|
|
379
|
+
description: block.description,
|
|
380
|
+
variants: result.plan.variantKeys
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return available;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/block-install/preview.ts
|
|
387
|
+
import path2 from "path";
|
|
388
|
+
import fs2 from "fs/promises";
|
|
389
|
+
async function previewBlock(plan, options) {
|
|
390
|
+
const previews = [];
|
|
391
|
+
for (const file of plan.files) {
|
|
392
|
+
const targetPath = path2.join(options.targetFolder, file.target);
|
|
393
|
+
let newContent;
|
|
394
|
+
try {
|
|
395
|
+
newContent = await options.source.fetchFile(file.source);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (options.onDownloadError === "empty") {
|
|
398
|
+
newContent = "";
|
|
399
|
+
} else {
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
let existingContent = null;
|
|
404
|
+
let status = "new";
|
|
405
|
+
try {
|
|
406
|
+
existingContent = await fs2.readFile(targetPath, "utf-8");
|
|
407
|
+
status = existingContent === newContent ? "unchanged" : "modified";
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
previews.push({
|
|
411
|
+
name: file.target,
|
|
412
|
+
sourcePath: file.source,
|
|
413
|
+
targetPath,
|
|
414
|
+
existingContent,
|
|
415
|
+
newContent,
|
|
416
|
+
status
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return previews;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/block-install/apply.ts
|
|
423
|
+
import path4, { dirname as dirname3 } from "path";
|
|
424
|
+
import fs4 from "fs/promises";
|
|
425
|
+
|
|
426
|
+
// src/block-install/imports.ts
|
|
427
|
+
import path3, { dirname } from "path";
|
|
428
|
+
function resolveIndexImport(importPath, sourceFileDir, downloadedFiles, blockRoot) {
|
|
429
|
+
const resolved = path3.resolve(sourceFileDir, importPath);
|
|
430
|
+
for (const downloaded of downloadedFiles) {
|
|
431
|
+
const downloadedAbs = path3.resolve(blockRoot, downloaded);
|
|
432
|
+
const dirOfDownloaded = dirname(downloadedAbs);
|
|
433
|
+
const basenameOfDownloaded = path3.basename(downloadedAbs);
|
|
434
|
+
if (dirOfDownloaded === resolved && basenameOfDownloaded.startsWith("index.")) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
function rewriteFileImports(fileContent, writtenFilePath, blocksRoot, aliasBase, strategy, downloadedFiles = /* @__PURE__ */ new Set(), blockRoot = dirname(writtenFilePath)) {
|
|
441
|
+
const importRegex = /((?:from|import)\s*)(['"`])(\.\.?\/[^'"` \n\r]+)(['"`])/g;
|
|
442
|
+
const fileDir = dirname(writtenFilePath);
|
|
443
|
+
return fileContent.replace(importRegex, (match, keyword, openQuote, importPath, closeQuote) => {
|
|
444
|
+
let targetPath = importPath;
|
|
445
|
+
if (aliasBase && !aliasBase.startsWith(".")) {
|
|
446
|
+
const absoluteTarget = path3.resolve(fileDir, importPath);
|
|
447
|
+
const fromBlocksRoot = path3.relative(blocksRoot, absoluteTarget).replace(/\\/g, "/");
|
|
448
|
+
if (!fromBlocksRoot.startsWith("..")) {
|
|
449
|
+
targetPath = `${aliasBase}/${fromBlocksRoot}`.replace(/\/+/g, "/");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const hasJsExt = targetPath.endsWith(".js");
|
|
453
|
+
if (strategy === "rewrite" && !hasJsExt) {
|
|
454
|
+
targetPath = targetPath.replace(/\.(ts|tsx|mts|cts)$/, "");
|
|
455
|
+
if (downloadedFiles.size > 0) {
|
|
456
|
+
if (resolveIndexImport(importPath, fileDir, downloadedFiles, blockRoot)) {
|
|
457
|
+
targetPath = `${targetPath}/index`;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
targetPath = `${targetPath}.js`;
|
|
461
|
+
} else if (strategy === "remove" && hasJsExt) {
|
|
462
|
+
targetPath = targetPath.slice(0, -3);
|
|
463
|
+
}
|
|
464
|
+
return `${keyword}${openQuote}${targetPath}${closeQuote}`;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/block-install/config.ts
|
|
469
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
470
|
+
import fs3 from "fs/promises";
|
|
471
|
+
async function findUp(filename, startDir) {
|
|
472
|
+
let dir = startDir;
|
|
473
|
+
while (true) {
|
|
474
|
+
const candidate = join2(dir, filename);
|
|
475
|
+
try {
|
|
476
|
+
await fs3.access(candidate);
|
|
477
|
+
return candidate;
|
|
478
|
+
} catch {
|
|
479
|
+
const parent = dirname2(dir);
|
|
480
|
+
if (parent === dir) break;
|
|
481
|
+
dir = parent;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
async function recordInstalledBlock(configPath, record) {
|
|
487
|
+
try {
|
|
488
|
+
const raw = await fs3.readFile(configPath, "utf-8");
|
|
489
|
+
const config = JSON.parse(raw);
|
|
490
|
+
if (!Array.isArray(config.installed)) config.installed = [];
|
|
491
|
+
config.installed = config.installed.filter((block) => block.name !== record.name);
|
|
492
|
+
config.installed.push(record);
|
|
493
|
+
await fs3.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
494
|
+
} catch {
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/block-install/apply.ts
|
|
499
|
+
function hashContent(content, previous) {
|
|
500
|
+
let hash = previous;
|
|
501
|
+
for (let i = 0; i < content.length; i++) {
|
|
502
|
+
hash = (hash << 5) - hash + content.charCodeAt(i) | 0;
|
|
503
|
+
}
|
|
504
|
+
return hash;
|
|
505
|
+
}
|
|
506
|
+
async function applyBlock(plan, options) {
|
|
507
|
+
const targetFolder = path4.resolve(options.blocksRoot, plan.blockName);
|
|
508
|
+
const downloadedTargets = new Set(plan.files.map((file) => file.target));
|
|
509
|
+
const downloaded = [];
|
|
510
|
+
for (const file of plan.files) {
|
|
511
|
+
downloaded.push({ file, raw: await options.source.fetchFile(file.source) });
|
|
512
|
+
}
|
|
513
|
+
const filesWritten = [];
|
|
514
|
+
let contentHash = 0;
|
|
515
|
+
for (const { file, raw } of downloaded) {
|
|
516
|
+
const writtenFilePath = path4.join(targetFolder, file.target);
|
|
517
|
+
const content = rewriteFileImports(
|
|
518
|
+
raw,
|
|
519
|
+
writtenFilePath,
|
|
520
|
+
options.blocksRoot,
|
|
521
|
+
options.aliasBase,
|
|
522
|
+
options.rewriteStrategy,
|
|
523
|
+
downloadedTargets,
|
|
524
|
+
targetFolder
|
|
525
|
+
);
|
|
526
|
+
await fs4.mkdir(dirname3(writtenFilePath), { recursive: true });
|
|
527
|
+
await fs4.writeFile(writtenFilePath, content, "utf-8");
|
|
528
|
+
filesWritten.push(writtenFilePath);
|
|
529
|
+
contentHash = hashContent(content, contentHash);
|
|
530
|
+
}
|
|
531
|
+
const record = {
|
|
532
|
+
name: plan.blockName,
|
|
533
|
+
version: plan.block.version ?? "0.0.0",
|
|
534
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
535
|
+
files: filesWritten.map((file) => path4.relative(targetFolder, file).replace(/\\/g, "/")),
|
|
536
|
+
contentHash: contentHash.toString(16),
|
|
537
|
+
variant: plan.variant
|
|
538
|
+
};
|
|
539
|
+
await recordInstalledBlock(options.configPath, record);
|
|
540
|
+
return {
|
|
541
|
+
block: plan.blockName,
|
|
542
|
+
variant: plan.variant,
|
|
543
|
+
files: filesWritten,
|
|
544
|
+
record
|
|
545
|
+
};
|
|
546
|
+
}
|
|
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
|
+
|
|
574
|
+
// src/block-install/deps.ts
|
|
51
575
|
function parseDepSpec(dep) {
|
|
52
576
|
const atIndex = dep.lastIndexOf("@");
|
|
53
577
|
if (atIndex > 0) {
|
|
@@ -105,49 +629,8 @@ var getMissingDependencies = (deps, installed, forceLatest = false) => {
|
|
|
105
629
|
}
|
|
106
630
|
return result;
|
|
107
631
|
};
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
function resolveIndexImport(importPath, sourceFileDir, downloadedFiles) {
|
|
112
|
-
const resolved = path.resolve(sourceFileDir, importPath);
|
|
113
|
-
for (const downloaded of downloadedFiles) {
|
|
114
|
-
const downloadedAbs = path.resolve(sourceFileDir, downloaded);
|
|
115
|
-
const dirOfDownloaded = dirname(downloadedAbs);
|
|
116
|
-
const basenameOfDownloaded = path.basename(downloadedAbs);
|
|
117
|
-
if (dirOfDownloaded === resolved && basenameOfDownloaded.startsWith("index.")) {
|
|
118
|
-
return `${importPath}/index`;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
function rewriteFileImports(fileContent, writtenFilePath, blocksRoot, aliasBase, strategy, downloadedFiles = /* @__PURE__ */ new Set()) {
|
|
124
|
-
const importRegex = /((?:from|import)\s*)(['"`])(\.\.?\/[^'"` \n\r]+)(['"`])/g;
|
|
125
|
-
const fileDir = dirname(writtenFilePath);
|
|
126
|
-
return fileContent.replace(importRegex, (match, keyword, openQuote, importPath, closeQuote) => {
|
|
127
|
-
let targetPath = importPath;
|
|
128
|
-
if (aliasBase && !aliasBase.startsWith(".")) {
|
|
129
|
-
const absoluteTarget = path.resolve(fileDir, importPath);
|
|
130
|
-
const fromBlocksRoot = path.relative(blocksRoot, absoluteTarget).replace(/\\/g, "/");
|
|
131
|
-
if (!fromBlocksRoot.startsWith("..")) {
|
|
132
|
-
targetPath = `${aliasBase}/${fromBlocksRoot}`.replace(/\/+/g, "/");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
const hasJsExt = targetPath.endsWith(".js");
|
|
136
|
-
if (strategy === "rewrite" && !hasJsExt) {
|
|
137
|
-
targetPath = targetPath.replace(/\.(ts|tsx|mts|cts)$/, "");
|
|
138
|
-
if (downloadedFiles.size > 0) {
|
|
139
|
-
const indexResolved = resolveIndexImport(targetPath, fileDir, downloadedFiles);
|
|
140
|
-
if (indexResolved) {
|
|
141
|
-
targetPath = indexResolved;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
targetPath = `${targetPath}.js`;
|
|
145
|
-
} else if (strategy === "remove" && hasJsExt) {
|
|
146
|
-
targetPath = targetPath.slice(0, -3);
|
|
147
|
-
}
|
|
148
|
-
return `${keyword}${openQuote}${targetPath}${closeQuote}`;
|
|
149
|
-
});
|
|
150
|
-
}
|
|
632
|
+
|
|
633
|
+
// src/commands/add.ts
|
|
151
634
|
function outputError(json, message) {
|
|
152
635
|
if (json) {
|
|
153
636
|
process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
|
|
@@ -168,46 +651,40 @@ function handleCancel(value) {
|
|
|
168
651
|
process.exit(0);
|
|
169
652
|
}
|
|
170
653
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await fs.access(candidate);
|
|
177
|
-
return candidate;
|
|
178
|
-
} catch {
|
|
179
|
-
const parent = dirname(dir);
|
|
180
|
-
if (parent === dir) break;
|
|
181
|
-
dir = parent;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return null;
|
|
654
|
+
function failureMessage(code, blockName) {
|
|
655
|
+
if (code === "not-found") return `Block "${blockName}" not found`;
|
|
656
|
+
if (code === "unsupported-environment") return `Block "${blockName}" incompatible`;
|
|
657
|
+
if (code === "no-adapter") return `No adapter for "${blockName}"`;
|
|
658
|
+
return `No variants for "${blockName}"`;
|
|
185
659
|
}
|
|
186
|
-
async function addSingleBlock(targetBlock,
|
|
187
|
-
const { yes = false, json = false } = options;
|
|
660
|
+
async function addSingleBlock(targetBlock, context, options = {}) {
|
|
661
|
+
const { yes = false, json = false } = context.options;
|
|
662
|
+
const {
|
|
663
|
+
registry,
|
|
664
|
+
source,
|
|
665
|
+
config,
|
|
666
|
+
configPath,
|
|
667
|
+
rootDir,
|
|
668
|
+
physicalBlocksPath,
|
|
669
|
+
blocksRootAbsolute,
|
|
670
|
+
aliasBase,
|
|
671
|
+
envKey,
|
|
672
|
+
rewriteStrategy
|
|
673
|
+
} = context;
|
|
188
674
|
const s = spinner();
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
outputError(
|
|
201
|
-
json,
|
|
202
|
-
`Block "${targetBlock}" has no adapter or environment entry for: ${envKey}. This is a registry configuration issue \u2014 please open an issue.`
|
|
203
|
-
);
|
|
204
|
-
return { success: false, filesWritten: [], message: `No adapter for "${targetBlock}"` };
|
|
205
|
-
}
|
|
206
|
-
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
207
|
-
if (variantKeys.length === 0) {
|
|
208
|
-
outputError(json, `No variants found for "${targetBlock}" / "${envKey}".`);
|
|
209
|
-
return { success: false, filesWritten: [], message: `No variants for "${targetBlock}"` };
|
|
675
|
+
const reportError = (message) => {
|
|
676
|
+
if (!options.suppressErrorOutput) outputError(json, message);
|
|
677
|
+
};
|
|
678
|
+
const initialPlan = await planBlock(registry, targetBlock, envKey);
|
|
679
|
+
if (!initialPlan.ok) {
|
|
680
|
+
reportError(initialPlan.error.message);
|
|
681
|
+
return {
|
|
682
|
+
success: false,
|
|
683
|
+
filesWritten: [],
|
|
684
|
+
message: failureMessage(initialPlan.error.code, targetBlock)
|
|
685
|
+
};
|
|
210
686
|
}
|
|
687
|
+
const variantKeys = initialPlan.plan.variantKeys;
|
|
211
688
|
let selectedVariant;
|
|
212
689
|
if (yes) {
|
|
213
690
|
selectedVariant = variantKeys.includes("memory") ? "memory" : variantKeys[0];
|
|
@@ -218,24 +695,38 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
218
695
|
} else {
|
|
219
696
|
const variantPrompt = await select({
|
|
220
697
|
message: `Select a storage variant for ${targetBlock}:`,
|
|
221
|
-
options: variantKeys.map((
|
|
698
|
+
options: variantKeys.map((variant) => ({ value: variant, label: variant.toUpperCase() }))
|
|
222
699
|
});
|
|
223
700
|
handleCancel(variantPrompt);
|
|
224
701
|
selectedVariant = variantPrompt;
|
|
225
702
|
}
|
|
226
|
-
|
|
227
|
-
|
|
703
|
+
let plan = initialPlan.plan;
|
|
704
|
+
if (selectedVariant !== initialPlan.plan.variant) {
|
|
705
|
+
const selectedPlan = await planBlock(registry, targetBlock, envKey, {
|
|
706
|
+
variant: selectedVariant
|
|
707
|
+
});
|
|
708
|
+
if (!selectedPlan.ok) {
|
|
709
|
+
reportError(selectedPlan.error.message);
|
|
710
|
+
return {
|
|
711
|
+
success: false,
|
|
712
|
+
filesWritten: [],
|
|
713
|
+
message: failureMessage(selectedPlan.error.code, targetBlock)
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
plan = selectedPlan.plan;
|
|
717
|
+
}
|
|
718
|
+
const targetFolder = path5.resolve(blocksRootAbsolute, targetBlock);
|
|
228
719
|
const packageJsonPath = await findUp("package.json", rootDir);
|
|
229
720
|
if (!packageJsonPath) {
|
|
230
|
-
|
|
721
|
+
reportError("Could not locate package.json.");
|
|
231
722
|
return { success: false, filesWritten: [], message: "package.json not found" };
|
|
232
723
|
}
|
|
233
|
-
const packageJsonDir =
|
|
724
|
+
const packageJsonDir = dirname4(packageJsonPath);
|
|
234
725
|
let packageJson;
|
|
235
726
|
try {
|
|
236
|
-
packageJson = JSON.parse(await
|
|
727
|
+
packageJson = JSON.parse(await fs5.readFile(packageJsonPath, "utf-8"));
|
|
237
728
|
} catch (err) {
|
|
238
|
-
|
|
729
|
+
reportError("Failed to parse package.json.");
|
|
239
730
|
if (!json) log.error(pc.dim(String(err)));
|
|
240
731
|
return { success: false, filesWritten: [], message: "Failed to parse package.json" };
|
|
241
732
|
}
|
|
@@ -243,20 +734,10 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
243
734
|
...packageJson.dependencies ?? {},
|
|
244
735
|
...packageJson.devDependencies ?? {}
|
|
245
736
|
};
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
];
|
|
251
|
-
const allBlockDevDeps = [
|
|
252
|
-
...blockMeta.devDependencies ?? [],
|
|
253
|
-
...adapterContext.devDependencies ?? [],
|
|
254
|
-
...variantMeta?.devDependencies ?? []
|
|
255
|
-
];
|
|
256
|
-
const missingProd = getMissingDependencies(allBlockDeps, installedDeps, true);
|
|
257
|
-
const missingDev = getMissingDependencies(allBlockDevDeps, installedDeps, true);
|
|
258
|
-
const prodConflicts = getVersionConflicts(allBlockDeps, installedDeps);
|
|
259
|
-
const devConflicts = getVersionConflicts(allBlockDevDeps, installedDeps);
|
|
737
|
+
const missingProd = getMissingDependencies(plan.dependencies, installedDeps, true);
|
|
738
|
+
const missingDev = getMissingDependencies(plan.devDependencies, installedDeps, true);
|
|
739
|
+
const prodConflicts = getVersionConflicts(plan.dependencies, installedDeps);
|
|
740
|
+
const devConflicts = getVersionConflicts(plan.devDependencies, installedDeps);
|
|
260
741
|
const allConflicts = [...prodConflicts, ...devConflicts];
|
|
261
742
|
if (allConflicts.length > 0 && !json) {
|
|
262
743
|
log.warn(`Version conflicts detected:`);
|
|
@@ -272,7 +753,7 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
272
753
|
});
|
|
273
754
|
if (!yes) handleCancel(shouldInstall);
|
|
274
755
|
if (shouldInstall) {
|
|
275
|
-
const pm =
|
|
756
|
+
const pm = config.packageManager || "npm";
|
|
276
757
|
if (!json) s.start(`Installing via ${pm}...`);
|
|
277
758
|
const tasks = [];
|
|
278
759
|
if (missingProd.length > 0) {
|
|
@@ -307,32 +788,15 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
307
788
|
if (!json) {
|
|
308
789
|
s.stop("Installation failed.");
|
|
309
790
|
log.error(pc.dim(String(err)));
|
|
310
|
-
} else
|
|
791
|
+
} else reportError("Dependency installation failed.");
|
|
311
792
|
return { success: false, filesWritten: [], message: "Dependency installation failed" };
|
|
312
793
|
}
|
|
313
794
|
}
|
|
314
795
|
}
|
|
315
|
-
const filesToDownload = [];
|
|
316
|
-
if (blockMeta.baseFiles?.length) {
|
|
317
|
-
filesToDownload.push(...blockMeta.baseFiles);
|
|
318
|
-
}
|
|
319
|
-
if (adapterContext.core) {
|
|
320
|
-
filesToDownload.push({
|
|
321
|
-
source: adapterContext.core,
|
|
322
|
-
target: path.basename(adapterContext.core)
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
326
|
-
for (const entry of variantMeta.files) {
|
|
327
|
-
filesToDownload.push(
|
|
328
|
-
typeof entry === "string" ? { source: entry, target: path.basename(entry) } : entry
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
796
|
let hasConflict = false;
|
|
333
|
-
for (const
|
|
797
|
+
for (const file of plan.files) {
|
|
334
798
|
try {
|
|
335
|
-
await
|
|
799
|
+
await fs5.access(join3(targetFolder, file.target));
|
|
336
800
|
hasConflict = true;
|
|
337
801
|
break;
|
|
338
802
|
} catch {
|
|
@@ -349,69 +813,82 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
349
813
|
}
|
|
350
814
|
}
|
|
351
815
|
if (!json) s.start(`Downloading ${targetBlock}...`);
|
|
352
|
-
const filesWritten = [];
|
|
353
|
-
const downloadedTargets = /* @__PURE__ */ new Set();
|
|
354
816
|
try {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
let contentHash = 0;
|
|
363
|
-
for (const { fm, raw } of fileBatches) {
|
|
364
|
-
const writtenFilePath = path.join(targetFolder, fm.target);
|
|
365
|
-
const content = rewriteFileImports(
|
|
366
|
-
raw,
|
|
367
|
-
writtenFilePath,
|
|
368
|
-
blocksRootAbsolute,
|
|
369
|
-
aliasBase,
|
|
370
|
-
rewriteStrategy,
|
|
371
|
-
downloadedTargets
|
|
372
|
-
);
|
|
373
|
-
await fs.mkdir(dirname(writtenFilePath), { recursive: true });
|
|
374
|
-
await fs.writeFile(writtenFilePath, content, "utf-8");
|
|
375
|
-
filesWritten.push(writtenFilePath);
|
|
376
|
-
for (let i = 0; i < content.length; i++) {
|
|
377
|
-
contentHash = (contentHash << 5) - contentHash + content.charCodeAt(i) | 0;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
const blockVersion = blockMeta.version ?? "0.0.0";
|
|
381
|
-
const installedRecord = {
|
|
382
|
-
name: targetBlock,
|
|
383
|
-
version: blockVersion,
|
|
384
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
385
|
-
files: filesWritten.map((f) => path.relative(targetFolder, f).replace(/\\/g, "/")),
|
|
386
|
-
contentHash: contentHash.toString(16),
|
|
387
|
-
variant: selectedVariant
|
|
388
|
-
};
|
|
389
|
-
try {
|
|
390
|
-
const blockendPath = join(rootDir, "blockend.json");
|
|
391
|
-
const cfgRaw = await fs.readFile(blockendPath, "utf-8");
|
|
392
|
-
const cfgData = JSON.parse(cfgRaw);
|
|
393
|
-
if (!Array.isArray(cfgData.installed)) cfgData.installed = [];
|
|
394
|
-
cfgData.installed = cfgData.installed.filter((b) => b.name !== targetBlock);
|
|
395
|
-
cfgData.installed.push(installedRecord);
|
|
396
|
-
await fs.writeFile(blockendPath, JSON.stringify(cfgData, null, 2), "utf-8");
|
|
397
|
-
} catch {
|
|
398
|
-
}
|
|
817
|
+
const applied = await applyBlock(plan, {
|
|
818
|
+
source,
|
|
819
|
+
blocksRoot: blocksRootAbsolute,
|
|
820
|
+
aliasBase,
|
|
821
|
+
rewriteStrategy,
|
|
822
|
+
configPath
|
|
823
|
+
});
|
|
399
824
|
if (!json) s.stop("Files written.");
|
|
400
825
|
return {
|
|
401
826
|
success: true,
|
|
402
|
-
filesWritten,
|
|
827
|
+
filesWritten: applied.files,
|
|
403
828
|
message: `${targetBlock} added to ${physicalBlocksPath}/${targetBlock}`
|
|
404
829
|
};
|
|
405
830
|
} catch (err) {
|
|
406
831
|
if (!json) {
|
|
407
832
|
s.stop("Failed.");
|
|
408
833
|
log.error(pc.dim(String(err)));
|
|
409
|
-
} else
|
|
834
|
+
} else reportError("Fatal error while writing block files.");
|
|
410
835
|
return { success: false, filesWritten: [], message: `Failed to add ${targetBlock}` };
|
|
411
836
|
}
|
|
412
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
|
+
}
|
|
413
890
|
async function addCommand(blockName, options = {}) {
|
|
414
|
-
const { yes = false, json = false, multi = false } = options;
|
|
891
|
+
const { yes = false, json = false, multi = false, recipe } = options;
|
|
415
892
|
if (!json) {
|
|
416
893
|
console.log("");
|
|
417
894
|
intro(`${pc.bgCyan(pc.black(" blockend "))} ${pc.dim("add")}`);
|
|
@@ -422,10 +899,10 @@ async function addCommand(blockName, options = {}) {
|
|
|
422
899
|
outputError(json, "blockend.json not found. Run 'npx blockend init' first.");
|
|
423
900
|
return;
|
|
424
901
|
}
|
|
425
|
-
const rootDir =
|
|
902
|
+
const rootDir = dirname4(configPath);
|
|
426
903
|
let config;
|
|
427
904
|
try {
|
|
428
|
-
config = JSON.parse(await
|
|
905
|
+
config = JSON.parse(await fs5.readFile(configPath, "utf-8"));
|
|
429
906
|
} catch (err) {
|
|
430
907
|
outputError(json, "Failed to parse blockend.json.");
|
|
431
908
|
if (!json) log.error(pc.dim(String(err)));
|
|
@@ -436,17 +913,16 @@ async function addCommand(blockName, options = {}) {
|
|
|
436
913
|
return;
|
|
437
914
|
}
|
|
438
915
|
const physicalBlocksPath = config.paths.blocks;
|
|
439
|
-
const blocksRootAbsolute =
|
|
916
|
+
const blocksRootAbsolute = path5.resolve(rootDir, physicalBlocksPath);
|
|
440
917
|
const aliasBase = config.aliases.blocks;
|
|
441
918
|
const envKey = config.environment;
|
|
442
919
|
const rewriteStrategy = config.importRewriteStrategy ?? "remove";
|
|
920
|
+
const source = createGitHubRegistrySource();
|
|
443
921
|
const s = spinner();
|
|
444
922
|
if (!json) s.start("Fetching registry...");
|
|
445
923
|
let registry;
|
|
446
924
|
try {
|
|
447
|
-
|
|
448
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
449
|
-
registry = await res.json();
|
|
925
|
+
registry = await loadRegistry(source);
|
|
450
926
|
if (!json) s.stop("Registry synced.");
|
|
451
927
|
} catch (err) {
|
|
452
928
|
if (!json) {
|
|
@@ -455,26 +931,50 @@ async function addCommand(blockName, options = {}) {
|
|
|
455
931
|
} else outputError(json, "Failed to fetch registry from GitHub.");
|
|
456
932
|
return;
|
|
457
933
|
}
|
|
458
|
-
const
|
|
459
|
-
|
|
934
|
+
const context = {
|
|
935
|
+
registry,
|
|
936
|
+
source,
|
|
937
|
+
config,
|
|
938
|
+
configPath,
|
|
939
|
+
rootDir,
|
|
940
|
+
physicalBlocksPath,
|
|
941
|
+
blocksRootAbsolute,
|
|
942
|
+
aliasBase,
|
|
943
|
+
envKey,
|
|
944
|
+
rewriteStrategy,
|
|
945
|
+
options
|
|
946
|
+
};
|
|
947
|
+
if (yes && !blockName && !recipe) {
|
|
460
948
|
outputError(json, "Block name required with --yes. Usage: blockend add <block> --yes");
|
|
461
949
|
return;
|
|
462
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
|
+
}
|
|
463
963
|
if (multi && !blockName) {
|
|
464
|
-
const available =
|
|
465
|
-
([, block]) => block != null && blockSupportsEnv(block, envKey)
|
|
466
|
-
);
|
|
964
|
+
const available = await listAvailableBlocks(registry, envKey);
|
|
467
965
|
if (available.length === 0) {
|
|
468
966
|
outputError(json, `No blocks available for environment: ${envKey}`);
|
|
469
967
|
if (!json) outro(pc.dim("Exiting."));
|
|
470
968
|
return;
|
|
471
969
|
}
|
|
472
|
-
const maxKeyLen = Math.max(...available.map((
|
|
970
|
+
const maxKeyLen = Math.max(...available.map((block) => block.name.length), 0);
|
|
473
971
|
const multiSelectPrompt = await multiselect({
|
|
474
972
|
message: "Select blocks to add (space to select, enter to confirm):",
|
|
475
|
-
options: available.map((
|
|
476
|
-
value:
|
|
477
|
-
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(
|
|
973
|
+
options: available.map((block, i) => ({
|
|
974
|
+
value: block.name,
|
|
975
|
+
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(
|
|
976
|
+
pc.cyan(block.name.padEnd(maxKeyLen + 4))
|
|
977
|
+
)}${pc.dim(
|
|
478
978
|
block.description.length > 60 ? `${block.description.slice(0, 60)}...` : block.description
|
|
479
979
|
)}`
|
|
480
980
|
}))
|
|
@@ -490,18 +990,7 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
490
990
|
`);
|
|
491
991
|
const results = [];
|
|
492
992
|
for (const block of selectedBlocks) {
|
|
493
|
-
const result2 = await addSingleBlock(
|
|
494
|
-
block,
|
|
495
|
-
blockMap,
|
|
496
|
-
config,
|
|
497
|
-
rootDir,
|
|
498
|
-
physicalBlocksPath,
|
|
499
|
-
blocksRootAbsolute,
|
|
500
|
-
aliasBase,
|
|
501
|
-
envKey,
|
|
502
|
-
rewriteStrategy,
|
|
503
|
-
options
|
|
504
|
-
);
|
|
993
|
+
const result2 = await addSingleBlock(block, context);
|
|
505
994
|
if (result2.success) {
|
|
506
995
|
results.push(result2.message);
|
|
507
996
|
}
|
|
@@ -516,20 +1005,20 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
516
1005
|
}
|
|
517
1006
|
let targetBlock = blockName;
|
|
518
1007
|
if (!targetBlock) {
|
|
519
|
-
const available =
|
|
520
|
-
([, block]) => block != null && blockSupportsEnv(block, envKey)
|
|
521
|
-
);
|
|
1008
|
+
const available = await listAvailableBlocks(registry, envKey);
|
|
522
1009
|
if (available.length === 0) {
|
|
523
1010
|
outputError(json, `No blocks available for environment: ${envKey}`);
|
|
524
1011
|
if (!json) outro(pc.dim("Exiting."));
|
|
525
1012
|
return;
|
|
526
1013
|
}
|
|
527
|
-
const maxKeyLen = Math.max(...available.map((
|
|
1014
|
+
const maxKeyLen = Math.max(...available.map((block) => block.name.length), 0);
|
|
528
1015
|
const selectPrompt = await select({
|
|
529
1016
|
message: "Which block would you like to add?",
|
|
530
|
-
options: available.map((
|
|
531
|
-
value:
|
|
532
|
-
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(
|
|
1017
|
+
options: available.map((block, i) => ({
|
|
1018
|
+
value: block.name,
|
|
1019
|
+
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(
|
|
1020
|
+
pc.cyan(block.name.padEnd(maxKeyLen + 4))
|
|
1021
|
+
)}${pc.dim(
|
|
533
1022
|
block.description.length > 60 ? `${block.description.slice(0, 60)}...` : block.description
|
|
534
1023
|
)}`
|
|
535
1024
|
}))
|
|
@@ -537,18 +1026,7 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
537
1026
|
handleCancel(selectPrompt);
|
|
538
1027
|
targetBlock = selectPrompt;
|
|
539
1028
|
}
|
|
540
|
-
const result = await addSingleBlock(
|
|
541
|
-
targetBlock,
|
|
542
|
-
blockMap,
|
|
543
|
-
config,
|
|
544
|
-
rootDir,
|
|
545
|
-
physicalBlocksPath,
|
|
546
|
-
blocksRootAbsolute,
|
|
547
|
-
aliasBase,
|
|
548
|
-
envKey,
|
|
549
|
-
rewriteStrategy,
|
|
550
|
-
options
|
|
551
|
-
);
|
|
1029
|
+
const result = await addSingleBlock(targetBlock, context);
|
|
552
1030
|
outputResult(json, {
|
|
553
1031
|
success: result.success,
|
|
554
1032
|
block: targetBlock,
|
|
@@ -558,20 +1036,20 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
558
1036
|
}
|
|
559
1037
|
|
|
560
1038
|
// src/commands/init.ts
|
|
561
|
-
import
|
|
562
|
-
import
|
|
1039
|
+
import path7, { join as join9 } from "path";
|
|
1040
|
+
import fs6 from "fs/promises";
|
|
563
1041
|
import { existsSync as existsSync4 } from "fs";
|
|
564
1042
|
import { intro as intro2, outro as outro2, select as select2, text, confirm as confirm2, spinner as spinner2, isCancel as isCancel2, log as log2 } from "@clack/prompts";
|
|
565
1043
|
import pc3 from "picocolors";
|
|
566
1044
|
|
|
567
1045
|
// src/detectors/index.ts
|
|
568
|
-
import { join as
|
|
1046
|
+
import path6, { join as join8 } from "path";
|
|
569
1047
|
|
|
570
1048
|
// src/detectors/readers/package.ts
|
|
571
1049
|
import { readFile } from "fs/promises";
|
|
572
|
-
import { join as
|
|
1050
|
+
import { join as join4 } from "path";
|
|
573
1051
|
async function readPackageJson(cwd) {
|
|
574
|
-
const pkgPath =
|
|
1052
|
+
const pkgPath = join4(cwd, "package.json");
|
|
575
1053
|
try {
|
|
576
1054
|
const content = await readFile(pkgPath, "utf-8");
|
|
577
1055
|
return JSON.parse(content);
|
|
@@ -583,30 +1061,122 @@ async function readPackageJson(cwd) {
|
|
|
583
1061
|
// src/detectors/readers/tsconfig.ts
|
|
584
1062
|
import { readFile as readFile2 } from "fs/promises";
|
|
585
1063
|
import { existsSync } from "fs";
|
|
586
|
-
import { join as
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1064
|
+
import { join as join5 } from "path";
|
|
1065
|
+
var CONFIG_FILES = [
|
|
1066
|
+
{ filename: "tsconfig.json", kind: "tsconfig" },
|
|
1067
|
+
{ filename: "jsconfig.json", kind: "jsconfig" }
|
|
1068
|
+
];
|
|
1069
|
+
async function readCompilerConfig(cwd) {
|
|
1070
|
+
for (const { filename, kind } of CONFIG_FILES) {
|
|
1071
|
+
const configPath = join5(cwd, filename);
|
|
1072
|
+
if (!existsSync(configPath)) continue;
|
|
1073
|
+
try {
|
|
1074
|
+
const content = await readFile2(configPath, "utf-8");
|
|
1075
|
+
const clean = stripTrailingCommas(stripJsonComments(content));
|
|
1076
|
+
return {
|
|
1077
|
+
kind,
|
|
1078
|
+
config: JSON.parse(clean)
|
|
1079
|
+
};
|
|
1080
|
+
} catch {
|
|
1081
|
+
}
|
|
599
1082
|
}
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
function stripJsonComments(input) {
|
|
1086
|
+
let output = "";
|
|
1087
|
+
let inString = false;
|
|
1088
|
+
let inLineComment = false;
|
|
1089
|
+
let inBlockComment = false;
|
|
1090
|
+
let escaped = false;
|
|
1091
|
+
for (let i = 0; i < input.length; i++) {
|
|
1092
|
+
const char = input[i];
|
|
1093
|
+
const next = input[i + 1];
|
|
1094
|
+
if (inLineComment) {
|
|
1095
|
+
if (char === "\n" || char === "\r") {
|
|
1096
|
+
inLineComment = false;
|
|
1097
|
+
output += char;
|
|
1098
|
+
}
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
if (inBlockComment) {
|
|
1102
|
+
if (char === "*" && next === "/") {
|
|
1103
|
+
inBlockComment = false;
|
|
1104
|
+
i++;
|
|
1105
|
+
} else if (char === "\n" || char === "\r") {
|
|
1106
|
+
output += char;
|
|
1107
|
+
}
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
if (inString) {
|
|
1111
|
+
output += char;
|
|
1112
|
+
if (escaped) {
|
|
1113
|
+
escaped = false;
|
|
1114
|
+
} else if (char === "\\") {
|
|
1115
|
+
escaped = true;
|
|
1116
|
+
} else if (char === '"') {
|
|
1117
|
+
inString = false;
|
|
1118
|
+
}
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (char === '"') {
|
|
1122
|
+
inString = true;
|
|
1123
|
+
output += char;
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
if (char === "/" && next === "/") {
|
|
1127
|
+
inLineComment = true;
|
|
1128
|
+
i++;
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (char === "/" && next === "*") {
|
|
1132
|
+
inBlockComment = true;
|
|
1133
|
+
i++;
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
output += char;
|
|
1137
|
+
}
|
|
1138
|
+
return output;
|
|
1139
|
+
}
|
|
1140
|
+
function stripTrailingCommas(input) {
|
|
1141
|
+
let output = "";
|
|
1142
|
+
let inString = false;
|
|
1143
|
+
let escaped = false;
|
|
1144
|
+
for (let i = 0; i < input.length; i++) {
|
|
1145
|
+
const char = input[i];
|
|
1146
|
+
if (inString) {
|
|
1147
|
+
output += char;
|
|
1148
|
+
if (escaped) {
|
|
1149
|
+
escaped = false;
|
|
1150
|
+
} else if (char === "\\") {
|
|
1151
|
+
escaped = true;
|
|
1152
|
+
} else if (char === '"') {
|
|
1153
|
+
inString = false;
|
|
1154
|
+
}
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
if (char === '"') {
|
|
1158
|
+
inString = true;
|
|
1159
|
+
output += char;
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
if (char === ",") {
|
|
1163
|
+
let next = i + 1;
|
|
1164
|
+
while (next < input.length && /\s/.test(input[next])) next++;
|
|
1165
|
+
if (input[next] === "}" || input[next] === "]") continue;
|
|
1166
|
+
}
|
|
1167
|
+
output += char;
|
|
1168
|
+
}
|
|
1169
|
+
return output;
|
|
600
1170
|
}
|
|
601
1171
|
|
|
602
1172
|
// src/detectors/readers/filesystem.ts
|
|
603
1173
|
import { existsSync as existsSync2 } from "fs";
|
|
604
|
-
import { join as
|
|
1174
|
+
import { join as join6 } from "path";
|
|
605
1175
|
async function inferSrcDir(cwd) {
|
|
606
1176
|
const candidates = ["src", "app", "lib"];
|
|
607
1177
|
for (const dir of candidates) {
|
|
608
|
-
if (existsSync2(
|
|
609
|
-
return
|
|
1178
|
+
if (existsSync2(join6(cwd, dir))) {
|
|
1179
|
+
return join6(cwd, dir);
|
|
610
1180
|
}
|
|
611
1181
|
}
|
|
612
1182
|
return cwd;
|
|
@@ -629,9 +1199,9 @@ function detectRuntime(deps) {
|
|
|
629
1199
|
|
|
630
1200
|
// src/detectors/package-manager.ts
|
|
631
1201
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
632
|
-
import { join as
|
|
1202
|
+
import { join as join7 } from "path";
|
|
633
1203
|
function detectPackageManager(cwd) {
|
|
634
|
-
const pkgJsonPath =
|
|
1204
|
+
const pkgJsonPath = join7(cwd, "package.json");
|
|
635
1205
|
if (existsSync3(pkgJsonPath)) {
|
|
636
1206
|
try {
|
|
637
1207
|
const packageJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
@@ -644,52 +1214,76 @@ function detectPackageManager(cwd) {
|
|
|
644
1214
|
} catch {
|
|
645
1215
|
}
|
|
646
1216
|
}
|
|
647
|
-
if (existsSync3(
|
|
648
|
-
if (existsSync3(
|
|
649
|
-
if (existsSync3(
|
|
650
|
-
return "npm";
|
|
1217
|
+
if (existsSync3(join7(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1218
|
+
if (existsSync3(join7(cwd, "bun.lockb"))) return "bun";
|
|
1219
|
+
if (existsSync3(join7(cwd, "yarn.lock"))) return "yarn";
|
|
1220
|
+
return "npm";
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/detectors/alias.ts
|
|
1224
|
+
function flattenTsPaths(paths) {
|
|
1225
|
+
const result = {};
|
|
1226
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
1227
|
+
if (Array.isArray(targets) && typeof targets[0] === "string" && targets[0].length > 0) {
|
|
1228
|
+
result[alias.replace("/*", "/")] = targets[0].replace("/*", "/");
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return result;
|
|
1232
|
+
}
|
|
1233
|
+
function deriveBlocksAlias(tsPaths, blocksPhysicalPath) {
|
|
1234
|
+
const normalizedBlocksPath = blocksPhysicalPath.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
1235
|
+
for (const [aliasKey, targets] of Object.entries(tsPaths)) {
|
|
1236
|
+
if (!Array.isArray(targets) || typeof targets[0] !== "string") continue;
|
|
1237
|
+
const rawTarget = targets[0].replace(/\\/g, "/");
|
|
1238
|
+
const targetRoot = rawTarget.replace(/\*$/, "").replace(/\/$/, "").replace(/^\.\//, "");
|
|
1239
|
+
const aliasRoot = aliasKey.replace(/\/\*$/, "").replace(/\*$/, "");
|
|
1240
|
+
if (targetRoot === "") {
|
|
1241
|
+
return `${aliasRoot}/${normalizedBlocksPath}`.replace(/\/+/g, "/");
|
|
1242
|
+
}
|
|
1243
|
+
if (normalizedBlocksPath === targetRoot || normalizedBlocksPath.startsWith(`${targetRoot}/`)) {
|
|
1244
|
+
const suffix = normalizedBlocksPath.slice(targetRoot.length).replace(/^\//, "");
|
|
1245
|
+
const alias = suffix ? `${aliasRoot}/${suffix}` : aliasRoot;
|
|
1246
|
+
return alias.replace(/\/+/g, "/");
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return void 0;
|
|
651
1250
|
}
|
|
652
1251
|
|
|
653
1252
|
// src/detectors/index.ts
|
|
654
|
-
async function detectProject(cwd) {
|
|
655
|
-
const [pkg,
|
|
1253
|
+
async function detectProject(cwd, options = {}) {
|
|
1254
|
+
const [pkg, compilerConfig] = await Promise.all([readPackageJson(cwd), readCompilerConfig(cwd)]);
|
|
656
1255
|
const allDeps = {
|
|
657
1256
|
...pkg.dependencies,
|
|
658
1257
|
...pkg.devDependencies,
|
|
659
1258
|
...pkg.peerDependencies
|
|
660
1259
|
};
|
|
661
1260
|
const srcDir = await inferSrcDir(cwd);
|
|
662
|
-
const
|
|
663
|
-
const
|
|
1261
|
+
const compilerOptions = compilerConfig?.config.compilerOptions;
|
|
1262
|
+
const moduleOpt = compilerOptions?.module?.toLocaleLowerCase();
|
|
1263
|
+
const resolutionOpt = compilerOptions?.moduleResolution?.toLocaleLowerCase();
|
|
664
1264
|
let importRewriteStrategy = "remove";
|
|
665
1265
|
if (moduleOpt === "nodenext" || moduleOpt === "node16" || resolutionOpt === "nodenext" || resolutionOpt === "node16" || pkg.type === "module" && !resolutionOpt) {
|
|
666
1266
|
importRewriteStrategy = "rewrite";
|
|
667
1267
|
}
|
|
1268
|
+
const blocksDir = options.blocksDir ? path6.resolve(cwd, options.blocksDir) : join8(srcDir, "lib", "blocks");
|
|
1269
|
+
const relativeBlocksDir = path6.relative(cwd, blocksDir).replace(/\\/g, "/");
|
|
1270
|
+
const tsPaths = compilerOptions?.paths;
|
|
668
1271
|
return {
|
|
669
1272
|
root: cwd,
|
|
670
|
-
language:
|
|
1273
|
+
language: compilerConfig?.kind === "tsconfig" ? "typescript" : "javascript",
|
|
671
1274
|
runtime: detectRuntime(allDeps),
|
|
672
1275
|
framework: detectFramework(allDeps),
|
|
673
1276
|
packageManager: detectPackageManager(cwd),
|
|
674
1277
|
hasRedis: "ioredis" in allDeps || "redis" in allDeps,
|
|
675
1278
|
hasPrisma: "@prisma/client" in allDeps,
|
|
676
1279
|
hasDrizzle: "drizzle-orm" in allDeps,
|
|
677
|
-
aliasMap:
|
|
1280
|
+
aliasMap: tsPaths ? flattenTsPaths(tsPaths) : {},
|
|
678
1281
|
srcDir,
|
|
679
|
-
|
|
680
|
-
|
|
1282
|
+
blocksDir,
|
|
1283
|
+
blocksAlias: tsPaths ? deriveBlocksAlias(tsPaths, relativeBlocksDir) : void 0,
|
|
681
1284
|
importRewriteStrategy
|
|
682
1285
|
};
|
|
683
1286
|
}
|
|
684
|
-
function flattenTsPaths(paths) {
|
|
685
|
-
const result = {};
|
|
686
|
-
for (const [alias, targets] of Object.entries(paths)) {
|
|
687
|
-
if (targets[0]) {
|
|
688
|
-
result[alias.replace("/*", "/")] = targets[0].replace("/*", "/");
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
return result;
|
|
692
|
-
}
|
|
693
1287
|
|
|
694
1288
|
// src/ui/theme.ts
|
|
695
1289
|
import pc2 from "picocolors";
|
|
@@ -728,41 +1322,6 @@ var format = {
|
|
|
728
1322
|
};
|
|
729
1323
|
|
|
730
1324
|
// src/commands/init.ts
|
|
731
|
-
async function resolveTsConfigPaths(cwd) {
|
|
732
|
-
const candidates = [join7(cwd, "tsconfig.json"), join7(cwd, "jsconfig.json")];
|
|
733
|
-
for (const configPath of candidates) {
|
|
734
|
-
if (!existsSync4(configPath)) continue;
|
|
735
|
-
try {
|
|
736
|
-
const raw = await fs2.readFile(configPath, "utf-8");
|
|
737
|
-
const clean = raw.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
|
|
738
|
-
const parsed = JSON.parse(clean);
|
|
739
|
-
return {
|
|
740
|
-
baseUrl: parsed.compilerOptions?.baseUrl ?? ".",
|
|
741
|
-
paths: parsed.compilerOptions?.paths ?? {}
|
|
742
|
-
};
|
|
743
|
-
} catch {
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
return { baseUrl: ".", paths: {} };
|
|
747
|
-
}
|
|
748
|
-
function deriveBlockAlias(tsPaths, blocksPhysicalPath) {
|
|
749
|
-
const normalizedBlocksPath = blocksPhysicalPath.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
750
|
-
for (const [aliasKey, targets] of Object.entries(tsPaths)) {
|
|
751
|
-
if (!Array.isArray(targets) || targets.length === 0) continue;
|
|
752
|
-
const rawTarget = targets[0].replace(/\\/g, "/");
|
|
753
|
-
const targetRoot = rawTarget.replace(/\*$/, "").replace(/\/$/, "").replace(/^\.\//, "");
|
|
754
|
-
const aliasRoot = aliasKey.replace(/\/\*$/, "").replace(/\*$/, "");
|
|
755
|
-
if (targetRoot === "") {
|
|
756
|
-
return `${aliasRoot}/${normalizedBlocksPath}`.replace(/\/+/g, "/");
|
|
757
|
-
}
|
|
758
|
-
if (normalizedBlocksPath === targetRoot || normalizedBlocksPath.startsWith(`${targetRoot}/`)) {
|
|
759
|
-
const suffix = normalizedBlocksPath.slice(targetRoot.length).replace(/^\//, "");
|
|
760
|
-
const alias = suffix ? `${aliasRoot}/${suffix}` : aliasRoot;
|
|
761
|
-
return alias.replace(/\/+/g, "/");
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return void 0;
|
|
765
|
-
}
|
|
766
1325
|
function outputInitError(json, message) {
|
|
767
1326
|
if (json) {
|
|
768
1327
|
process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
|
|
@@ -781,7 +1340,7 @@ function outputInitResult(json, result) {
|
|
|
781
1340
|
async function initCommand(options = {}) {
|
|
782
1341
|
const { yes = false, json = false } = options;
|
|
783
1342
|
const cwd = process.cwd();
|
|
784
|
-
const configPath =
|
|
1343
|
+
const configPath = join9(cwd, "blockend.json");
|
|
785
1344
|
if (!json) {
|
|
786
1345
|
console.log("");
|
|
787
1346
|
intro2(`${pc3.bgCyan(pc3.black(" blockend "))} ${theme.text.muted("setup")}`);
|
|
@@ -813,15 +1372,35 @@ async function initCommand(options = {}) {
|
|
|
813
1372
|
action = actionPrompt;
|
|
814
1373
|
}
|
|
815
1374
|
if (action === "regenerate") {
|
|
816
|
-
await
|
|
1375
|
+
await fs6.unlink(configPath);
|
|
817
1376
|
if (!json) log2.warn("Existing configuration deleted.");
|
|
818
1377
|
}
|
|
819
1378
|
}
|
|
1379
|
+
const hasSrcDir = existsSync4(join9(cwd, "src"));
|
|
1380
|
+
const defaultDir = hasSrcDir ? "src/blocks" : "blocks";
|
|
1381
|
+
let rawPhysicalInput = defaultDir;
|
|
1382
|
+
if (!yes) {
|
|
1383
|
+
const directoryPrompt = await text({
|
|
1384
|
+
message: "Where would you like to install blocks?",
|
|
1385
|
+
placeholder: defaultDir,
|
|
1386
|
+
initialValue: defaultDir,
|
|
1387
|
+
validate(value) {
|
|
1388
|
+
if (!value || value.trim().length === 0) return "Directory path cannot be empty.";
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
if (isCancel2(directoryPrompt)) {
|
|
1392
|
+
if (json) {
|
|
1393
|
+
outputInitResult(json, { success: false, message: "Setup cancelled." });
|
|
1394
|
+
} else {
|
|
1395
|
+
outro2(format.muted("Setup cancelled."));
|
|
1396
|
+
}
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
rawPhysicalInput = String(directoryPrompt).trim();
|
|
1400
|
+
}
|
|
820
1401
|
const s = spinner2();
|
|
821
1402
|
if (!json) s.start("Scanning project architecture...");
|
|
822
|
-
const context = await detectProject(cwd);
|
|
823
|
-
const hasSrcDir = existsSync4(join7(cwd, "src"));
|
|
824
|
-
const tsConfig = await resolveTsConfigPaths(cwd);
|
|
1403
|
+
const context = await detectProject(cwd, { blocksDir: rawPhysicalInput });
|
|
825
1404
|
if (!json) s.stop("Project scanned.");
|
|
826
1405
|
let framework = context.framework;
|
|
827
1406
|
if (!framework) {
|
|
@@ -850,27 +1429,9 @@ async function initCommand(options = {}) {
|
|
|
850
1429
|
} else if (!json) {
|
|
851
1430
|
log2.info(`Framework detected: ${theme.state.info(framework)}`);
|
|
852
1431
|
}
|
|
853
|
-
const
|
|
854
|
-
let rawPhysicalInput = defaultDir;
|
|
855
|
-
if (!yes) {
|
|
856
|
-
const directoryPrompt = await text({
|
|
857
|
-
message: "Where would you like to install blocks?",
|
|
858
|
-
placeholder: defaultDir,
|
|
859
|
-
initialValue: defaultDir,
|
|
860
|
-
validate(value) {
|
|
861
|
-
if (!value || value.trim().length === 0) return "Directory path cannot be empty.";
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
if (isCancel2(directoryPrompt)) {
|
|
865
|
-
outro2(format.muted("Setup cancelled."));
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
rawPhysicalInput = String(directoryPrompt).trim();
|
|
869
|
-
}
|
|
870
|
-
const relativeBlocksPath = path2.relative(cwd, path2.resolve(cwd, rawPhysicalInput)).replace(/\\/g, "/");
|
|
1432
|
+
const relativeBlocksPath = path7.relative(cwd, context.blocksDir).replace(/\\/g, "/");
|
|
871
1433
|
const finalPath = relativeBlocksPath.startsWith(".") ? relativeBlocksPath : `./${relativeBlocksPath}`;
|
|
872
|
-
const
|
|
873
|
-
const finalBlockAlias = hasTsPaths ? deriveBlockAlias(tsConfig.paths, relativeBlocksPath) : void 0;
|
|
1434
|
+
const finalBlockAlias = context.blocksAlias;
|
|
874
1435
|
if (!json) {
|
|
875
1436
|
log2.info(
|
|
876
1437
|
`Import alias: ${theme.state.info(finalBlockAlias === void 0 ? " relative imports" : finalBlockAlias)}`
|
|
@@ -909,7 +1470,7 @@ async function initCommand(options = {}) {
|
|
|
909
1470
|
};
|
|
910
1471
|
if (!json) s.start("Writing configuration...");
|
|
911
1472
|
try {
|
|
912
|
-
await
|
|
1473
|
+
await fs6.writeFile(configPath, JSON.stringify(configPayload, null, 2), "utf-8");
|
|
913
1474
|
if (!json) s.stop("Configuration saved.");
|
|
914
1475
|
outputInitResult(json, {
|
|
915
1476
|
success: true,
|
|
@@ -967,33 +1528,65 @@ async function detectCommand(options = {}) {
|
|
|
967
1528
|
}
|
|
968
1529
|
|
|
969
1530
|
// src/commands/list.ts
|
|
970
|
-
import
|
|
971
|
-
import fs3 from "fs/promises";
|
|
1531
|
+
import fs7 from "fs/promises";
|
|
972
1532
|
import pc5 from "picocolors";
|
|
973
1533
|
import { outro as outro4, spinner as spinner3 } from "@clack/prompts";
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
const checkPath = join8(dir, filename);
|
|
1534
|
+
async function listCommand(options = {}) {
|
|
1535
|
+
const { json = false, recipes: listRecipes = false } = options;
|
|
1536
|
+
const cwd = process.cwd();
|
|
1537
|
+
const source = createGitHubRegistrySource();
|
|
1538
|
+
const s = spinner3();
|
|
1539
|
+
if (listRecipes) {
|
|
1540
|
+
if (!json) s.start("Fetching published recipes...");
|
|
982
1541
|
try {
|
|
983
|
-
await
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
+
}
|
|
989
1586
|
}
|
|
1587
|
+
return;
|
|
990
1588
|
}
|
|
991
|
-
|
|
992
|
-
}
|
|
993
|
-
async function listCommand(options = {}) {
|
|
994
|
-
const { json = false } = options;
|
|
995
|
-
const cwd = process.cwd();
|
|
996
|
-
const configPath = await findUp2("blockend.json", cwd);
|
|
1589
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
997
1590
|
if (!configPath) {
|
|
998
1591
|
if (json) {
|
|
999
1592
|
process.stdout.write(
|
|
@@ -1006,7 +1599,7 @@ async function listCommand(options = {}) {
|
|
|
1006
1599
|
}
|
|
1007
1600
|
let config;
|
|
1008
1601
|
try {
|
|
1009
|
-
const configFile = await
|
|
1602
|
+
const configFile = await fs7.readFile(configPath, "utf-8");
|
|
1010
1603
|
config = JSON.parse(configFile);
|
|
1011
1604
|
} catch {
|
|
1012
1605
|
if (json) {
|
|
@@ -1019,15 +1612,12 @@ async function listCommand(options = {}) {
|
|
|
1019
1612
|
return;
|
|
1020
1613
|
}
|
|
1021
1614
|
const envKey = String(config.environment);
|
|
1022
|
-
const s = spinner3();
|
|
1023
1615
|
if (!json) {
|
|
1024
1616
|
s.start("Syncing registry metadata blocks...");
|
|
1025
1617
|
}
|
|
1026
1618
|
let registry;
|
|
1027
1619
|
try {
|
|
1028
|
-
|
|
1029
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1030
|
-
registry = await response.json();
|
|
1620
|
+
registry = await loadRegistry(source);
|
|
1031
1621
|
if (!json) s.stop(pc5.green("\u2714 Synced available block registries."));
|
|
1032
1622
|
} catch {
|
|
1033
1623
|
if (json) {
|
|
@@ -1039,25 +1629,12 @@ async function listCommand(options = {}) {
|
|
|
1039
1629
|
}
|
|
1040
1630
|
return;
|
|
1041
1631
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
const blocksForEnv = Object.entries(blockMap).filter(([_, block]) => {
|
|
1049
|
-
if (!block) return false;
|
|
1050
|
-
const hasAdapter = block.adapters?.[envKey] !== void 0;
|
|
1051
|
-
const hasEnvironment = block.environments?.[envKey] !== void 0;
|
|
1052
|
-
return hasAdapter || hasEnvironment;
|
|
1053
|
-
}).map(([key, block]) => {
|
|
1054
|
-
const context = block.adapters?.[envKey] ?? block.environments?.[envKey];
|
|
1055
|
-
return {
|
|
1056
|
-
name: key,
|
|
1057
|
-
description: block.description,
|
|
1058
|
-
variants: Object.keys(context?.variants || {})
|
|
1059
|
-
};
|
|
1060
|
-
});
|
|
1632
|
+
const available = await listAvailableBlocks(registry, envKey);
|
|
1633
|
+
const blocksForEnv = available.map((block) => ({
|
|
1634
|
+
name: block.name,
|
|
1635
|
+
description: block.description,
|
|
1636
|
+
variants: block.variants
|
|
1637
|
+
}));
|
|
1061
1638
|
if (json) {
|
|
1062
1639
|
process.stdout.write(
|
|
1063
1640
|
JSON.stringify({ success: true, environment: envKey, blocks: blocksForEnv }) + "\n"
|
|
@@ -1068,11 +1645,13 @@ Available backend blocks for ${pc5.magenta(envKey)}:`);
|
|
|
1068
1645
|
if (blocksForEnv.length === 0) {
|
|
1069
1646
|
console.log(pc5.dim(" No blocks found for this framework."));
|
|
1070
1647
|
} else {
|
|
1071
|
-
blocksForEnv.forEach((
|
|
1648
|
+
blocksForEnv.forEach((block) => {
|
|
1072
1649
|
console.log(`
|
|
1073
|
-
${pc5.cyan(
|
|
1074
|
-
console.log(` ${pc5.dim(
|
|
1075
|
-
console.log(
|
|
1650
|
+
${pc5.cyan(block.name)}`);
|
|
1651
|
+
console.log(` ${pc5.dim(block.description)}`);
|
|
1652
|
+
console.log(
|
|
1653
|
+
` Storage Variants: ${block.variants.map((variant) => pc5.yellow(variant)).join(", ")}`
|
|
1654
|
+
);
|
|
1076
1655
|
});
|
|
1077
1656
|
console.log();
|
|
1078
1657
|
}
|
|
@@ -1080,8 +1659,8 @@ Available backend blocks for ${pc5.magenta(envKey)}:`);
|
|
|
1080
1659
|
}
|
|
1081
1660
|
|
|
1082
1661
|
// src/commands/mcp.ts
|
|
1083
|
-
import
|
|
1084
|
-
import
|
|
1662
|
+
import path8, { join as join10 } from "path";
|
|
1663
|
+
import fs8 from "fs/promises";
|
|
1085
1664
|
import pc6 from "picocolors";
|
|
1086
1665
|
import { outro as outro5, spinner as spinner4, select as select3, confirm as confirm3 } from "@clack/prompts";
|
|
1087
1666
|
var CLIENT_MATRIX = {
|
|
@@ -1127,12 +1706,12 @@ args = [${args}]
|
|
|
1127
1706
|
async function mcpStartCommand() {
|
|
1128
1707
|
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
1129
1708
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1130
|
-
const { z } = await import("zod");
|
|
1709
|
+
const { z: z2 } = await import("zod");
|
|
1131
1710
|
const { execa } = await import("execa");
|
|
1132
1711
|
const server = new McpServer({
|
|
1133
1712
|
name: "blockend",
|
|
1134
1713
|
version: "0.1.0",
|
|
1135
|
-
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"
|
|
1136
1715
|
});
|
|
1137
1716
|
async function safeExec(command, args, options) {
|
|
1138
1717
|
try {
|
|
@@ -1146,19 +1725,34 @@ async function mcpStartCommand() {
|
|
|
1146
1725
|
}
|
|
1147
1726
|
server.tool(
|
|
1148
1727
|
"list_blocks",
|
|
1149
|
-
"Lists all available Blockend blocks with names, descriptions, and
|
|
1728
|
+
"Lists all available Blockend blocks with names, descriptions, and variants",
|
|
1150
1729
|
{},
|
|
1151
1730
|
async () => {
|
|
1152
1731
|
const result = await safeExec("npx", ["--no-install", "blockend", "list", "--json"]);
|
|
1153
1732
|
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
1154
1733
|
}
|
|
1155
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
|
+
);
|
|
1156
1750
|
server.tool(
|
|
1157
1751
|
"add_block",
|
|
1158
1752
|
"Installs a Blockend block into a project.",
|
|
1159
1753
|
{
|
|
1160
|
-
blockName:
|
|
1161
|
-
projectPath:
|
|
1754
|
+
blockName: z2.string().describe("Block name, e.g. rate-limiter, error-handler, logger"),
|
|
1755
|
+
projectPath: z2.string().describe("Absolute path to the project root where blockend should install")
|
|
1162
1756
|
},
|
|
1163
1757
|
async ({ blockName, projectPath }) => {
|
|
1164
1758
|
const result = await safeExec(
|
|
@@ -1173,7 +1767,7 @@ async function mcpStartCommand() {
|
|
|
1173
1767
|
"detect_project",
|
|
1174
1768
|
"Analyzes a project context: framework, language, package manager, etc.",
|
|
1175
1769
|
{
|
|
1176
|
-
projectPath:
|
|
1770
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1177
1771
|
},
|
|
1178
1772
|
async ({ projectPath }) => {
|
|
1179
1773
|
const result = await safeExec("npx", ["--no-install", "blockend", "detect", "--json"], {
|
|
@@ -1186,8 +1780,8 @@ async function mcpStartCommand() {
|
|
|
1186
1780
|
"diff_block",
|
|
1187
1781
|
"Preview what files a block would generate without writing to disk. Shows new/modified/unchanged files.",
|
|
1188
1782
|
{
|
|
1189
|
-
blockName:
|
|
1190
|
-
projectPath:
|
|
1783
|
+
blockName: z2.string().describe("Block name to preview, e.g. rate-limiter, logger"),
|
|
1784
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1191
1785
|
},
|
|
1192
1786
|
async ({ blockName, projectPath }) => {
|
|
1193
1787
|
const result = await safeExec(
|
|
@@ -1202,7 +1796,7 @@ async function mcpStartCommand() {
|
|
|
1202
1796
|
"doctor",
|
|
1203
1797
|
"Check project health: validates blockend.json, detects framework, checks for issues.",
|
|
1204
1798
|
{
|
|
1205
|
-
projectPath:
|
|
1799
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1206
1800
|
},
|
|
1207
1801
|
async ({ projectPath }) => {
|
|
1208
1802
|
const result = await safeExec("npx", ["--no-install", "blockend", "doctor", "--json"], {
|
|
@@ -1215,7 +1809,7 @@ async function mcpStartCommand() {
|
|
|
1215
1809
|
"update_check",
|
|
1216
1810
|
"Check for available updates to installed blocks. Shows version comparison and diffs.",
|
|
1217
1811
|
{
|
|
1218
|
-
projectPath:
|
|
1812
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1219
1813
|
},
|
|
1220
1814
|
async ({ projectPath }) => {
|
|
1221
1815
|
const result = await safeExec("npx", ["--no-install", "blockend", "update", "--json"], {
|
|
@@ -1228,12 +1822,12 @@ async function mcpStartCommand() {
|
|
|
1228
1822
|
"update_apply",
|
|
1229
1823
|
"Apply updates to all installed blocks. Downloads new versions, rewrites imports, and updates blockend.json.",
|
|
1230
1824
|
{
|
|
1231
|
-
projectPath:
|
|
1825
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1232
1826
|
},
|
|
1233
1827
|
async ({ projectPath }) => {
|
|
1234
1828
|
const result = await safeExec(
|
|
1235
1829
|
"npx",
|
|
1236
|
-
["--no-install", "blockend", "update", "--apply", "--
|
|
1830
|
+
["--no-install", "blockend", "update", "--apply", "--json"],
|
|
1237
1831
|
{ cwd: projectPath }
|
|
1238
1832
|
);
|
|
1239
1833
|
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
@@ -1241,14 +1835,38 @@ async function mcpStartCommand() {
|
|
|
1241
1835
|
);
|
|
1242
1836
|
server.tool(
|
|
1243
1837
|
"add_blocks",
|
|
1244
|
-
"Install multiple blocks
|
|
1838
|
+
"Install multiple blocks into a project.",
|
|
1245
1839
|
{
|
|
1246
|
-
|
|
1840
|
+
blockNames: z2.array(z2.string()).min(1).describe("Block names, e.g. rate-limiter, logger"),
|
|
1841
|
+
projectPath: z2.string().describe("Absolute path to the project root where blockend should install")
|
|
1247
1842
|
},
|
|
1248
|
-
async ({ projectPath }) => {
|
|
1843
|
+
async ({ blockNames, projectPath }) => {
|
|
1844
|
+
const results = [];
|
|
1845
|
+
for (const blockName of blockNames) {
|
|
1846
|
+
const result = await safeExec(
|
|
1847
|
+
"npx",
|
|
1848
|
+
["--no-install", "blockend", "add", blockName, "--yes", "--json"],
|
|
1849
|
+
{ cwd: projectPath }
|
|
1850
|
+
);
|
|
1851
|
+
results.push({ blockName, output: result.text, isError: result.isError });
|
|
1852
|
+
}
|
|
1853
|
+
return {
|
|
1854
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
1855
|
+
isError: results.some((result) => result.isError)
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
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 }) => {
|
|
1249
1867
|
const result = await safeExec(
|
|
1250
1868
|
"npx",
|
|
1251
|
-
["--no-install", "blockend", "add", "--
|
|
1869
|
+
["--no-install", "blockend", "add", "--recipe", recipe, "--yes", "--json"],
|
|
1252
1870
|
{ cwd: projectPath }
|
|
1253
1871
|
);
|
|
1254
1872
|
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
@@ -1280,7 +1898,7 @@ async function mcpInitCommand(options) {
|
|
|
1280
1898
|
outro5(pc6.red(`\u2716 Unsupported client identifier profile: ${options.client}`));
|
|
1281
1899
|
return;
|
|
1282
1900
|
}
|
|
1283
|
-
const absoluteConfigTarget =
|
|
1901
|
+
const absoluteConfigTarget = join10(cwd, meta.relativePath);
|
|
1284
1902
|
if (options.dryRun) {
|
|
1285
1903
|
console.log(
|
|
1286
1904
|
pc6.cyan(`
|
|
@@ -1297,7 +1915,7 @@ async function mcpInitCommand(options) {
|
|
|
1297
1915
|
}
|
|
1298
1916
|
let fileConflict = false;
|
|
1299
1917
|
try {
|
|
1300
|
-
await
|
|
1918
|
+
await fs8.access(absoluteConfigTarget);
|
|
1301
1919
|
fileConflict = true;
|
|
1302
1920
|
} catch {
|
|
1303
1921
|
}
|
|
@@ -1314,12 +1932,12 @@ async function mcpInitCommand(options) {
|
|
|
1314
1932
|
const s = spinner4();
|
|
1315
1933
|
s.start(`Writing localized project configurations inside ${pc6.dim(meta.relativePath)}...`);
|
|
1316
1934
|
try {
|
|
1317
|
-
await
|
|
1935
|
+
await fs8.mkdir(path8.dirname(absoluteConfigTarget), { recursive: true });
|
|
1318
1936
|
if (meta.format === "json") {
|
|
1319
1937
|
let workingConfig = {};
|
|
1320
1938
|
if (fileConflict) {
|
|
1321
1939
|
try {
|
|
1322
|
-
const rawContent = await
|
|
1940
|
+
const rawContent = await fs8.readFile(absoluteConfigTarget, "utf-8");
|
|
1323
1941
|
workingConfig = JSON.parse(rawContent);
|
|
1324
1942
|
} catch {
|
|
1325
1943
|
workingConfig = {};
|
|
@@ -1332,9 +1950,9 @@ async function mcpInitCommand(options) {
|
|
|
1332
1950
|
const existingServers = workingConfig.mcpServers || {};
|
|
1333
1951
|
workingConfig.mcpServers = { ...existingServers, blockend: BLOCKEND_SERVER };
|
|
1334
1952
|
}
|
|
1335
|
-
await
|
|
1953
|
+
await fs8.writeFile(absoluteConfigTarget, JSON.stringify(workingConfig, null, 2), "utf-8");
|
|
1336
1954
|
} else {
|
|
1337
|
-
await
|
|
1955
|
+
await fs8.writeFile(absoluteConfigTarget, generateToml(BLOCKEND_SERVER), "utf-8");
|
|
1338
1956
|
}
|
|
1339
1957
|
s.stop(
|
|
1340
1958
|
pc6.green(`\u2714 Local workspace integration fully complete! File target: ${meta.relativePath}`)
|
|
@@ -1346,47 +1964,10 @@ async function mcpInitCommand(options) {
|
|
|
1346
1964
|
}
|
|
1347
1965
|
|
|
1348
1966
|
// src/commands/diff.ts
|
|
1349
|
-
import
|
|
1350
|
-
import
|
|
1967
|
+
import path9 from "path";
|
|
1968
|
+
import fs9 from "fs/promises";
|
|
1351
1969
|
import { intro as intro3, outro as outro6, spinner as spinner5, log as log3 } from "@clack/prompts";
|
|
1352
1970
|
import pc7 from "picocolors";
|
|
1353
|
-
var REPO_OWNER3 = "codewithnuh";
|
|
1354
|
-
var REPO_NAME3 = "blockend";
|
|
1355
|
-
var BRANCH3 = "master";
|
|
1356
|
-
var RAW_CDN_BASE2 = `https://raw.githubusercontent.com/${REPO_OWNER3}/${REPO_NAME3}/${BRANCH3}`;
|
|
1357
|
-
var MANIFEST_URL3 = `${RAW_CDN_BASE2}/registry/index.json`;
|
|
1358
|
-
function blockSupportsEnv2(block, envKey) {
|
|
1359
|
-
if (block.frameworks && block.frameworks.length > 0) {
|
|
1360
|
-
return block.frameworks.includes("*") || block.frameworks.includes(envKey);
|
|
1361
|
-
}
|
|
1362
|
-
return block.adapters?.[envKey] !== void 0 || block.environments?.[envKey] !== void 0;
|
|
1363
|
-
}
|
|
1364
|
-
function resolveAdapterContext2(block, envKey) {
|
|
1365
|
-
return block.adapters?.[envKey] ?? block.adapters?.["*"] ?? block.environments?.[envKey] ?? block.environments?.["*"] ?? null;
|
|
1366
|
-
}
|
|
1367
|
-
async function findUp3(filename, startDir) {
|
|
1368
|
-
let dir = startDir;
|
|
1369
|
-
while (true) {
|
|
1370
|
-
const candidate = join10(dir, filename);
|
|
1371
|
-
try {
|
|
1372
|
-
await fs5.access(candidate);
|
|
1373
|
-
return candidate;
|
|
1374
|
-
} catch {
|
|
1375
|
-
const parent = dirname3(dir);
|
|
1376
|
-
if (parent === dir) break;
|
|
1377
|
-
dir = parent;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
return null;
|
|
1381
|
-
}
|
|
1382
|
-
async function fileExists(filePath) {
|
|
1383
|
-
try {
|
|
1384
|
-
await fs5.access(filePath);
|
|
1385
|
-
return true;
|
|
1386
|
-
} catch {
|
|
1387
|
-
return false;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
1971
|
async function diffCommand(blockName, options = {}) {
|
|
1391
1972
|
const { json = false } = options;
|
|
1392
1973
|
if (!json) {
|
|
@@ -1397,7 +1978,7 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1397
1978
|
return { block: "", files: [], error: "Block name is required. Usage: blockend diff <block>" };
|
|
1398
1979
|
}
|
|
1399
1980
|
const cwd = process.cwd();
|
|
1400
|
-
const configPath = await
|
|
1981
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
1401
1982
|
if (!configPath) {
|
|
1402
1983
|
const error = "blockend.json not found. Run 'npx blockend init' first.";
|
|
1403
1984
|
if (json) {
|
|
@@ -1407,10 +1988,10 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1407
1988
|
}
|
|
1408
1989
|
return { block: blockName, files: [], error };
|
|
1409
1990
|
}
|
|
1410
|
-
const rootDir =
|
|
1991
|
+
const rootDir = path9.dirname(configPath);
|
|
1411
1992
|
let config;
|
|
1412
1993
|
try {
|
|
1413
|
-
config = JSON.parse(await
|
|
1994
|
+
config = JSON.parse(await fs9.readFile(configPath, "utf-8"));
|
|
1414
1995
|
} catch {
|
|
1415
1996
|
const error = "Failed to parse blockend.json.";
|
|
1416
1997
|
if (json) {
|
|
@@ -1421,15 +2002,14 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1421
2002
|
return { block: blockName, files: [], error };
|
|
1422
2003
|
}
|
|
1423
2004
|
const physicalBlocksPath = config.paths.blocks;
|
|
1424
|
-
const blocksRootAbsolute =
|
|
2005
|
+
const blocksRootAbsolute = path9.resolve(rootDir, physicalBlocksPath);
|
|
1425
2006
|
const envKey = config.environment;
|
|
2007
|
+
const source = createGitHubRegistrySource();
|
|
1426
2008
|
const s = spinner5();
|
|
1427
2009
|
if (!json) s.start("Fetching registry...");
|
|
1428
2010
|
let registry;
|
|
1429
2011
|
try {
|
|
1430
|
-
|
|
1431
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1432
|
-
registry = await res.json();
|
|
2012
|
+
registry = await loadRegistry(source);
|
|
1433
2013
|
if (!json) s.stop("Registry synced.");
|
|
1434
2014
|
} catch (err) {
|
|
1435
2015
|
const error = `Failed to fetch registry: ${String(err)}`;
|
|
@@ -1439,103 +2019,32 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1439
2019
|
}
|
|
1440
2020
|
return { block: blockName, files: [], error };
|
|
1441
2021
|
}
|
|
1442
|
-
const
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
outro6(pc7.dim("Block not found."));
|
|
1451
|
-
}
|
|
1452
|
-
return { block: blockName, files: [], error };
|
|
1453
|
-
}
|
|
1454
|
-
if (!blockSupportsEnv2(blockMeta, envKey)) {
|
|
1455
|
-
const error = `Block "${blockName}" does not support environment: ${envKey}`;
|
|
1456
|
-
if (json) {
|
|
1457
|
-
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1458
|
-
} else {
|
|
1459
|
-
log3.error(error);
|
|
1460
|
-
}
|
|
1461
|
-
return { block: blockName, files: [], error };
|
|
1462
|
-
}
|
|
1463
|
-
const adapterContext = resolveAdapterContext2(blockMeta, envKey);
|
|
1464
|
-
if (!adapterContext) {
|
|
1465
|
-
const error = `Block "${blockName}" has no adapter for: ${envKey}`;
|
|
1466
|
-
if (json) {
|
|
1467
|
-
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1468
|
-
} else {
|
|
1469
|
-
log3.error(error);
|
|
1470
|
-
}
|
|
1471
|
-
return { block: blockName, files: [], error };
|
|
1472
|
-
}
|
|
1473
|
-
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
1474
|
-
if (variantKeys.length === 0) {
|
|
1475
|
-
const error = `No variants found for "${blockName}" / "${envKey}".`;
|
|
2022
|
+
const targetFolder = path9.resolve(blocksRootAbsolute, blockName);
|
|
2023
|
+
const installedRecord = config.installed?.find((block) => block.name === blockName);
|
|
2024
|
+
const planResult = await planBlock(registry, blockName, envKey, {
|
|
2025
|
+
recordedVariant: installedRecord?.variant,
|
|
2026
|
+
targetFolder
|
|
2027
|
+
});
|
|
2028
|
+
if (!planResult.ok) {
|
|
2029
|
+
const error = planResult.error.code === "no-adapter" ? `Block "${blockName}" has no adapter for: ${envKey}` : planResult.error.message;
|
|
1476
2030
|
if (json) {
|
|
1477
2031
|
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1478
2032
|
} else {
|
|
1479
2033
|
log3.error(error);
|
|
2034
|
+
if (planResult.error.code === "not-found") outro6(pc7.dim("Block not found."));
|
|
1480
2035
|
}
|
|
1481
2036
|
return { block: blockName, files: [], error };
|
|
1482
2037
|
}
|
|
1483
|
-
const
|
|
1484
|
-
const installedRecord = config.installed?.find((b) => b.name === blockName);
|
|
1485
|
-
const preferred = await resolvePreferredVariant(
|
|
1486
|
-
variantKeys,
|
|
1487
|
-
installedRecord?.variant,
|
|
1488
|
-
targetFolder
|
|
1489
|
-
);
|
|
1490
|
-
const selectedVariant = selectVariant(variantKeys, preferred);
|
|
1491
|
-
const variantMeta = adapterContext.variants[selectedVariant];
|
|
1492
|
-
const filesToDownload = [];
|
|
1493
|
-
if (blockMeta.baseFiles?.length) {
|
|
1494
|
-
filesToDownload.push(...blockMeta.baseFiles);
|
|
1495
|
-
}
|
|
1496
|
-
if (adapterContext.core) {
|
|
1497
|
-
filesToDownload.push({
|
|
1498
|
-
source: adapterContext.core,
|
|
1499
|
-
target: path4.basename(adapterContext.core)
|
|
1500
|
-
});
|
|
1501
|
-
}
|
|
1502
|
-
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
1503
|
-
for (const entry of variantMeta.files) {
|
|
1504
|
-
filesToDownload.push(
|
|
1505
|
-
typeof entry === "string" ? { source: entry, target: path4.basename(entry) } : entry
|
|
1506
|
-
);
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
2038
|
+
const plan = planResult.plan;
|
|
1509
2039
|
if (!json) s.start(`Comparing ${blockName} files...`);
|
|
1510
2040
|
const diffFiles = [];
|
|
1511
2041
|
try {
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
if (!res.ok) throw new Error(`Download failed: ${fm.source} (HTTP ${res.status})`);
|
|
1515
|
-
const newContent = await res.text();
|
|
1516
|
-
const targetPath = path4.join(targetFolder, fm.target);
|
|
1517
|
-
let existingContent = null;
|
|
1518
|
-
let status = "new";
|
|
1519
|
-
if (await fileExists(targetPath)) {
|
|
1520
|
-
try {
|
|
1521
|
-
existingContent = await fs5.readFile(targetPath, "utf-8");
|
|
1522
|
-
status = existingContent === newContent ? "unchanged" : "modified";
|
|
1523
|
-
} catch {
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
diffFiles.push({
|
|
1527
|
-
name: fm.target,
|
|
1528
|
-
sourcePath: fm.source,
|
|
1529
|
-
targetPath,
|
|
1530
|
-
existingContent,
|
|
1531
|
-
newContent,
|
|
1532
|
-
status
|
|
1533
|
-
});
|
|
1534
|
-
}
|
|
2042
|
+
const previews = await previewBlock(plan, { source, targetFolder });
|
|
2043
|
+
diffFiles.push(...previews);
|
|
1535
2044
|
if (!json) s.stop("Comparison complete.");
|
|
1536
2045
|
if (!json) {
|
|
1537
2046
|
console.log(`
|
|
1538
|
-
${pc7.bold(pc7.cyan(blockName))} \u2014 ${pc7.dim(
|
|
2047
|
+
${pc7.bold(pc7.cyan(blockName))} \u2014 ${pc7.dim(plan.variant)} variant
|
|
1539
2048
|
`);
|
|
1540
2049
|
for (const file of diffFiles) {
|
|
1541
2050
|
const icon = file.status === "new" ? pc7.green("+") : file.status === "modified" ? pc7.yellow("~") : pc7.dim("=");
|
|
@@ -1563,11 +2072,13 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1563
2072
|
}
|
|
1564
2073
|
}
|
|
1565
2074
|
console.log("");
|
|
1566
|
-
const newCount = diffFiles.filter((
|
|
1567
|
-
const modCount = diffFiles.filter((
|
|
1568
|
-
const unchangedCount = diffFiles.filter((
|
|
2075
|
+
const newCount = diffFiles.filter((file) => file.status === "new").length;
|
|
2076
|
+
const modCount = diffFiles.filter((file) => file.status === "modified").length;
|
|
2077
|
+
const unchangedCount = diffFiles.filter((file) => file.status === "unchanged").length;
|
|
1569
2078
|
outro6(
|
|
1570
|
-
`${pc7.green(`${newCount} new`)} | ${pc7.yellow(`${modCount} modified`)} | ${pc7.dim(
|
|
2079
|
+
`${pc7.green(`${newCount} new`)} | ${pc7.yellow(`${modCount} modified`)} | ${pc7.dim(
|
|
2080
|
+
`${unchangedCount} unchanged`
|
|
2081
|
+
)}`
|
|
1571
2082
|
);
|
|
1572
2083
|
}
|
|
1573
2084
|
const result = { block: blockName, files: diffFiles };
|
|
@@ -1590,8 +2101,8 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1590
2101
|
}
|
|
1591
2102
|
|
|
1592
2103
|
// src/commands/doctor.ts
|
|
1593
|
-
import
|
|
1594
|
-
import
|
|
2104
|
+
import path10, { join as join11, dirname as dirname5 } from "path";
|
|
2105
|
+
import fs10 from "fs/promises";
|
|
1595
2106
|
import { existsSync as existsSync5 } from "fs";
|
|
1596
2107
|
import { intro as intro4, outro as outro7, spinner as spinner6 } from "@clack/prompts";
|
|
1597
2108
|
import pc8 from "picocolors";
|
|
@@ -1622,15 +2133,15 @@ function outputDoctorResult(json, result) {
|
|
|
1622
2133
|
}
|
|
1623
2134
|
}
|
|
1624
2135
|
}
|
|
1625
|
-
async function
|
|
2136
|
+
async function findUp2(filename, startDir) {
|
|
1626
2137
|
let dir = startDir;
|
|
1627
2138
|
while (true) {
|
|
1628
2139
|
const candidate = join11(dir, filename);
|
|
1629
2140
|
try {
|
|
1630
|
-
await
|
|
2141
|
+
await fs10.access(candidate);
|
|
1631
2142
|
return candidate;
|
|
1632
2143
|
} catch {
|
|
1633
|
-
const parent =
|
|
2144
|
+
const parent = dirname5(dir);
|
|
1634
2145
|
if (parent === dir) break;
|
|
1635
2146
|
dir = parent;
|
|
1636
2147
|
}
|
|
@@ -1654,7 +2165,7 @@ async function doctorCommand(options = {}) {
|
|
|
1654
2165
|
}
|
|
1655
2166
|
const s = spinner6();
|
|
1656
2167
|
if (!json) s.start("Scanning project health...");
|
|
1657
|
-
const packageJsonPath = await
|
|
2168
|
+
const packageJsonPath = await findUp2("package.json", cwd);
|
|
1658
2169
|
if (packageJsonPath) {
|
|
1659
2170
|
addCheck({ name: "package.json", status: "ok", message: "Found" });
|
|
1660
2171
|
} else {
|
|
@@ -1664,7 +2175,7 @@ async function doctorCommand(options = {}) {
|
|
|
1664
2175
|
message: "Not found. Run 'npm init' or 'pnpm init' first."
|
|
1665
2176
|
});
|
|
1666
2177
|
}
|
|
1667
|
-
const configPath = await
|
|
2178
|
+
const configPath = await findUp2("blockend.json", cwd);
|
|
1668
2179
|
if (configPath) {
|
|
1669
2180
|
addCheck({ name: "blockend.json", status: "ok", message: "Found" });
|
|
1670
2181
|
} else {
|
|
@@ -1674,7 +2185,7 @@ async function doctorCommand(options = {}) {
|
|
|
1674
2185
|
message: "Not found. Run 'npx blockend init' first."
|
|
1675
2186
|
});
|
|
1676
2187
|
}
|
|
1677
|
-
const tsConfigPath = await
|
|
2188
|
+
const tsConfigPath = await findUp2("tsconfig.json", cwd);
|
|
1678
2189
|
if (tsConfigPath) {
|
|
1679
2190
|
addCheck({ name: "tsconfig.json", status: "ok", message: "Found" });
|
|
1680
2191
|
} else {
|
|
@@ -1736,7 +2247,7 @@ async function doctorCommand(options = {}) {
|
|
|
1736
2247
|
}
|
|
1737
2248
|
if (configPath) {
|
|
1738
2249
|
try {
|
|
1739
|
-
const raw = await
|
|
2250
|
+
const raw = await fs10.readFile(configPath, "utf-8");
|
|
1740
2251
|
const config = JSON.parse(raw);
|
|
1741
2252
|
if (!config.environment) {
|
|
1742
2253
|
addCheck({
|
|
@@ -1758,7 +2269,7 @@ async function doctorCommand(options = {}) {
|
|
|
1758
2269
|
message: "Missing 'paths.blocks' in blockend.json"
|
|
1759
2270
|
});
|
|
1760
2271
|
} else {
|
|
1761
|
-
const blocksDir =
|
|
2272
|
+
const blocksDir = path10.resolve(dirname5(configPath), config.paths.blocks);
|
|
1762
2273
|
if (existsSync5(blocksDir)) {
|
|
1763
2274
|
addCheck({
|
|
1764
2275
|
name: "Config: blocks directory",
|
|
@@ -1791,39 +2302,10 @@ async function doctorCommand(options = {}) {
|
|
|
1791
2302
|
}
|
|
1792
2303
|
|
|
1793
2304
|
// src/commands/update.ts
|
|
1794
|
-
import
|
|
1795
|
-
import
|
|
2305
|
+
import path11 from "path";
|
|
2306
|
+
import fs11 from "fs/promises";
|
|
1796
2307
|
import { intro as intro5, outro as outro8, spinner as spinner7, log as log4, multiselect as multiselect2, confirm as confirm4, isCancel as isCancel3 } from "@clack/prompts";
|
|
1797
2308
|
import pc9 from "picocolors";
|
|
1798
|
-
var REPO_OWNER4 = "codewithnuh";
|
|
1799
|
-
var REPO_NAME4 = "blockend";
|
|
1800
|
-
var BRANCH4 = "master";
|
|
1801
|
-
var RAW_CDN_BASE3 = `https://raw.githubusercontent.com/${REPO_OWNER4}/${REPO_NAME4}/${BRANCH4}`;
|
|
1802
|
-
var MANIFEST_URL4 = `${RAW_CDN_BASE3}/registry/index.json`;
|
|
1803
|
-
function blockSupportsEnv3(block, envKey) {
|
|
1804
|
-
if (block.frameworks && block.frameworks.length > 0) {
|
|
1805
|
-
return block.frameworks.includes("*") || block.frameworks.includes(envKey);
|
|
1806
|
-
}
|
|
1807
|
-
return block.adapters?.[envKey] !== void 0 || block.environments?.[envKey] !== void 0;
|
|
1808
|
-
}
|
|
1809
|
-
function resolveAdapterContext3(block, envKey) {
|
|
1810
|
-
return block.adapters?.[envKey] ?? block.adapters?.["*"] ?? block.environments?.[envKey] ?? block.environments?.["*"] ?? null;
|
|
1811
|
-
}
|
|
1812
|
-
async function findUp5(filename, startDir) {
|
|
1813
|
-
let dir = startDir;
|
|
1814
|
-
while (true) {
|
|
1815
|
-
const candidate = join12(dir, filename);
|
|
1816
|
-
try {
|
|
1817
|
-
await fs7.access(candidate);
|
|
1818
|
-
return candidate;
|
|
1819
|
-
} catch {
|
|
1820
|
-
const parent = dirname5(dir);
|
|
1821
|
-
if (parent === dir) break;
|
|
1822
|
-
dir = parent;
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
return null;
|
|
1826
|
-
}
|
|
1827
2309
|
function computeLineDiff(oldContent, newContent) {
|
|
1828
2310
|
const oldLines = oldContent.split("\n");
|
|
1829
2311
|
const newLines = newContent.split("\n");
|
|
@@ -1915,7 +2397,9 @@ function outputUpdateResult(json, result) {
|
|
|
1915
2397
|
`);
|
|
1916
2398
|
for (const block of updates) {
|
|
1917
2399
|
console.log(
|
|
1918
|
-
` ${pc9.yellow("~")} ${pc9.bold(block.name)} ${pc9.dim(
|
|
2400
|
+
` ${pc9.yellow("~")} ${pc9.bold(block.name)} ${pc9.dim(
|
|
2401
|
+
`${block.localVersion} \u2192 ${block.remoteVersion}`
|
|
2402
|
+
)}`
|
|
1919
2403
|
);
|
|
1920
2404
|
}
|
|
1921
2405
|
console.log("");
|
|
@@ -1944,31 +2428,6 @@ function outputUpdateResult(json, result) {
|
|
|
1944
2428
|
outro8(pc9.green("All tracked blocks are up to date."));
|
|
1945
2429
|
}
|
|
1946
2430
|
}
|
|
1947
|
-
async function resolveBlockFiles(blockMeta, envKey, preferredVariant, targetFolder) {
|
|
1948
|
-
const adapterContext = resolveAdapterContext3(blockMeta, envKey);
|
|
1949
|
-
if (!adapterContext) return null;
|
|
1950
|
-
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
1951
|
-
if (variantKeys.length === 0) return null;
|
|
1952
|
-
let preferred = preferredVariant;
|
|
1953
|
-
if (targetFolder && (!preferred || !variantKeys.includes(preferred))) {
|
|
1954
|
-
preferred = await resolvePreferredVariant(variantKeys, preferred, targetFolder) ?? preferred;
|
|
1955
|
-
}
|
|
1956
|
-
const selectedVariant = selectVariant(variantKeys, preferred);
|
|
1957
|
-
const variantMeta = adapterContext.variants[selectedVariant];
|
|
1958
|
-
const expectedFiles = [];
|
|
1959
|
-
if (blockMeta.baseFiles?.length) expectedFiles.push(...blockMeta.baseFiles);
|
|
1960
|
-
if (adapterContext.core) {
|
|
1961
|
-
expectedFiles.push({ source: adapterContext.core, target: path6.basename(adapterContext.core) });
|
|
1962
|
-
}
|
|
1963
|
-
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
1964
|
-
for (const entry of variantMeta.files) {
|
|
1965
|
-
expectedFiles.push(
|
|
1966
|
-
typeof entry === "string" ? { source: entry, target: path6.basename(entry) } : entry
|
|
1967
|
-
);
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
return { files: expectedFiles, variant: selectedVariant };
|
|
1971
|
-
}
|
|
1972
2431
|
async function updateCommand(options = {}) {
|
|
1973
2432
|
const { json = false, diff: diff2 = true, apply = false } = options;
|
|
1974
2433
|
if (!json) {
|
|
@@ -1976,17 +2435,17 @@ async function updateCommand(options = {}) {
|
|
|
1976
2435
|
intro5(`${pc9.bgCyan(pc9.black(" blockend "))} ${pc9.dim("update")}`);
|
|
1977
2436
|
}
|
|
1978
2437
|
const cwd = process.cwd();
|
|
1979
|
-
const configPath = await
|
|
2438
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
1980
2439
|
if (!configPath) {
|
|
1981
2440
|
const error = "blockend.json not found. Run 'npx blockend init' first.";
|
|
1982
2441
|
if (json) process.stdout.write(JSON.stringify({ blocks: [], error }) + "\n");
|
|
1983
2442
|
else log4.error(error);
|
|
1984
2443
|
return { blocks: [], error };
|
|
1985
2444
|
}
|
|
1986
|
-
const rootDir =
|
|
2445
|
+
const rootDir = path11.dirname(configPath);
|
|
1987
2446
|
let config;
|
|
1988
2447
|
try {
|
|
1989
|
-
config = JSON.parse(await
|
|
2448
|
+
config = JSON.parse(await fs11.readFile(configPath, "utf-8"));
|
|
1990
2449
|
} catch {
|
|
1991
2450
|
const error = "Failed to parse blockend.json.";
|
|
1992
2451
|
if (json) process.stdout.write(JSON.stringify({ blocks: [], error }) + "\n");
|
|
@@ -1994,18 +2453,17 @@ async function updateCommand(options = {}) {
|
|
|
1994
2453
|
return { blocks: [], error };
|
|
1995
2454
|
}
|
|
1996
2455
|
const physicalBlocksPath = config.paths.blocks;
|
|
1997
|
-
const blocksRootAbsolute =
|
|
2456
|
+
const blocksRootAbsolute = path11.resolve(rootDir, physicalBlocksPath);
|
|
1998
2457
|
const envKey = config.environment;
|
|
1999
2458
|
const aliasBase = config.aliases.blocks;
|
|
2000
2459
|
const rewriteStrategy = config.importRewriteStrategy ?? "remove";
|
|
2001
2460
|
const installedRecords = Array.isArray(config.installed) ? config.installed : [];
|
|
2461
|
+
const source = createGitHubRegistrySource();
|
|
2002
2462
|
const s = spinner7();
|
|
2003
2463
|
if (!json) s.start("Fetching registry...");
|
|
2004
2464
|
let registry;
|
|
2005
2465
|
try {
|
|
2006
|
-
|
|
2007
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2008
|
-
registry = await res.json();
|
|
2466
|
+
registry = await loadRegistry(source);
|
|
2009
2467
|
if (!json) s.stop("Registry synced.");
|
|
2010
2468
|
} catch (err) {
|
|
2011
2469
|
const error = `Failed to fetch registry: ${String(err)}`;
|
|
@@ -2015,14 +2473,17 @@ async function updateCommand(options = {}) {
|
|
|
2015
2473
|
}
|
|
2016
2474
|
return { blocks: [], error };
|
|
2017
2475
|
}
|
|
2018
|
-
const blockMap = registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks) ? registry.blocks : registry;
|
|
2019
2476
|
if (!json) s.start("Comparing versions...");
|
|
2020
2477
|
const blockResults = [];
|
|
2021
2478
|
const diffs = {};
|
|
2479
|
+
const plans = {};
|
|
2022
2480
|
for (const record of installedRecords) {
|
|
2023
|
-
const
|
|
2024
|
-
const
|
|
2025
|
-
|
|
2481
|
+
const targetFolder = path11.resolve(blocksRootAbsolute, record.name);
|
|
2482
|
+
const planResult = await planBlock(registry, record.name, envKey, {
|
|
2483
|
+
recordedVariant: record.variant,
|
|
2484
|
+
targetFolder
|
|
2485
|
+
});
|
|
2486
|
+
if (!planResult.ok) {
|
|
2026
2487
|
blockResults.push({
|
|
2027
2488
|
name: record.name,
|
|
2028
2489
|
localVersion: record.version,
|
|
@@ -2031,6 +2492,9 @@ async function updateCommand(options = {}) {
|
|
|
2031
2492
|
});
|
|
2032
2493
|
continue;
|
|
2033
2494
|
}
|
|
2495
|
+
const plan = planResult.plan;
|
|
2496
|
+
plans[record.name] = plan;
|
|
2497
|
+
const remoteVersion = plan.block.version ?? "0.0.0";
|
|
2034
2498
|
const hasUpdate = record.version !== remoteVersion || record.contentHash === "0";
|
|
2035
2499
|
blockResults.push({
|
|
2036
2500
|
name: record.name,
|
|
@@ -2039,55 +2503,39 @@ async function updateCommand(options = {}) {
|
|
|
2039
2503
|
status: hasUpdate ? "update-available" : "up-to-date"
|
|
2040
2504
|
});
|
|
2041
2505
|
if (hasUpdate && diff2) {
|
|
2042
|
-
const
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
localExists = true;
|
|
2053
|
-
} catch {
|
|
2054
|
-
localContent = "";
|
|
2055
|
-
}
|
|
2056
|
-
let remoteContent = "";
|
|
2057
|
-
try {
|
|
2058
|
-
const res = await fetch(`${RAW_CDN_BASE3}/${fm.source}`);
|
|
2059
|
-
if (res.ok) remoteContent = await res.text();
|
|
2060
|
-
} catch {
|
|
2061
|
-
remoteContent = "";
|
|
2062
|
-
}
|
|
2063
|
-
if (!localExists) {
|
|
2064
|
-
fileDiffs.push({
|
|
2065
|
-
name: fm.target,
|
|
2066
|
-
file: fm.target,
|
|
2506
|
+
const previews = await previewBlock(plan, {
|
|
2507
|
+
source,
|
|
2508
|
+
targetFolder,
|
|
2509
|
+
onDownloadError: "empty"
|
|
2510
|
+
});
|
|
2511
|
+
diffs[record.name] = previews.map((preview) => {
|
|
2512
|
+
if (preview.existingContent === null) {
|
|
2513
|
+
return {
|
|
2514
|
+
name: preview.name,
|
|
2515
|
+
file: preview.name,
|
|
2067
2516
|
status: "added",
|
|
2068
|
-
lines:
|
|
2517
|
+
lines: preview.newContent.split("\n").map((line, index) => ({
|
|
2069
2518
|
type: "added",
|
|
2070
|
-
content:
|
|
2071
|
-
newLineNo:
|
|
2519
|
+
content: line,
|
|
2520
|
+
newLineNo: index + 1
|
|
2072
2521
|
}))
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
if (preview.existingContent !== preview.newContent) {
|
|
2525
|
+
return {
|
|
2526
|
+
name: preview.name,
|
|
2527
|
+
file: preview.name,
|
|
2078
2528
|
status: "modified",
|
|
2079
|
-
lines: computeLineDiff(
|
|
2080
|
-
}
|
|
2081
|
-
} else {
|
|
2082
|
-
fileDiffs.push({
|
|
2083
|
-
name: fm.target,
|
|
2084
|
-
file: fm.target,
|
|
2085
|
-
status: "unchanged",
|
|
2086
|
-
lines: []
|
|
2087
|
-
});
|
|
2529
|
+
lines: computeLineDiff(preview.existingContent, preview.newContent)
|
|
2530
|
+
};
|
|
2088
2531
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2532
|
+
return {
|
|
2533
|
+
name: preview.name,
|
|
2534
|
+
file: preview.name,
|
|
2535
|
+
status: "unchanged",
|
|
2536
|
+
lines: []
|
|
2537
|
+
};
|
|
2538
|
+
});
|
|
2091
2539
|
}
|
|
2092
2540
|
}
|
|
2093
2541
|
if (!json) s.stop("Comparison complete.");
|
|
@@ -2124,60 +2572,19 @@ async function updateCommand(options = {}) {
|
|
|
2124
2572
|
}
|
|
2125
2573
|
const applied = [];
|
|
2126
2574
|
for (const blockName of selectedNames) {
|
|
2127
|
-
const
|
|
2128
|
-
if (!
|
|
2129
|
-
const record = installedRecords.find((r) => r.name === blockName);
|
|
2130
|
-
const targetFolder = path6.resolve(blocksRootAbsolute, blockName);
|
|
2131
|
-
const resolved = await resolveBlockFiles(blockMeta, envKey, record?.variant, targetFolder);
|
|
2132
|
-
if (!resolved) continue;
|
|
2133
|
-
const downloadedTargets = /* @__PURE__ */ new Set();
|
|
2575
|
+
const plan = plans[blockName];
|
|
2576
|
+
if (!plan) continue;
|
|
2134
2577
|
try {
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
}
|
|
2142
|
-
let contentHash = 0;
|
|
2143
|
-
for (const { fm, raw } of fileBatches) {
|
|
2144
|
-
const writtenFilePath = path6.join(targetFolder, fm.target);
|
|
2145
|
-
const content = rewriteFileImports(
|
|
2146
|
-
raw,
|
|
2147
|
-
writtenFilePath,
|
|
2148
|
-
blocksRootAbsolute,
|
|
2149
|
-
aliasBase,
|
|
2150
|
-
rewriteStrategy,
|
|
2151
|
-
downloadedTargets
|
|
2152
|
-
);
|
|
2153
|
-
await fs7.mkdir(dirname5(writtenFilePath), { recursive: true });
|
|
2154
|
-
await fs7.writeFile(writtenFilePath, content, "utf-8");
|
|
2155
|
-
for (let i = 0; i < content.length; i++) {
|
|
2156
|
-
contentHash = (contentHash << 5) - contentHash + content.charCodeAt(i) | 0;
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
const blockVersion = blockMeta.version ?? "0.0.0";
|
|
2160
|
-
const installedRecord = {
|
|
2161
|
-
name: blockName,
|
|
2162
|
-
version: blockVersion,
|
|
2163
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2164
|
-
files: resolved.files.map((f) => f.target),
|
|
2165
|
-
contentHash: contentHash.toString(16),
|
|
2166
|
-
variant: resolved.variant
|
|
2167
|
-
};
|
|
2168
|
-
try {
|
|
2169
|
-
const cfgRaw = await fs7.readFile(configPath, "utf-8");
|
|
2170
|
-
const cfgData = JSON.parse(cfgRaw);
|
|
2171
|
-
if (!Array.isArray(cfgData.installed)) cfgData.installed = [];
|
|
2172
|
-
cfgData.installed = cfgData.installed.filter(
|
|
2173
|
-
(b) => b.name !== blockName
|
|
2174
|
-
);
|
|
2175
|
-
cfgData.installed.push(installedRecord);
|
|
2176
|
-
await fs7.writeFile(configPath, JSON.stringify(cfgData, null, 2), "utf-8");
|
|
2177
|
-
} catch {
|
|
2178
|
-
}
|
|
2578
|
+
await applyBlock(plan, {
|
|
2579
|
+
source,
|
|
2580
|
+
blocksRoot: blocksRootAbsolute,
|
|
2581
|
+
aliasBase,
|
|
2582
|
+
rewriteStrategy,
|
|
2583
|
+
configPath
|
|
2584
|
+
});
|
|
2179
2585
|
applied.push(blockName);
|
|
2180
|
-
if (!json)
|
|
2586
|
+
if (!json)
|
|
2587
|
+
log4.info(` ${pc9.green("\u2714")} ${blockName} updated to v${plan.block.version ?? "0.0.0"}`);
|
|
2181
2588
|
} catch (err) {
|
|
2182
2589
|
if (!json) log4.error(` ${pc9.red("\u2716")} ${blockName}: ${String(err)}`);
|
|
2183
2590
|
}
|
|
@@ -2269,10 +2676,15 @@ var list = defineCommand({
|
|
|
2269
2676
|
type: "boolean",
|
|
2270
2677
|
default: false,
|
|
2271
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"
|
|
2272
2684
|
}
|
|
2273
2685
|
},
|
|
2274
2686
|
async run({ args }) {
|
|
2275
|
-
await listCommand({ json: args.json });
|
|
2687
|
+
await listCommand({ json: args.json, recipes: args.recipes });
|
|
2276
2688
|
}
|
|
2277
2689
|
});
|
|
2278
2690
|
var add = defineCommand({
|
|
@@ -2302,10 +2714,19 @@ var add = defineCommand({
|
|
|
2302
2714
|
alias: "m",
|
|
2303
2715
|
default: false,
|
|
2304
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')"
|
|
2305
2721
|
}
|
|
2306
2722
|
},
|
|
2307
2723
|
async run({ args }) {
|
|
2308
|
-
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
|
+
});
|
|
2309
2730
|
}
|
|
2310
2731
|
});
|
|
2311
2732
|
var detect = defineCommand({
|