blockend-cli 2.0.2 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -606
- 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 +9 -0
- package/dist/block-install/plan.d.ts +3 -0
- package/dist/block-install/preview.d.ts +2 -0
- package/dist/block-install/registry.d.ts +11 -0
- package/dist/block-install/types.d.ts +94 -0
- package/dist/block-install/variants.d.ts +2 -0
- package/dist/commands/add.d.ts +0 -39
- package/dist/commands/init.d.ts +0 -22
- 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 +964 -701
- package/dist/types/types.d.ts +1 -0
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -4,22 +4,537 @@
|
|
|
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
|
-
|
|
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
|
+
|
|
199
|
+
// src/block-install/registry.ts
|
|
200
|
+
var DEFAULT_REPO_OWNER = "codewithnuh";
|
|
201
|
+
var DEFAULT_REPO_NAME = "blockend";
|
|
202
|
+
var DEFAULT_BRANCH = "master";
|
|
203
|
+
function createGitHubRegistrySource(options = {}) {
|
|
204
|
+
const baseUrl = options.baseUrl ?? `https://raw.githubusercontent.com/${options.owner ?? DEFAULT_REPO_OWNER}/${options.repo ?? DEFAULT_REPO_NAME}/${options.branch ?? DEFAULT_BRANCH}`;
|
|
205
|
+
return {
|
|
206
|
+
async fetchRegistry() {
|
|
207
|
+
const response = await fetch(`${baseUrl}/registry/index.json`);
|
|
208
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
209
|
+
return parseRegistry(await response.json(), { allowLegacy: true });
|
|
210
|
+
},
|
|
211
|
+
async fetchFile(sourcePath) {
|
|
212
|
+
const response = await fetch(`${baseUrl}/${sourcePath}`);
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
throw new Error(`Download failed: ${sourcePath} (HTTP ${response.status})`);
|
|
215
|
+
}
|
|
216
|
+
return response.text();
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async function loadRegistry(source) {
|
|
221
|
+
return source.fetchRegistry();
|
|
222
|
+
}
|
|
223
|
+
function getBlockMap(registry) {
|
|
224
|
+
return registry.blocks;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/block-install/plan.ts
|
|
228
|
+
import path from "path";
|
|
229
|
+
|
|
230
|
+
// src/block-install/variants.ts
|
|
231
|
+
import { join } from "path";
|
|
232
|
+
import fs from "fs/promises";
|
|
233
|
+
function selectVariant(variantKeys, preferredVariant) {
|
|
234
|
+
if (preferredVariant && variantKeys.includes(preferredVariant)) {
|
|
235
|
+
return preferredVariant;
|
|
236
|
+
}
|
|
237
|
+
if (variantKeys.includes("memory")) return "memory";
|
|
238
|
+
if (variantKeys.includes("default")) return "default";
|
|
239
|
+
return variantKeys[0];
|
|
240
|
+
}
|
|
241
|
+
async function resolvePreferredVariant(variantKeys, recordedVariant, targetFolder) {
|
|
242
|
+
if (recordedVariant && variantKeys.includes(recordedVariant)) {
|
|
243
|
+
return recordedVariant;
|
|
244
|
+
}
|
|
245
|
+
if (variantKeys.includes("redis")) {
|
|
246
|
+
const markers = [
|
|
247
|
+
join(targetFolder, "variants", "redis-store.ts"),
|
|
248
|
+
join(targetFolder, "variants", "redis-store.test.ts"),
|
|
249
|
+
join(targetFolder, "variants", "redis.test.ts")
|
|
250
|
+
];
|
|
251
|
+
for (const marker of markers) {
|
|
252
|
+
try {
|
|
253
|
+
await fs.access(marker);
|
|
254
|
+
return "redis";
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/block-install/plan.ts
|
|
263
|
+
function blockSupportsEnv(block, environment) {
|
|
18
264
|
if (block.frameworks && block.frameworks.length > 0) {
|
|
19
|
-
return block.frameworks.includes("*") || block.frameworks.includes(
|
|
265
|
+
return block.frameworks.includes("*") || block.frameworks.includes(environment);
|
|
266
|
+
}
|
|
267
|
+
const framework = environment;
|
|
268
|
+
return block.adapters?.[framework] !== void 0 || block.environments?.[framework] !== void 0;
|
|
269
|
+
}
|
|
270
|
+
function resolveAdapterContext(block, environment) {
|
|
271
|
+
const framework = environment;
|
|
272
|
+
return block.adapters?.[framework] ?? block.adapters?.["*"] ?? block.environments?.[framework] ?? block.environments?.["*"] ?? null;
|
|
273
|
+
}
|
|
274
|
+
function normalizeFiles(files) {
|
|
275
|
+
if (!files) return [];
|
|
276
|
+
return files.map(
|
|
277
|
+
(entry) => typeof entry === "string" ? { source: entry, target: path.basename(entry) } : entry
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
function planError(code, blockName, environment) {
|
|
281
|
+
if (code === "not-found") {
|
|
282
|
+
return {
|
|
283
|
+
code,
|
|
284
|
+
blockName,
|
|
285
|
+
environment,
|
|
286
|
+
message: `Block "${blockName}" not found in the registry.`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (code === "unsupported-environment") {
|
|
290
|
+
return {
|
|
291
|
+
code,
|
|
292
|
+
blockName,
|
|
293
|
+
environment,
|
|
294
|
+
message: `Block "${blockName}" does not support environment: ${environment}`
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (code === "no-adapter") {
|
|
298
|
+
return {
|
|
299
|
+
code,
|
|
300
|
+
blockName,
|
|
301
|
+
environment,
|
|
302
|
+
message: `Block "${blockName}" has no adapter or environment entry for: ${environment}. This is a registry configuration issue \u2014 please open an issue.`
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
code,
|
|
307
|
+
blockName,
|
|
308
|
+
environment,
|
|
309
|
+
message: `No variants found for "${blockName}" / "${environment}".`
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async function planBlock(registry, blockName, environment, options = {}) {
|
|
313
|
+
const block = getBlockMap(registry)[blockName];
|
|
314
|
+
if (!block) {
|
|
315
|
+
return { ok: false, error: planError("not-found", blockName, environment) };
|
|
316
|
+
}
|
|
317
|
+
if (!blockSupportsEnv(block, environment)) {
|
|
318
|
+
return { ok: false, error: planError("unsupported-environment", blockName, environment) };
|
|
319
|
+
}
|
|
320
|
+
const adapterContext = resolveAdapterContext(block, environment);
|
|
321
|
+
if (!adapterContext) {
|
|
322
|
+
return { ok: false, error: planError("no-adapter", blockName, environment) };
|
|
323
|
+
}
|
|
324
|
+
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
325
|
+
if (variantKeys.length === 0) {
|
|
326
|
+
return { ok: false, error: planError("no-variants", blockName, environment) };
|
|
327
|
+
}
|
|
328
|
+
let preferred = options.variant ?? options.recordedVariant;
|
|
329
|
+
if (options.targetFolder && (!preferred || !variantKeys.includes(preferred))) {
|
|
330
|
+
preferred = await resolvePreferredVariant(variantKeys, preferred, options.targetFolder) ?? preferred;
|
|
331
|
+
}
|
|
332
|
+
const selectedVariant = selectVariant(variantKeys, preferred);
|
|
333
|
+
const variantMeta = adapterContext.variants[selectedVariant];
|
|
334
|
+
const files = [];
|
|
335
|
+
if (block.baseFiles?.length) files.push(...block.baseFiles);
|
|
336
|
+
if (adapterContext.core) {
|
|
337
|
+
files.push({ source: adapterContext.core, target: path.basename(adapterContext.core) });
|
|
338
|
+
}
|
|
339
|
+
files.push(...normalizeFiles(variantMeta?.files));
|
|
340
|
+
const plan = {
|
|
341
|
+
blockName,
|
|
342
|
+
block,
|
|
343
|
+
environment,
|
|
344
|
+
variant: selectedVariant,
|
|
345
|
+
variantKeys,
|
|
346
|
+
files,
|
|
347
|
+
dependencies: [
|
|
348
|
+
...block.dependencies ?? [],
|
|
349
|
+
...adapterContext.dependencies ?? [],
|
|
350
|
+
...variantMeta?.dependencies ?? []
|
|
351
|
+
],
|
|
352
|
+
devDependencies: [
|
|
353
|
+
...block.devDependencies ?? [],
|
|
354
|
+
...adapterContext.devDependencies ?? [],
|
|
355
|
+
...variantMeta?.devDependencies ?? []
|
|
356
|
+
]
|
|
357
|
+
};
|
|
358
|
+
return { ok: true, plan };
|
|
359
|
+
}
|
|
360
|
+
async function listAvailableBlocks(registry, environment) {
|
|
361
|
+
const available = [];
|
|
362
|
+
for (const [name, block] of Object.entries(getBlockMap(registry))) {
|
|
363
|
+
if (!block) continue;
|
|
364
|
+
const result = await planBlock(registry, name, environment);
|
|
365
|
+
if (!result.ok) continue;
|
|
366
|
+
available.push({
|
|
367
|
+
name,
|
|
368
|
+
description: block.description,
|
|
369
|
+
variants: result.plan.variantKeys
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return available;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/block-install/preview.ts
|
|
376
|
+
import path2 from "path";
|
|
377
|
+
import fs2 from "fs/promises";
|
|
378
|
+
async function previewBlock(plan, options) {
|
|
379
|
+
const previews = [];
|
|
380
|
+
for (const file of plan.files) {
|
|
381
|
+
const targetPath = path2.join(options.targetFolder, file.target);
|
|
382
|
+
let newContent;
|
|
383
|
+
try {
|
|
384
|
+
newContent = await options.source.fetchFile(file.source);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (options.onDownloadError === "empty") {
|
|
387
|
+
newContent = "";
|
|
388
|
+
} else {
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
let existingContent = null;
|
|
393
|
+
let status = "new";
|
|
394
|
+
try {
|
|
395
|
+
existingContent = await fs2.readFile(targetPath, "utf-8");
|
|
396
|
+
status = existingContent === newContent ? "unchanged" : "modified";
|
|
397
|
+
} catch {
|
|
398
|
+
}
|
|
399
|
+
previews.push({
|
|
400
|
+
name: file.target,
|
|
401
|
+
sourcePath: file.source,
|
|
402
|
+
targetPath,
|
|
403
|
+
existingContent,
|
|
404
|
+
newContent,
|
|
405
|
+
status
|
|
406
|
+
});
|
|
20
407
|
}
|
|
21
|
-
return
|
|
408
|
+
return previews;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/block-install/apply.ts
|
|
412
|
+
import path4, { dirname as dirname3 } from "path";
|
|
413
|
+
import fs4 from "fs/promises";
|
|
414
|
+
|
|
415
|
+
// src/block-install/imports.ts
|
|
416
|
+
import path3, { dirname } from "path";
|
|
417
|
+
function resolveIndexImport(importPath, sourceFileDir, downloadedFiles, blockRoot) {
|
|
418
|
+
const resolved = path3.resolve(sourceFileDir, importPath);
|
|
419
|
+
for (const downloaded of downloadedFiles) {
|
|
420
|
+
const downloadedAbs = path3.resolve(blockRoot, downloaded);
|
|
421
|
+
const dirOfDownloaded = dirname(downloadedAbs);
|
|
422
|
+
const basenameOfDownloaded = path3.basename(downloadedAbs);
|
|
423
|
+
if (dirOfDownloaded === resolved && basenameOfDownloaded.startsWith("index.")) {
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
function rewriteFileImports(fileContent, writtenFilePath, blocksRoot, aliasBase, strategy, downloadedFiles = /* @__PURE__ */ new Set(), blockRoot = dirname(writtenFilePath)) {
|
|
430
|
+
const importRegex = /((?:from|import)\s*)(['"`])(\.\.?\/[^'"` \n\r]+)(['"`])/g;
|
|
431
|
+
const fileDir = dirname(writtenFilePath);
|
|
432
|
+
return fileContent.replace(importRegex, (match, keyword, openQuote, importPath, closeQuote) => {
|
|
433
|
+
let targetPath = importPath;
|
|
434
|
+
if (aliasBase && !aliasBase.startsWith(".")) {
|
|
435
|
+
const absoluteTarget = path3.resolve(fileDir, importPath);
|
|
436
|
+
const fromBlocksRoot = path3.relative(blocksRoot, absoluteTarget).replace(/\\/g, "/");
|
|
437
|
+
if (!fromBlocksRoot.startsWith("..")) {
|
|
438
|
+
targetPath = `${aliasBase}/${fromBlocksRoot}`.replace(/\/+/g, "/");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const hasJsExt = targetPath.endsWith(".js");
|
|
442
|
+
if (strategy === "rewrite" && !hasJsExt) {
|
|
443
|
+
targetPath = targetPath.replace(/\.(ts|tsx|mts|cts)$/, "");
|
|
444
|
+
if (downloadedFiles.size > 0) {
|
|
445
|
+
if (resolveIndexImport(importPath, fileDir, downloadedFiles, blockRoot)) {
|
|
446
|
+
targetPath = `${targetPath}/index`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
targetPath = `${targetPath}.js`;
|
|
450
|
+
} else if (strategy === "remove" && hasJsExt) {
|
|
451
|
+
targetPath = targetPath.slice(0, -3);
|
|
452
|
+
}
|
|
453
|
+
return `${keyword}${openQuote}${targetPath}${closeQuote}`;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/block-install/config.ts
|
|
458
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
459
|
+
import fs3 from "fs/promises";
|
|
460
|
+
async function findUp(filename, startDir) {
|
|
461
|
+
let dir = startDir;
|
|
462
|
+
while (true) {
|
|
463
|
+
const candidate = join2(dir, filename);
|
|
464
|
+
try {
|
|
465
|
+
await fs3.access(candidate);
|
|
466
|
+
return candidate;
|
|
467
|
+
} catch {
|
|
468
|
+
const parent = dirname2(dir);
|
|
469
|
+
if (parent === dir) break;
|
|
470
|
+
dir = parent;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
async function recordInstalledBlock(configPath, record) {
|
|
476
|
+
try {
|
|
477
|
+
const raw = await fs3.readFile(configPath, "utf-8");
|
|
478
|
+
const config = JSON.parse(raw);
|
|
479
|
+
if (!Array.isArray(config.installed)) config.installed = [];
|
|
480
|
+
config.installed = config.installed.filter((block) => block.name !== record.name);
|
|
481
|
+
config.installed.push(record);
|
|
482
|
+
await fs3.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/block-install/apply.ts
|
|
488
|
+
function hashContent(content, previous) {
|
|
489
|
+
let hash = previous;
|
|
490
|
+
for (let i = 0; i < content.length; i++) {
|
|
491
|
+
hash = (hash << 5) - hash + content.charCodeAt(i) | 0;
|
|
492
|
+
}
|
|
493
|
+
return hash;
|
|
494
|
+
}
|
|
495
|
+
async function applyBlock(plan, options) {
|
|
496
|
+
const targetFolder = path4.resolve(options.blocksRoot, plan.blockName);
|
|
497
|
+
const downloadedTargets = new Set(plan.files.map((file) => file.target));
|
|
498
|
+
const downloaded = [];
|
|
499
|
+
for (const file of plan.files) {
|
|
500
|
+
downloaded.push({ file, raw: await options.source.fetchFile(file.source) });
|
|
501
|
+
}
|
|
502
|
+
const filesWritten = [];
|
|
503
|
+
let contentHash = 0;
|
|
504
|
+
for (const { file, raw } of downloaded) {
|
|
505
|
+
const writtenFilePath = path4.join(targetFolder, file.target);
|
|
506
|
+
const content = rewriteFileImports(
|
|
507
|
+
raw,
|
|
508
|
+
writtenFilePath,
|
|
509
|
+
options.blocksRoot,
|
|
510
|
+
options.aliasBase,
|
|
511
|
+
options.rewriteStrategy,
|
|
512
|
+
downloadedTargets,
|
|
513
|
+
targetFolder
|
|
514
|
+
);
|
|
515
|
+
await fs4.mkdir(dirname3(writtenFilePath), { recursive: true });
|
|
516
|
+
await fs4.writeFile(writtenFilePath, content, "utf-8");
|
|
517
|
+
filesWritten.push(writtenFilePath);
|
|
518
|
+
contentHash = hashContent(content, contentHash);
|
|
519
|
+
}
|
|
520
|
+
const record = {
|
|
521
|
+
name: plan.blockName,
|
|
522
|
+
version: plan.block.version ?? "0.0.0",
|
|
523
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
524
|
+
files: filesWritten.map((file) => path4.relative(targetFolder, file).replace(/\\/g, "/")),
|
|
525
|
+
contentHash: contentHash.toString(16),
|
|
526
|
+
variant: plan.variant
|
|
527
|
+
};
|
|
528
|
+
await recordInstalledBlock(options.configPath, record);
|
|
529
|
+
return {
|
|
530
|
+
block: plan.blockName,
|
|
531
|
+
variant: plan.variant,
|
|
532
|
+
files: filesWritten,
|
|
533
|
+
record
|
|
534
|
+
};
|
|
22
535
|
}
|
|
536
|
+
|
|
537
|
+
// src/block-install/deps.ts
|
|
23
538
|
function parseDepSpec(dep) {
|
|
24
539
|
const atIndex = dep.lastIndexOf("@");
|
|
25
540
|
if (atIndex > 0) {
|
|
@@ -77,49 +592,8 @@ var getMissingDependencies = (deps, installed, forceLatest = false) => {
|
|
|
77
592
|
}
|
|
78
593
|
return result;
|
|
79
594
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
function resolveIndexImport(importPath, sourceFileDir, downloadedFiles) {
|
|
84
|
-
const resolved = path.resolve(sourceFileDir, importPath);
|
|
85
|
-
for (const downloaded of downloadedFiles) {
|
|
86
|
-
const downloadedAbs = path.resolve(sourceFileDir, downloaded);
|
|
87
|
-
const dirOfDownloaded = dirname(downloadedAbs);
|
|
88
|
-
const basenameOfDownloaded = path.basename(downloadedAbs);
|
|
89
|
-
if (dirOfDownloaded === resolved && basenameOfDownloaded.startsWith("index.")) {
|
|
90
|
-
return `${importPath}/index`;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
function rewriteFileImports(fileContent, writtenFilePath, blocksRoot, aliasBase, strategy, downloadedFiles = /* @__PURE__ */ new Set()) {
|
|
96
|
-
const importRegex = /((?:from|import)\s*)(['"`])(\.\.?\/[^'"` \n\r]+)(['"`])/g;
|
|
97
|
-
const fileDir = dirname(writtenFilePath);
|
|
98
|
-
return fileContent.replace(importRegex, (match, keyword, openQuote, importPath, closeQuote) => {
|
|
99
|
-
let targetPath = importPath;
|
|
100
|
-
if (aliasBase && !aliasBase.startsWith(".")) {
|
|
101
|
-
const absoluteTarget = path.resolve(fileDir, importPath);
|
|
102
|
-
const fromBlocksRoot = path.relative(blocksRoot, absoluteTarget).replace(/\\/g, "/");
|
|
103
|
-
if (!fromBlocksRoot.startsWith("..")) {
|
|
104
|
-
targetPath = `${aliasBase}/${fromBlocksRoot}`.replace(/\/+/g, "/");
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const hasJsExt = targetPath.endsWith(".js");
|
|
108
|
-
if (strategy === "rewrite" && !hasJsExt) {
|
|
109
|
-
targetPath = targetPath.replace(/\.(ts|tsx|mts|cts)$/, "");
|
|
110
|
-
if (downloadedFiles.size > 0) {
|
|
111
|
-
const indexResolved = resolveIndexImport(targetPath, fileDir, downloadedFiles);
|
|
112
|
-
if (indexResolved) {
|
|
113
|
-
targetPath = indexResolved;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
targetPath = `${targetPath}.js`;
|
|
117
|
-
} else if (strategy === "remove" && hasJsExt) {
|
|
118
|
-
targetPath = targetPath.slice(0, -3);
|
|
119
|
-
}
|
|
120
|
-
return `${keyword}${openQuote}${targetPath}${closeQuote}`;
|
|
121
|
-
});
|
|
122
|
-
}
|
|
595
|
+
|
|
596
|
+
// src/commands/add.ts
|
|
123
597
|
function outputError(json, message) {
|
|
124
598
|
if (json) {
|
|
125
599
|
process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
|
|
@@ -140,46 +614,37 @@ function handleCancel(value) {
|
|
|
140
614
|
process.exit(0);
|
|
141
615
|
}
|
|
142
616
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
await fs.access(candidate);
|
|
149
|
-
return candidate;
|
|
150
|
-
} catch {
|
|
151
|
-
const parent = dirname(dir);
|
|
152
|
-
if (parent === dir) break;
|
|
153
|
-
dir = parent;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return null;
|
|
617
|
+
function failureMessage(code, blockName) {
|
|
618
|
+
if (code === "not-found") return `Block "${blockName}" not found`;
|
|
619
|
+
if (code === "unsupported-environment") return `Block "${blockName}" incompatible`;
|
|
620
|
+
if (code === "no-adapter") return `No adapter for "${blockName}"`;
|
|
621
|
+
return `No variants for "${blockName}"`;
|
|
157
622
|
}
|
|
158
|
-
async function addSingleBlock(targetBlock,
|
|
159
|
-
const { yes = false, json = false } = options;
|
|
623
|
+
async function addSingleBlock(targetBlock, context) {
|
|
624
|
+
const { yes = false, json = false } = context.options;
|
|
625
|
+
const {
|
|
626
|
+
registry,
|
|
627
|
+
source,
|
|
628
|
+
config,
|
|
629
|
+
configPath,
|
|
630
|
+
rootDir,
|
|
631
|
+
physicalBlocksPath,
|
|
632
|
+
blocksRootAbsolute,
|
|
633
|
+
aliasBase,
|
|
634
|
+
envKey,
|
|
635
|
+
rewriteStrategy
|
|
636
|
+
} = context;
|
|
160
637
|
const s = spinner();
|
|
161
|
-
const
|
|
162
|
-
if (!
|
|
163
|
-
outputError(json,
|
|
164
|
-
return {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
const adapterContext = resolveAdapterContext(blockMeta, envKey);
|
|
171
|
-
if (!adapterContext) {
|
|
172
|
-
outputError(
|
|
173
|
-
json,
|
|
174
|
-
`Block "${targetBlock}" has no adapter or environment entry for: ${envKey}. This is a registry configuration issue \u2014 please open an issue.`
|
|
175
|
-
);
|
|
176
|
-
return { success: false, filesWritten: [], message: `No adapter for "${targetBlock}"` };
|
|
177
|
-
}
|
|
178
|
-
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
179
|
-
if (variantKeys.length === 0) {
|
|
180
|
-
outputError(json, `No variants found for "${targetBlock}" / "${envKey}".`);
|
|
181
|
-
return { success: false, filesWritten: [], message: `No variants for "${targetBlock}"` };
|
|
638
|
+
const initialPlan = await planBlock(registry, targetBlock, envKey);
|
|
639
|
+
if (!initialPlan.ok) {
|
|
640
|
+
outputError(json, initialPlan.error.message);
|
|
641
|
+
return {
|
|
642
|
+
success: false,
|
|
643
|
+
filesWritten: [],
|
|
644
|
+
message: failureMessage(initialPlan.error.code, targetBlock)
|
|
645
|
+
};
|
|
182
646
|
}
|
|
647
|
+
const variantKeys = initialPlan.plan.variantKeys;
|
|
183
648
|
let selectedVariant;
|
|
184
649
|
if (yes) {
|
|
185
650
|
selectedVariant = variantKeys.includes("memory") ? "memory" : variantKeys[0];
|
|
@@ -190,22 +655,36 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
190
655
|
} else {
|
|
191
656
|
const variantPrompt = await select({
|
|
192
657
|
message: `Select a storage variant for ${targetBlock}:`,
|
|
193
|
-
options: variantKeys.map((
|
|
658
|
+
options: variantKeys.map((variant) => ({ value: variant, label: variant.toUpperCase() }))
|
|
194
659
|
});
|
|
195
660
|
handleCancel(variantPrompt);
|
|
196
661
|
selectedVariant = variantPrompt;
|
|
197
662
|
}
|
|
198
|
-
|
|
199
|
-
|
|
663
|
+
let plan = initialPlan.plan;
|
|
664
|
+
if (selectedVariant !== initialPlan.plan.variant) {
|
|
665
|
+
const selectedPlan = await planBlock(registry, targetBlock, envKey, {
|
|
666
|
+
variant: selectedVariant
|
|
667
|
+
});
|
|
668
|
+
if (!selectedPlan.ok) {
|
|
669
|
+
outputError(json, selectedPlan.error.message);
|
|
670
|
+
return {
|
|
671
|
+
success: false,
|
|
672
|
+
filesWritten: [],
|
|
673
|
+
message: failureMessage(selectedPlan.error.code, targetBlock)
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
plan = selectedPlan.plan;
|
|
677
|
+
}
|
|
678
|
+
const targetFolder = path5.resolve(blocksRootAbsolute, targetBlock);
|
|
200
679
|
const packageJsonPath = await findUp("package.json", rootDir);
|
|
201
680
|
if (!packageJsonPath) {
|
|
202
681
|
outputError(json, "Could not locate package.json.");
|
|
203
682
|
return { success: false, filesWritten: [], message: "package.json not found" };
|
|
204
683
|
}
|
|
205
|
-
const packageJsonDir =
|
|
684
|
+
const packageJsonDir = dirname4(packageJsonPath);
|
|
206
685
|
let packageJson;
|
|
207
686
|
try {
|
|
208
|
-
packageJson = JSON.parse(await
|
|
687
|
+
packageJson = JSON.parse(await fs5.readFile(packageJsonPath, "utf-8"));
|
|
209
688
|
} catch (err) {
|
|
210
689
|
outputError(json, "Failed to parse package.json.");
|
|
211
690
|
if (!json) log.error(pc.dim(String(err)));
|
|
@@ -215,20 +694,10 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
215
694
|
...packageJson.dependencies ?? {},
|
|
216
695
|
...packageJson.devDependencies ?? {}
|
|
217
696
|
};
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
];
|
|
223
|
-
const allBlockDevDeps = [
|
|
224
|
-
...blockMeta.devDependencies ?? [],
|
|
225
|
-
...adapterContext.devDependencies ?? [],
|
|
226
|
-
...variantMeta?.devDependencies ?? []
|
|
227
|
-
];
|
|
228
|
-
const missingProd = getMissingDependencies(allBlockDeps, installedDeps, true);
|
|
229
|
-
const missingDev = getMissingDependencies(allBlockDevDeps, installedDeps, true);
|
|
230
|
-
const prodConflicts = getVersionConflicts(allBlockDeps, installedDeps);
|
|
231
|
-
const devConflicts = getVersionConflicts(allBlockDevDeps, installedDeps);
|
|
697
|
+
const missingProd = getMissingDependencies(plan.dependencies, installedDeps, true);
|
|
698
|
+
const missingDev = getMissingDependencies(plan.devDependencies, installedDeps, true);
|
|
699
|
+
const prodConflicts = getVersionConflicts(plan.dependencies, installedDeps);
|
|
700
|
+
const devConflicts = getVersionConflicts(plan.devDependencies, installedDeps);
|
|
232
701
|
const allConflicts = [...prodConflicts, ...devConflicts];
|
|
233
702
|
if (allConflicts.length > 0 && !json) {
|
|
234
703
|
log.warn(`Version conflicts detected:`);
|
|
@@ -244,7 +713,7 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
244
713
|
});
|
|
245
714
|
if (!yes) handleCancel(shouldInstall);
|
|
246
715
|
if (shouldInstall) {
|
|
247
|
-
const pm =
|
|
716
|
+
const pm = config.packageManager || "npm";
|
|
248
717
|
if (!json) s.start(`Installing via ${pm}...`);
|
|
249
718
|
const tasks = [];
|
|
250
719
|
if (missingProd.length > 0) {
|
|
@@ -284,27 +753,10 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
284
753
|
}
|
|
285
754
|
}
|
|
286
755
|
}
|
|
287
|
-
const filesToDownload = [];
|
|
288
|
-
if (blockMeta.baseFiles?.length) {
|
|
289
|
-
filesToDownload.push(...blockMeta.baseFiles);
|
|
290
|
-
}
|
|
291
|
-
if (adapterContext.core) {
|
|
292
|
-
filesToDownload.push({
|
|
293
|
-
source: adapterContext.core,
|
|
294
|
-
target: path.basename(adapterContext.core)
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
298
|
-
for (const entry of variantMeta.files) {
|
|
299
|
-
filesToDownload.push(
|
|
300
|
-
typeof entry === "string" ? { source: entry, target: path.basename(entry) } : entry
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
756
|
let hasConflict = false;
|
|
305
|
-
for (const
|
|
757
|
+
for (const file of plan.files) {
|
|
306
758
|
try {
|
|
307
|
-
await
|
|
759
|
+
await fs5.access(join3(targetFolder, file.target));
|
|
308
760
|
hasConflict = true;
|
|
309
761
|
break;
|
|
310
762
|
} catch {
|
|
@@ -321,56 +773,18 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
321
773
|
}
|
|
322
774
|
}
|
|
323
775
|
if (!json) s.start(`Downloading ${targetBlock}...`);
|
|
324
|
-
const filesWritten = [];
|
|
325
|
-
const downloadedTargets = /* @__PURE__ */ new Set();
|
|
326
776
|
try {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
let contentHash = 0;
|
|
335
|
-
for (const { fm, raw } of fileBatches) {
|
|
336
|
-
const writtenFilePath = path.join(targetFolder, fm.target);
|
|
337
|
-
const content = rewriteFileImports(
|
|
338
|
-
raw,
|
|
339
|
-
writtenFilePath,
|
|
340
|
-
blocksRootAbsolute,
|
|
341
|
-
aliasBase,
|
|
342
|
-
rewriteStrategy,
|
|
343
|
-
downloadedTargets
|
|
344
|
-
);
|
|
345
|
-
await fs.mkdir(dirname(writtenFilePath), { recursive: true });
|
|
346
|
-
await fs.writeFile(writtenFilePath, content, "utf-8");
|
|
347
|
-
filesWritten.push(writtenFilePath);
|
|
348
|
-
for (let i = 0; i < content.length; i++) {
|
|
349
|
-
contentHash = (contentHash << 5) - contentHash + content.charCodeAt(i) | 0;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
const blockVersion = blockMeta.version ?? "0.0.0";
|
|
353
|
-
const installedRecord = {
|
|
354
|
-
name: targetBlock,
|
|
355
|
-
version: blockVersion,
|
|
356
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
357
|
-
files: filesWritten.map((f) => path.relative(targetFolder, f).replace(/\\/g, "/")),
|
|
358
|
-
contentHash: contentHash.toString(16)
|
|
359
|
-
};
|
|
360
|
-
try {
|
|
361
|
-
const blockendPath = join(rootDir, "blockend.json");
|
|
362
|
-
const cfgRaw = await fs.readFile(blockendPath, "utf-8");
|
|
363
|
-
const cfgData = JSON.parse(cfgRaw);
|
|
364
|
-
if (!Array.isArray(cfgData.installed)) cfgData.installed = [];
|
|
365
|
-
cfgData.installed = cfgData.installed.filter((b) => b.name !== targetBlock);
|
|
366
|
-
cfgData.installed.push(installedRecord);
|
|
367
|
-
await fs.writeFile(blockendPath, JSON.stringify(cfgData, null, 2), "utf-8");
|
|
368
|
-
} catch {
|
|
369
|
-
}
|
|
777
|
+
const applied = await applyBlock(plan, {
|
|
778
|
+
source,
|
|
779
|
+
blocksRoot: blocksRootAbsolute,
|
|
780
|
+
aliasBase,
|
|
781
|
+
rewriteStrategy,
|
|
782
|
+
configPath
|
|
783
|
+
});
|
|
370
784
|
if (!json) s.stop("Files written.");
|
|
371
785
|
return {
|
|
372
786
|
success: true,
|
|
373
|
-
filesWritten,
|
|
787
|
+
filesWritten: applied.files,
|
|
374
788
|
message: `${targetBlock} added to ${physicalBlocksPath}/${targetBlock}`
|
|
375
789
|
};
|
|
376
790
|
} catch (err) {
|
|
@@ -393,10 +807,10 @@ async function addCommand(blockName, options = {}) {
|
|
|
393
807
|
outputError(json, "blockend.json not found. Run 'npx blockend init' first.");
|
|
394
808
|
return;
|
|
395
809
|
}
|
|
396
|
-
const rootDir =
|
|
810
|
+
const rootDir = dirname4(configPath);
|
|
397
811
|
let config;
|
|
398
812
|
try {
|
|
399
|
-
config = JSON.parse(await
|
|
813
|
+
config = JSON.parse(await fs5.readFile(configPath, "utf-8"));
|
|
400
814
|
} catch (err) {
|
|
401
815
|
outputError(json, "Failed to parse blockend.json.");
|
|
402
816
|
if (!json) log.error(pc.dim(String(err)));
|
|
@@ -407,17 +821,16 @@ async function addCommand(blockName, options = {}) {
|
|
|
407
821
|
return;
|
|
408
822
|
}
|
|
409
823
|
const physicalBlocksPath = config.paths.blocks;
|
|
410
|
-
const blocksRootAbsolute =
|
|
824
|
+
const blocksRootAbsolute = path5.resolve(rootDir, physicalBlocksPath);
|
|
411
825
|
const aliasBase = config.aliases.blocks;
|
|
412
826
|
const envKey = config.environment;
|
|
413
827
|
const rewriteStrategy = config.importRewriteStrategy ?? "remove";
|
|
828
|
+
const source = createGitHubRegistrySource();
|
|
414
829
|
const s = spinner();
|
|
415
830
|
if (!json) s.start("Fetching registry...");
|
|
416
831
|
let registry;
|
|
417
832
|
try {
|
|
418
|
-
|
|
419
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
420
|
-
registry = await res.json();
|
|
833
|
+
registry = await loadRegistry(source);
|
|
421
834
|
if (!json) s.stop("Registry synced.");
|
|
422
835
|
} catch (err) {
|
|
423
836
|
if (!json) {
|
|
@@ -426,26 +839,38 @@ async function addCommand(blockName, options = {}) {
|
|
|
426
839
|
} else outputError(json, "Failed to fetch registry from GitHub.");
|
|
427
840
|
return;
|
|
428
841
|
}
|
|
429
|
-
const
|
|
842
|
+
const context = {
|
|
843
|
+
registry,
|
|
844
|
+
source,
|
|
845
|
+
config,
|
|
846
|
+
configPath,
|
|
847
|
+
rootDir,
|
|
848
|
+
physicalBlocksPath,
|
|
849
|
+
blocksRootAbsolute,
|
|
850
|
+
aliasBase,
|
|
851
|
+
envKey,
|
|
852
|
+
rewriteStrategy,
|
|
853
|
+
options
|
|
854
|
+
};
|
|
430
855
|
if (yes && !blockName) {
|
|
431
856
|
outputError(json, "Block name required with --yes. Usage: blockend add <block> --yes");
|
|
432
857
|
return;
|
|
433
858
|
}
|
|
434
859
|
if (multi && !blockName) {
|
|
435
|
-
const available =
|
|
436
|
-
([, block]) => block != null && blockSupportsEnv(block, envKey)
|
|
437
|
-
);
|
|
860
|
+
const available = await listAvailableBlocks(registry, envKey);
|
|
438
861
|
if (available.length === 0) {
|
|
439
862
|
outputError(json, `No blocks available for environment: ${envKey}`);
|
|
440
863
|
if (!json) outro(pc.dim("Exiting."));
|
|
441
864
|
return;
|
|
442
865
|
}
|
|
443
|
-
const maxKeyLen = Math.max(...available.map((
|
|
866
|
+
const maxKeyLen = Math.max(...available.map((block) => block.name.length), 0);
|
|
444
867
|
const multiSelectPrompt = await multiselect({
|
|
445
868
|
message: "Select blocks to add (space to select, enter to confirm):",
|
|
446
|
-
options: available.map((
|
|
447
|
-
value:
|
|
448
|
-
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(
|
|
869
|
+
options: available.map((block, i) => ({
|
|
870
|
+
value: block.name,
|
|
871
|
+
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(
|
|
872
|
+
pc.cyan(block.name.padEnd(maxKeyLen + 4))
|
|
873
|
+
)}${pc.dim(
|
|
449
874
|
block.description.length > 60 ? `${block.description.slice(0, 60)}...` : block.description
|
|
450
875
|
)}`
|
|
451
876
|
}))
|
|
@@ -461,18 +886,7 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
461
886
|
`);
|
|
462
887
|
const results = [];
|
|
463
888
|
for (const block of selectedBlocks) {
|
|
464
|
-
const result2 = await addSingleBlock(
|
|
465
|
-
block,
|
|
466
|
-
blockMap,
|
|
467
|
-
config,
|
|
468
|
-
rootDir,
|
|
469
|
-
physicalBlocksPath,
|
|
470
|
-
blocksRootAbsolute,
|
|
471
|
-
aliasBase,
|
|
472
|
-
envKey,
|
|
473
|
-
rewriteStrategy,
|
|
474
|
-
options
|
|
475
|
-
);
|
|
889
|
+
const result2 = await addSingleBlock(block, context);
|
|
476
890
|
if (result2.success) {
|
|
477
891
|
results.push(result2.message);
|
|
478
892
|
}
|
|
@@ -487,20 +901,20 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
487
901
|
}
|
|
488
902
|
let targetBlock = blockName;
|
|
489
903
|
if (!targetBlock) {
|
|
490
|
-
const available =
|
|
491
|
-
([, block]) => block != null && blockSupportsEnv(block, envKey)
|
|
492
|
-
);
|
|
904
|
+
const available = await listAvailableBlocks(registry, envKey);
|
|
493
905
|
if (available.length === 0) {
|
|
494
906
|
outputError(json, `No blocks available for environment: ${envKey}`);
|
|
495
907
|
if (!json) outro(pc.dim("Exiting."));
|
|
496
908
|
return;
|
|
497
909
|
}
|
|
498
|
-
const maxKeyLen = Math.max(...available.map((
|
|
910
|
+
const maxKeyLen = Math.max(...available.map((block) => block.name.length), 0);
|
|
499
911
|
const selectPrompt = await select({
|
|
500
912
|
message: "Which block would you like to add?",
|
|
501
|
-
options: available.map((
|
|
502
|
-
value:
|
|
503
|
-
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(
|
|
913
|
+
options: available.map((block, i) => ({
|
|
914
|
+
value: block.name,
|
|
915
|
+
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(
|
|
916
|
+
pc.cyan(block.name.padEnd(maxKeyLen + 4))
|
|
917
|
+
)}${pc.dim(
|
|
504
918
|
block.description.length > 60 ? `${block.description.slice(0, 60)}...` : block.description
|
|
505
919
|
)}`
|
|
506
920
|
}))
|
|
@@ -508,18 +922,7 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
508
922
|
handleCancel(selectPrompt);
|
|
509
923
|
targetBlock = selectPrompt;
|
|
510
924
|
}
|
|
511
|
-
const result = await addSingleBlock(
|
|
512
|
-
targetBlock,
|
|
513
|
-
blockMap,
|
|
514
|
-
config,
|
|
515
|
-
rootDir,
|
|
516
|
-
physicalBlocksPath,
|
|
517
|
-
blocksRootAbsolute,
|
|
518
|
-
aliasBase,
|
|
519
|
-
envKey,
|
|
520
|
-
rewriteStrategy,
|
|
521
|
-
options
|
|
522
|
-
);
|
|
925
|
+
const result = await addSingleBlock(targetBlock, context);
|
|
523
926
|
outputResult(json, {
|
|
524
927
|
success: result.success,
|
|
525
928
|
block: targetBlock,
|
|
@@ -529,20 +932,20 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
529
932
|
}
|
|
530
933
|
|
|
531
934
|
// src/commands/init.ts
|
|
532
|
-
import
|
|
533
|
-
import
|
|
935
|
+
import path7, { join as join9 } from "path";
|
|
936
|
+
import fs6 from "fs/promises";
|
|
534
937
|
import { existsSync as existsSync4 } from "fs";
|
|
535
938
|
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";
|
|
536
939
|
import pc3 from "picocolors";
|
|
537
940
|
|
|
538
941
|
// src/detectors/index.ts
|
|
539
|
-
import { join as
|
|
942
|
+
import path6, { join as join8 } from "path";
|
|
540
943
|
|
|
541
944
|
// src/detectors/readers/package.ts
|
|
542
945
|
import { readFile } from "fs/promises";
|
|
543
|
-
import { join as
|
|
946
|
+
import { join as join4 } from "path";
|
|
544
947
|
async function readPackageJson(cwd) {
|
|
545
|
-
const pkgPath =
|
|
948
|
+
const pkgPath = join4(cwd, "package.json");
|
|
546
949
|
try {
|
|
547
950
|
const content = await readFile(pkgPath, "utf-8");
|
|
548
951
|
return JSON.parse(content);
|
|
@@ -550,34 +953,126 @@ async function readPackageJson(cwd) {
|
|
|
550
953
|
throw new Error(`No package.json found at ${pkgPath}. Run blockend from your project root.`);
|
|
551
954
|
}
|
|
552
955
|
}
|
|
553
|
-
|
|
554
|
-
// src/detectors/readers/tsconfig.ts
|
|
555
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
556
|
-
import { existsSync } from "fs";
|
|
557
|
-
import { join as
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
956
|
+
|
|
957
|
+
// src/detectors/readers/tsconfig.ts
|
|
958
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
959
|
+
import { existsSync } from "fs";
|
|
960
|
+
import { join as join5 } from "path";
|
|
961
|
+
var CONFIG_FILES = [
|
|
962
|
+
{ filename: "tsconfig.json", kind: "tsconfig" },
|
|
963
|
+
{ filename: "jsconfig.json", kind: "jsconfig" }
|
|
964
|
+
];
|
|
965
|
+
async function readCompilerConfig(cwd) {
|
|
966
|
+
for (const { filename, kind } of CONFIG_FILES) {
|
|
967
|
+
const configPath = join5(cwd, filename);
|
|
968
|
+
if (!existsSync(configPath)) continue;
|
|
969
|
+
try {
|
|
970
|
+
const content = await readFile2(configPath, "utf-8");
|
|
971
|
+
const clean = stripTrailingCommas(stripJsonComments(content));
|
|
972
|
+
return {
|
|
973
|
+
kind,
|
|
974
|
+
config: JSON.parse(clean)
|
|
975
|
+
};
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
function stripJsonComments(input) {
|
|
982
|
+
let output = "";
|
|
983
|
+
let inString = false;
|
|
984
|
+
let inLineComment = false;
|
|
985
|
+
let inBlockComment = false;
|
|
986
|
+
let escaped = false;
|
|
987
|
+
for (let i = 0; i < input.length; i++) {
|
|
988
|
+
const char = input[i];
|
|
989
|
+
const next = input[i + 1];
|
|
990
|
+
if (inLineComment) {
|
|
991
|
+
if (char === "\n" || char === "\r") {
|
|
992
|
+
inLineComment = false;
|
|
993
|
+
output += char;
|
|
994
|
+
}
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (inBlockComment) {
|
|
998
|
+
if (char === "*" && next === "/") {
|
|
999
|
+
inBlockComment = false;
|
|
1000
|
+
i++;
|
|
1001
|
+
} else if (char === "\n" || char === "\r") {
|
|
1002
|
+
output += char;
|
|
1003
|
+
}
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (inString) {
|
|
1007
|
+
output += char;
|
|
1008
|
+
if (escaped) {
|
|
1009
|
+
escaped = false;
|
|
1010
|
+
} else if (char === "\\") {
|
|
1011
|
+
escaped = true;
|
|
1012
|
+
} else if (char === '"') {
|
|
1013
|
+
inString = false;
|
|
1014
|
+
}
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (char === '"') {
|
|
1018
|
+
inString = true;
|
|
1019
|
+
output += char;
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
if (char === "/" && next === "/") {
|
|
1023
|
+
inLineComment = true;
|
|
1024
|
+
i++;
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (char === "/" && next === "*") {
|
|
1028
|
+
inBlockComment = true;
|
|
1029
|
+
i++;
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
output += char;
|
|
1033
|
+
}
|
|
1034
|
+
return output;
|
|
1035
|
+
}
|
|
1036
|
+
function stripTrailingCommas(input) {
|
|
1037
|
+
let output = "";
|
|
1038
|
+
let inString = false;
|
|
1039
|
+
let escaped = false;
|
|
1040
|
+
for (let i = 0; i < input.length; i++) {
|
|
1041
|
+
const char = input[i];
|
|
1042
|
+
if (inString) {
|
|
1043
|
+
output += char;
|
|
1044
|
+
if (escaped) {
|
|
1045
|
+
escaped = false;
|
|
1046
|
+
} else if (char === "\\") {
|
|
1047
|
+
escaped = true;
|
|
1048
|
+
} else if (char === '"') {
|
|
1049
|
+
inString = false;
|
|
1050
|
+
}
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (char === '"') {
|
|
1054
|
+
inString = true;
|
|
1055
|
+
output += char;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (char === ",") {
|
|
1059
|
+
let next = i + 1;
|
|
1060
|
+
while (next < input.length && /\s/.test(input[next])) next++;
|
|
1061
|
+
if (input[next] === "}" || input[next] === "]") continue;
|
|
1062
|
+
}
|
|
1063
|
+
output += char;
|
|
570
1064
|
}
|
|
1065
|
+
return output;
|
|
571
1066
|
}
|
|
572
1067
|
|
|
573
1068
|
// src/detectors/readers/filesystem.ts
|
|
574
1069
|
import { existsSync as existsSync2 } from "fs";
|
|
575
|
-
import { join as
|
|
1070
|
+
import { join as join6 } from "path";
|
|
576
1071
|
async function inferSrcDir(cwd) {
|
|
577
1072
|
const candidates = ["src", "app", "lib"];
|
|
578
1073
|
for (const dir of candidates) {
|
|
579
|
-
if (existsSync2(
|
|
580
|
-
return
|
|
1074
|
+
if (existsSync2(join6(cwd, dir))) {
|
|
1075
|
+
return join6(cwd, dir);
|
|
581
1076
|
}
|
|
582
1077
|
}
|
|
583
1078
|
return cwd;
|
|
@@ -600,9 +1095,9 @@ function detectRuntime(deps) {
|
|
|
600
1095
|
|
|
601
1096
|
// src/detectors/package-manager.ts
|
|
602
1097
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
603
|
-
import { join as
|
|
1098
|
+
import { join as join7 } from "path";
|
|
604
1099
|
function detectPackageManager(cwd) {
|
|
605
|
-
const pkgJsonPath =
|
|
1100
|
+
const pkgJsonPath = join7(cwd, "package.json");
|
|
606
1101
|
if (existsSync3(pkgJsonPath)) {
|
|
607
1102
|
try {
|
|
608
1103
|
const packageJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
@@ -615,52 +1110,76 @@ function detectPackageManager(cwd) {
|
|
|
615
1110
|
} catch {
|
|
616
1111
|
}
|
|
617
1112
|
}
|
|
618
|
-
if (existsSync3(
|
|
619
|
-
if (existsSync3(
|
|
620
|
-
if (existsSync3(
|
|
1113
|
+
if (existsSync3(join7(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1114
|
+
if (existsSync3(join7(cwd, "bun.lockb"))) return "bun";
|
|
1115
|
+
if (existsSync3(join7(cwd, "yarn.lock"))) return "yarn";
|
|
621
1116
|
return "npm";
|
|
622
1117
|
}
|
|
623
1118
|
|
|
1119
|
+
// src/detectors/alias.ts
|
|
1120
|
+
function flattenTsPaths(paths) {
|
|
1121
|
+
const result = {};
|
|
1122
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
1123
|
+
if (Array.isArray(targets) && typeof targets[0] === "string" && targets[0].length > 0) {
|
|
1124
|
+
result[alias.replace("/*", "/")] = targets[0].replace("/*", "/");
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return result;
|
|
1128
|
+
}
|
|
1129
|
+
function deriveBlocksAlias(tsPaths, blocksPhysicalPath) {
|
|
1130
|
+
const normalizedBlocksPath = blocksPhysicalPath.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
1131
|
+
for (const [aliasKey, targets] of Object.entries(tsPaths)) {
|
|
1132
|
+
if (!Array.isArray(targets) || typeof targets[0] !== "string") continue;
|
|
1133
|
+
const rawTarget = targets[0].replace(/\\/g, "/");
|
|
1134
|
+
const targetRoot = rawTarget.replace(/\*$/, "").replace(/\/$/, "").replace(/^\.\//, "");
|
|
1135
|
+
const aliasRoot = aliasKey.replace(/\/\*$/, "").replace(/\*$/, "");
|
|
1136
|
+
if (targetRoot === "") {
|
|
1137
|
+
return `${aliasRoot}/${normalizedBlocksPath}`.replace(/\/+/g, "/");
|
|
1138
|
+
}
|
|
1139
|
+
if (normalizedBlocksPath === targetRoot || normalizedBlocksPath.startsWith(`${targetRoot}/`)) {
|
|
1140
|
+
const suffix = normalizedBlocksPath.slice(targetRoot.length).replace(/^\//, "");
|
|
1141
|
+
const alias = suffix ? `${aliasRoot}/${suffix}` : aliasRoot;
|
|
1142
|
+
return alias.replace(/\/+/g, "/");
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return void 0;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
624
1148
|
// src/detectors/index.ts
|
|
625
|
-
async function detectProject(cwd) {
|
|
626
|
-
const [pkg,
|
|
1149
|
+
async function detectProject(cwd, options = {}) {
|
|
1150
|
+
const [pkg, compilerConfig] = await Promise.all([readPackageJson(cwd), readCompilerConfig(cwd)]);
|
|
627
1151
|
const allDeps = {
|
|
628
1152
|
...pkg.dependencies,
|
|
629
1153
|
...pkg.devDependencies,
|
|
630
1154
|
...pkg.peerDependencies
|
|
631
1155
|
};
|
|
632
1156
|
const srcDir = await inferSrcDir(cwd);
|
|
633
|
-
const
|
|
634
|
-
const
|
|
1157
|
+
const compilerOptions = compilerConfig?.config.compilerOptions;
|
|
1158
|
+
const moduleOpt = compilerOptions?.module?.toLocaleLowerCase();
|
|
1159
|
+
const resolutionOpt = compilerOptions?.moduleResolution?.toLocaleLowerCase();
|
|
635
1160
|
let importRewriteStrategy = "remove";
|
|
636
1161
|
if (moduleOpt === "nodenext" || moduleOpt === "node16" || resolutionOpt === "nodenext" || resolutionOpt === "node16" || pkg.type === "module" && !resolutionOpt) {
|
|
637
1162
|
importRewriteStrategy = "rewrite";
|
|
638
1163
|
}
|
|
1164
|
+
const blocksDir = options.blocksDir ? path6.resolve(cwd, options.blocksDir) : join8(srcDir, "lib", "blocks");
|
|
1165
|
+
const relativeBlocksDir = path6.relative(cwd, blocksDir).replace(/\\/g, "/");
|
|
1166
|
+
const tsPaths = compilerOptions?.paths;
|
|
639
1167
|
return {
|
|
640
1168
|
root: cwd,
|
|
641
|
-
language:
|
|
1169
|
+
language: compilerConfig?.kind === "tsconfig" ? "typescript" : "javascript",
|
|
642
1170
|
runtime: detectRuntime(allDeps),
|
|
643
1171
|
framework: detectFramework(allDeps),
|
|
644
1172
|
packageManager: detectPackageManager(cwd),
|
|
645
1173
|
hasRedis: "ioredis" in allDeps || "redis" in allDeps,
|
|
646
1174
|
hasPrisma: "@prisma/client" in allDeps,
|
|
647
1175
|
hasDrizzle: "drizzle-orm" in allDeps,
|
|
648
|
-
aliasMap:
|
|
1176
|
+
aliasMap: tsPaths ? flattenTsPaths(tsPaths) : {},
|
|
649
1177
|
srcDir,
|
|
650
|
-
|
|
651
|
-
|
|
1178
|
+
blocksDir,
|
|
1179
|
+
blocksAlias: tsPaths ? deriveBlocksAlias(tsPaths, relativeBlocksDir) : void 0,
|
|
652
1180
|
importRewriteStrategy
|
|
653
1181
|
};
|
|
654
1182
|
}
|
|
655
|
-
function flattenTsPaths(paths) {
|
|
656
|
-
const result = {};
|
|
657
|
-
for (const [alias, targets] of Object.entries(paths)) {
|
|
658
|
-
if (targets[0]) {
|
|
659
|
-
result[alias.replace("/*", "/")] = targets[0].replace("/*", "/");
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
return result;
|
|
663
|
-
}
|
|
664
1183
|
|
|
665
1184
|
// src/ui/theme.ts
|
|
666
1185
|
import pc2 from "picocolors";
|
|
@@ -699,41 +1218,6 @@ var format = {
|
|
|
699
1218
|
};
|
|
700
1219
|
|
|
701
1220
|
// src/commands/init.ts
|
|
702
|
-
async function resolveTsConfigPaths(cwd) {
|
|
703
|
-
const candidates = [join7(cwd, "tsconfig.json"), join7(cwd, "jsconfig.json")];
|
|
704
|
-
for (const configPath of candidates) {
|
|
705
|
-
if (!existsSync4(configPath)) continue;
|
|
706
|
-
try {
|
|
707
|
-
const raw = await fs2.readFile(configPath, "utf-8");
|
|
708
|
-
const clean = raw.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
|
|
709
|
-
const parsed = JSON.parse(clean);
|
|
710
|
-
return {
|
|
711
|
-
baseUrl: parsed.compilerOptions?.baseUrl ?? ".",
|
|
712
|
-
paths: parsed.compilerOptions?.paths ?? {}
|
|
713
|
-
};
|
|
714
|
-
} catch {
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
return { baseUrl: ".", paths: {} };
|
|
718
|
-
}
|
|
719
|
-
function deriveBlockAlias(tsPaths, blocksPhysicalPath) {
|
|
720
|
-
const normalizedBlocksPath = blocksPhysicalPath.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
721
|
-
for (const [aliasKey, targets] of Object.entries(tsPaths)) {
|
|
722
|
-
if (!Array.isArray(targets) || targets.length === 0) continue;
|
|
723
|
-
const rawTarget = targets[0].replace(/\\/g, "/");
|
|
724
|
-
const targetRoot = rawTarget.replace(/\*$/, "").replace(/\/$/, "").replace(/^\.\//, "");
|
|
725
|
-
const aliasRoot = aliasKey.replace(/\/\*$/, "").replace(/\*$/, "");
|
|
726
|
-
if (targetRoot === "") {
|
|
727
|
-
return `${aliasRoot}/${normalizedBlocksPath}`.replace(/\/+/g, "/");
|
|
728
|
-
}
|
|
729
|
-
if (normalizedBlocksPath === targetRoot || normalizedBlocksPath.startsWith(`${targetRoot}/`)) {
|
|
730
|
-
const suffix = normalizedBlocksPath.slice(targetRoot.length).replace(/^\//, "");
|
|
731
|
-
const alias = suffix ? `${aliasRoot}/${suffix}` : aliasRoot;
|
|
732
|
-
return alias.replace(/\/+/g, "/");
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
return void 0;
|
|
736
|
-
}
|
|
737
1221
|
function outputInitError(json, message) {
|
|
738
1222
|
if (json) {
|
|
739
1223
|
process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
|
|
@@ -752,7 +1236,7 @@ function outputInitResult(json, result) {
|
|
|
752
1236
|
async function initCommand(options = {}) {
|
|
753
1237
|
const { yes = false, json = false } = options;
|
|
754
1238
|
const cwd = process.cwd();
|
|
755
|
-
const configPath =
|
|
1239
|
+
const configPath = join9(cwd, "blockend.json");
|
|
756
1240
|
if (!json) {
|
|
757
1241
|
console.log("");
|
|
758
1242
|
intro2(`${pc3.bgCyan(pc3.black(" blockend "))} ${theme.text.muted("setup")}`);
|
|
@@ -784,15 +1268,35 @@ async function initCommand(options = {}) {
|
|
|
784
1268
|
action = actionPrompt;
|
|
785
1269
|
}
|
|
786
1270
|
if (action === "regenerate") {
|
|
787
|
-
await
|
|
1271
|
+
await fs6.unlink(configPath);
|
|
788
1272
|
if (!json) log2.warn("Existing configuration deleted.");
|
|
789
1273
|
}
|
|
790
1274
|
}
|
|
1275
|
+
const hasSrcDir = existsSync4(join9(cwd, "src"));
|
|
1276
|
+
const defaultDir = hasSrcDir ? "src/blocks" : "blocks";
|
|
1277
|
+
let rawPhysicalInput = defaultDir;
|
|
1278
|
+
if (!yes) {
|
|
1279
|
+
const directoryPrompt = await text({
|
|
1280
|
+
message: "Where would you like to install blocks?",
|
|
1281
|
+
placeholder: defaultDir,
|
|
1282
|
+
initialValue: defaultDir,
|
|
1283
|
+
validate(value) {
|
|
1284
|
+
if (!value || value.trim().length === 0) return "Directory path cannot be empty.";
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
if (isCancel2(directoryPrompt)) {
|
|
1288
|
+
if (json) {
|
|
1289
|
+
outputInitResult(json, { success: false, message: "Setup cancelled." });
|
|
1290
|
+
} else {
|
|
1291
|
+
outro2(format.muted("Setup cancelled."));
|
|
1292
|
+
}
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
rawPhysicalInput = String(directoryPrompt).trim();
|
|
1296
|
+
}
|
|
791
1297
|
const s = spinner2();
|
|
792
1298
|
if (!json) s.start("Scanning project architecture...");
|
|
793
|
-
const context = await detectProject(cwd);
|
|
794
|
-
const hasSrcDir = existsSync4(join7(cwd, "src"));
|
|
795
|
-
const tsConfig = await resolveTsConfigPaths(cwd);
|
|
1299
|
+
const context = await detectProject(cwd, { blocksDir: rawPhysicalInput });
|
|
796
1300
|
if (!json) s.stop("Project scanned.");
|
|
797
1301
|
let framework = context.framework;
|
|
798
1302
|
if (!framework) {
|
|
@@ -821,27 +1325,9 @@ async function initCommand(options = {}) {
|
|
|
821
1325
|
} else if (!json) {
|
|
822
1326
|
log2.info(`Framework detected: ${theme.state.info(framework)}`);
|
|
823
1327
|
}
|
|
824
|
-
const
|
|
825
|
-
let rawPhysicalInput = defaultDir;
|
|
826
|
-
if (!yes) {
|
|
827
|
-
const directoryPrompt = await text({
|
|
828
|
-
message: "Where would you like to install blocks?",
|
|
829
|
-
placeholder: defaultDir,
|
|
830
|
-
initialValue: defaultDir,
|
|
831
|
-
validate(value) {
|
|
832
|
-
if (!value || value.trim().length === 0) return "Directory path cannot be empty.";
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
if (isCancel2(directoryPrompt)) {
|
|
836
|
-
outro2(format.muted("Setup cancelled."));
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
rawPhysicalInput = String(directoryPrompt).trim();
|
|
840
|
-
}
|
|
841
|
-
const relativeBlocksPath = path2.relative(cwd, path2.resolve(cwd, rawPhysicalInput)).replace(/\\/g, "/");
|
|
1328
|
+
const relativeBlocksPath = path7.relative(cwd, context.blocksDir).replace(/\\/g, "/");
|
|
842
1329
|
const finalPath = relativeBlocksPath.startsWith(".") ? relativeBlocksPath : `./${relativeBlocksPath}`;
|
|
843
|
-
const
|
|
844
|
-
const finalBlockAlias = hasTsPaths ? deriveBlockAlias(tsConfig.paths, relativeBlocksPath) : void 0;
|
|
1330
|
+
const finalBlockAlias = context.blocksAlias;
|
|
845
1331
|
if (!json) {
|
|
846
1332
|
log2.info(
|
|
847
1333
|
`Import alias: ${theme.state.info(finalBlockAlias === void 0 ? " relative imports" : finalBlockAlias)}`
|
|
@@ -880,7 +1366,7 @@ async function initCommand(options = {}) {
|
|
|
880
1366
|
};
|
|
881
1367
|
if (!json) s.start("Writing configuration...");
|
|
882
1368
|
try {
|
|
883
|
-
await
|
|
1369
|
+
await fs6.writeFile(configPath, JSON.stringify(configPayload, null, 2), "utf-8");
|
|
884
1370
|
if (!json) s.stop("Configuration saved.");
|
|
885
1371
|
outputInitResult(json, {
|
|
886
1372
|
success: true,
|
|
@@ -938,33 +1424,13 @@ async function detectCommand(options = {}) {
|
|
|
938
1424
|
}
|
|
939
1425
|
|
|
940
1426
|
// src/commands/list.ts
|
|
941
|
-
import
|
|
942
|
-
import fs3 from "fs/promises";
|
|
1427
|
+
import fs7 from "fs/promises";
|
|
943
1428
|
import pc5 from "picocolors";
|
|
944
1429
|
import { outro as outro4, spinner as spinner3 } from "@clack/prompts";
|
|
945
|
-
var REPO_OWNER2 = "codewithnuh";
|
|
946
|
-
var REPO_NAME2 = "blockend";
|
|
947
|
-
var BRANCH2 = "master";
|
|
948
|
-
var MANIFEST_URL2 = `https://raw.githubusercontent.com/${REPO_OWNER2}/${REPO_NAME2}/${BRANCH2}/registry/index.json`;
|
|
949
|
-
async function findUp2(filename, startDir) {
|
|
950
|
-
let dir = startDir;
|
|
951
|
-
while (true) {
|
|
952
|
-
const checkPath = join8(dir, filename);
|
|
953
|
-
try {
|
|
954
|
-
await fs3.access(checkPath);
|
|
955
|
-
return checkPath;
|
|
956
|
-
} catch {
|
|
957
|
-
const parent = dirname2(dir);
|
|
958
|
-
if (parent === dir) break;
|
|
959
|
-
dir = parent;
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
return null;
|
|
963
|
-
}
|
|
964
1430
|
async function listCommand(options = {}) {
|
|
965
1431
|
const { json = false } = options;
|
|
966
1432
|
const cwd = process.cwd();
|
|
967
|
-
const configPath = await
|
|
1433
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
968
1434
|
if (!configPath) {
|
|
969
1435
|
if (json) {
|
|
970
1436
|
process.stdout.write(
|
|
@@ -977,7 +1443,7 @@ async function listCommand(options = {}) {
|
|
|
977
1443
|
}
|
|
978
1444
|
let config;
|
|
979
1445
|
try {
|
|
980
|
-
const configFile = await
|
|
1446
|
+
const configFile = await fs7.readFile(configPath, "utf-8");
|
|
981
1447
|
config = JSON.parse(configFile);
|
|
982
1448
|
} catch {
|
|
983
1449
|
if (json) {
|
|
@@ -990,15 +1456,14 @@ async function listCommand(options = {}) {
|
|
|
990
1456
|
return;
|
|
991
1457
|
}
|
|
992
1458
|
const envKey = String(config.environment);
|
|
1459
|
+
const source = createGitHubRegistrySource();
|
|
993
1460
|
const s = spinner3();
|
|
994
1461
|
if (!json) {
|
|
995
1462
|
s.start("Syncing registry metadata blocks...");
|
|
996
1463
|
}
|
|
997
1464
|
let registry;
|
|
998
1465
|
try {
|
|
999
|
-
|
|
1000
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1001
|
-
registry = await response.json();
|
|
1466
|
+
registry = await loadRegistry(source);
|
|
1002
1467
|
if (!json) s.stop(pc5.green("\u2714 Synced available block registries."));
|
|
1003
1468
|
} catch {
|
|
1004
1469
|
if (json) {
|
|
@@ -1010,25 +1475,12 @@ async function listCommand(options = {}) {
|
|
|
1010
1475
|
}
|
|
1011
1476
|
return;
|
|
1012
1477
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
}
|
|
1019
|
-
const blocksForEnv = Object.entries(blockMap).filter(([_, block]) => {
|
|
1020
|
-
if (!block) return false;
|
|
1021
|
-
const hasAdapter = block.adapters?.[envKey] !== void 0;
|
|
1022
|
-
const hasEnvironment = block.environments?.[envKey] !== void 0;
|
|
1023
|
-
return hasAdapter || hasEnvironment;
|
|
1024
|
-
}).map(([key, block]) => {
|
|
1025
|
-
const context = block.adapters?.[envKey] ?? block.environments?.[envKey];
|
|
1026
|
-
return {
|
|
1027
|
-
name: key,
|
|
1028
|
-
description: block.description,
|
|
1029
|
-
variants: Object.keys(context?.variants || {})
|
|
1030
|
-
};
|
|
1031
|
-
});
|
|
1478
|
+
const available = await listAvailableBlocks(registry, envKey);
|
|
1479
|
+
const blocksForEnv = available.map((block) => ({
|
|
1480
|
+
name: block.name,
|
|
1481
|
+
description: block.description,
|
|
1482
|
+
variants: block.variants
|
|
1483
|
+
}));
|
|
1032
1484
|
if (json) {
|
|
1033
1485
|
process.stdout.write(
|
|
1034
1486
|
JSON.stringify({ success: true, environment: envKey, blocks: blocksForEnv }) + "\n"
|
|
@@ -1039,11 +1491,13 @@ Available backend blocks for ${pc5.magenta(envKey)}:`);
|
|
|
1039
1491
|
if (blocksForEnv.length === 0) {
|
|
1040
1492
|
console.log(pc5.dim(" No blocks found for this framework."));
|
|
1041
1493
|
} else {
|
|
1042
|
-
blocksForEnv.forEach((
|
|
1494
|
+
blocksForEnv.forEach((block) => {
|
|
1043
1495
|
console.log(`
|
|
1044
|
-
${pc5.cyan(
|
|
1045
|
-
console.log(` ${pc5.dim(
|
|
1046
|
-
console.log(
|
|
1496
|
+
${pc5.cyan(block.name)}`);
|
|
1497
|
+
console.log(` ${pc5.dim(block.description)}`);
|
|
1498
|
+
console.log(
|
|
1499
|
+
` Storage Variants: ${block.variants.map((variant) => pc5.yellow(variant)).join(", ")}`
|
|
1500
|
+
);
|
|
1047
1501
|
});
|
|
1048
1502
|
console.log();
|
|
1049
1503
|
}
|
|
@@ -1051,8 +1505,8 @@ Available backend blocks for ${pc5.magenta(envKey)}:`);
|
|
|
1051
1505
|
}
|
|
1052
1506
|
|
|
1053
1507
|
// src/commands/mcp.ts
|
|
1054
|
-
import
|
|
1055
|
-
import
|
|
1508
|
+
import path8, { join as join10 } from "path";
|
|
1509
|
+
import fs8 from "fs/promises";
|
|
1056
1510
|
import pc6 from "picocolors";
|
|
1057
1511
|
import { outro as outro5, spinner as spinner4, select as select3, confirm as confirm3 } from "@clack/prompts";
|
|
1058
1512
|
var CLIENT_MATRIX = {
|
|
@@ -1098,7 +1552,7 @@ args = [${args}]
|
|
|
1098
1552
|
async function mcpStartCommand() {
|
|
1099
1553
|
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
1100
1554
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1101
|
-
const { z } = await import("zod");
|
|
1555
|
+
const { z: z2 } = await import("zod");
|
|
1102
1556
|
const { execa } = await import("execa");
|
|
1103
1557
|
const server = new McpServer({
|
|
1104
1558
|
name: "blockend",
|
|
@@ -1117,7 +1571,7 @@ async function mcpStartCommand() {
|
|
|
1117
1571
|
}
|
|
1118
1572
|
server.tool(
|
|
1119
1573
|
"list_blocks",
|
|
1120
|
-
"Lists all available Blockend blocks with names, descriptions, and
|
|
1574
|
+
"Lists all available Blockend blocks with names, descriptions, and variants",
|
|
1121
1575
|
{},
|
|
1122
1576
|
async () => {
|
|
1123
1577
|
const result = await safeExec("npx", ["--no-install", "blockend", "list", "--json"]);
|
|
@@ -1128,8 +1582,8 @@ async function mcpStartCommand() {
|
|
|
1128
1582
|
"add_block",
|
|
1129
1583
|
"Installs a Blockend block into a project.",
|
|
1130
1584
|
{
|
|
1131
|
-
blockName:
|
|
1132
|
-
projectPath:
|
|
1585
|
+
blockName: z2.string().describe("Block name, e.g. rate-limiter, error-handler, logger"),
|
|
1586
|
+
projectPath: z2.string().describe("Absolute path to the project root where blockend should install")
|
|
1133
1587
|
},
|
|
1134
1588
|
async ({ blockName, projectPath }) => {
|
|
1135
1589
|
const result = await safeExec(
|
|
@@ -1144,7 +1598,7 @@ async function mcpStartCommand() {
|
|
|
1144
1598
|
"detect_project",
|
|
1145
1599
|
"Analyzes a project context: framework, language, package manager, etc.",
|
|
1146
1600
|
{
|
|
1147
|
-
projectPath:
|
|
1601
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1148
1602
|
},
|
|
1149
1603
|
async ({ projectPath }) => {
|
|
1150
1604
|
const result = await safeExec("npx", ["--no-install", "blockend", "detect", "--json"], {
|
|
@@ -1157,8 +1611,8 @@ async function mcpStartCommand() {
|
|
|
1157
1611
|
"diff_block",
|
|
1158
1612
|
"Preview what files a block would generate without writing to disk. Shows new/modified/unchanged files.",
|
|
1159
1613
|
{
|
|
1160
|
-
blockName:
|
|
1161
|
-
projectPath:
|
|
1614
|
+
blockName: z2.string().describe("Block name to preview, e.g. rate-limiter, logger"),
|
|
1615
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1162
1616
|
},
|
|
1163
1617
|
async ({ blockName, projectPath }) => {
|
|
1164
1618
|
const result = await safeExec(
|
|
@@ -1173,7 +1627,7 @@ async function mcpStartCommand() {
|
|
|
1173
1627
|
"doctor",
|
|
1174
1628
|
"Check project health: validates blockend.json, detects framework, checks for issues.",
|
|
1175
1629
|
{
|
|
1176
|
-
projectPath:
|
|
1630
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1177
1631
|
},
|
|
1178
1632
|
async ({ projectPath }) => {
|
|
1179
1633
|
const result = await safeExec("npx", ["--no-install", "blockend", "doctor", "--json"], {
|
|
@@ -1186,7 +1640,7 @@ async function mcpStartCommand() {
|
|
|
1186
1640
|
"update_check",
|
|
1187
1641
|
"Check for available updates to installed blocks. Shows version comparison and diffs.",
|
|
1188
1642
|
{
|
|
1189
|
-
projectPath:
|
|
1643
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1190
1644
|
},
|
|
1191
1645
|
async ({ projectPath }) => {
|
|
1192
1646
|
const result = await safeExec("npx", ["--no-install", "blockend", "update", "--json"], {
|
|
@@ -1199,12 +1653,12 @@ async function mcpStartCommand() {
|
|
|
1199
1653
|
"update_apply",
|
|
1200
1654
|
"Apply updates to all installed blocks. Downloads new versions, rewrites imports, and updates blockend.json.",
|
|
1201
1655
|
{
|
|
1202
|
-
projectPath:
|
|
1656
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1203
1657
|
},
|
|
1204
1658
|
async ({ projectPath }) => {
|
|
1205
1659
|
const result = await safeExec(
|
|
1206
1660
|
"npx",
|
|
1207
|
-
["--no-install", "blockend", "update", "--apply", "--
|
|
1661
|
+
["--no-install", "blockend", "update", "--apply", "--json"],
|
|
1208
1662
|
{ cwd: projectPath }
|
|
1209
1663
|
);
|
|
1210
1664
|
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
@@ -1212,17 +1666,25 @@ async function mcpStartCommand() {
|
|
|
1212
1666
|
);
|
|
1213
1667
|
server.tool(
|
|
1214
1668
|
"add_blocks",
|
|
1215
|
-
"Install multiple blocks
|
|
1669
|
+
"Install multiple blocks into a project.",
|
|
1216
1670
|
{
|
|
1217
|
-
|
|
1671
|
+
blockNames: z2.array(z2.string()).min(1).describe("Block names, e.g. rate-limiter, logger"),
|
|
1672
|
+
projectPath: z2.string().describe("Absolute path to the project root where blockend should install")
|
|
1218
1673
|
},
|
|
1219
|
-
async ({ projectPath }) => {
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1674
|
+
async ({ blockNames, projectPath }) => {
|
|
1675
|
+
const results = [];
|
|
1676
|
+
for (const blockName of blockNames) {
|
|
1677
|
+
const result = await safeExec(
|
|
1678
|
+
"npx",
|
|
1679
|
+
["--no-install", "blockend", "add", blockName, "--yes", "--json"],
|
|
1680
|
+
{ cwd: projectPath }
|
|
1681
|
+
);
|
|
1682
|
+
results.push({ blockName, output: result.text, isError: result.isError });
|
|
1683
|
+
}
|
|
1684
|
+
return {
|
|
1685
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
1686
|
+
isError: results.some((result) => result.isError)
|
|
1687
|
+
};
|
|
1226
1688
|
}
|
|
1227
1689
|
);
|
|
1228
1690
|
const transport = new StdioServerTransport();
|
|
@@ -1251,7 +1713,7 @@ async function mcpInitCommand(options) {
|
|
|
1251
1713
|
outro5(pc6.red(`\u2716 Unsupported client identifier profile: ${options.client}`));
|
|
1252
1714
|
return;
|
|
1253
1715
|
}
|
|
1254
|
-
const absoluteConfigTarget =
|
|
1716
|
+
const absoluteConfigTarget = join10(cwd, meta.relativePath);
|
|
1255
1717
|
if (options.dryRun) {
|
|
1256
1718
|
console.log(
|
|
1257
1719
|
pc6.cyan(`
|
|
@@ -1268,7 +1730,7 @@ async function mcpInitCommand(options) {
|
|
|
1268
1730
|
}
|
|
1269
1731
|
let fileConflict = false;
|
|
1270
1732
|
try {
|
|
1271
|
-
await
|
|
1733
|
+
await fs8.access(absoluteConfigTarget);
|
|
1272
1734
|
fileConflict = true;
|
|
1273
1735
|
} catch {
|
|
1274
1736
|
}
|
|
@@ -1285,12 +1747,12 @@ async function mcpInitCommand(options) {
|
|
|
1285
1747
|
const s = spinner4();
|
|
1286
1748
|
s.start(`Writing localized project configurations inside ${pc6.dim(meta.relativePath)}...`);
|
|
1287
1749
|
try {
|
|
1288
|
-
await
|
|
1750
|
+
await fs8.mkdir(path8.dirname(absoluteConfigTarget), { recursive: true });
|
|
1289
1751
|
if (meta.format === "json") {
|
|
1290
1752
|
let workingConfig = {};
|
|
1291
1753
|
if (fileConflict) {
|
|
1292
1754
|
try {
|
|
1293
|
-
const rawContent = await
|
|
1755
|
+
const rawContent = await fs8.readFile(absoluteConfigTarget, "utf-8");
|
|
1294
1756
|
workingConfig = JSON.parse(rawContent);
|
|
1295
1757
|
} catch {
|
|
1296
1758
|
workingConfig = {};
|
|
@@ -1303,9 +1765,9 @@ async function mcpInitCommand(options) {
|
|
|
1303
1765
|
const existingServers = workingConfig.mcpServers || {};
|
|
1304
1766
|
workingConfig.mcpServers = { ...existingServers, blockend: BLOCKEND_SERVER };
|
|
1305
1767
|
}
|
|
1306
|
-
await
|
|
1768
|
+
await fs8.writeFile(absoluteConfigTarget, JSON.stringify(workingConfig, null, 2), "utf-8");
|
|
1307
1769
|
} else {
|
|
1308
|
-
await
|
|
1770
|
+
await fs8.writeFile(absoluteConfigTarget, generateToml(BLOCKEND_SERVER), "utf-8");
|
|
1309
1771
|
}
|
|
1310
1772
|
s.stop(
|
|
1311
1773
|
pc6.green(`\u2714 Local workspace integration fully complete! File target: ${meta.relativePath}`)
|
|
@@ -1317,47 +1779,10 @@ async function mcpInitCommand(options) {
|
|
|
1317
1779
|
}
|
|
1318
1780
|
|
|
1319
1781
|
// src/commands/diff.ts
|
|
1320
|
-
import
|
|
1321
|
-
import
|
|
1782
|
+
import path9 from "path";
|
|
1783
|
+
import fs9 from "fs/promises";
|
|
1322
1784
|
import { intro as intro3, outro as outro6, spinner as spinner5, log as log3 } from "@clack/prompts";
|
|
1323
1785
|
import pc7 from "picocolors";
|
|
1324
|
-
var REPO_OWNER3 = "codewithnuh";
|
|
1325
|
-
var REPO_NAME3 = "blockend";
|
|
1326
|
-
var BRANCH3 = "master";
|
|
1327
|
-
var RAW_CDN_BASE2 = `https://raw.githubusercontent.com/${REPO_OWNER3}/${REPO_NAME3}/${BRANCH3}`;
|
|
1328
|
-
var MANIFEST_URL3 = `${RAW_CDN_BASE2}/registry/index.json`;
|
|
1329
|
-
function blockSupportsEnv2(block, envKey) {
|
|
1330
|
-
if (block.frameworks && block.frameworks.length > 0) {
|
|
1331
|
-
return block.frameworks.includes("*") || block.frameworks.includes(envKey);
|
|
1332
|
-
}
|
|
1333
|
-
return block.adapters?.[envKey] !== void 0 || block.environments?.[envKey] !== void 0;
|
|
1334
|
-
}
|
|
1335
|
-
function resolveAdapterContext2(block, envKey) {
|
|
1336
|
-
return block.adapters?.[envKey] ?? block.adapters?.["*"] ?? block.environments?.[envKey] ?? block.environments?.["*"] ?? null;
|
|
1337
|
-
}
|
|
1338
|
-
async function findUp3(filename, startDir) {
|
|
1339
|
-
let dir = startDir;
|
|
1340
|
-
while (true) {
|
|
1341
|
-
const candidate = join10(dir, filename);
|
|
1342
|
-
try {
|
|
1343
|
-
await fs5.access(candidate);
|
|
1344
|
-
return candidate;
|
|
1345
|
-
} catch {
|
|
1346
|
-
const parent = dirname3(dir);
|
|
1347
|
-
if (parent === dir) break;
|
|
1348
|
-
dir = parent;
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
return null;
|
|
1352
|
-
}
|
|
1353
|
-
async function fileExists(filePath) {
|
|
1354
|
-
try {
|
|
1355
|
-
await fs5.access(filePath);
|
|
1356
|
-
return true;
|
|
1357
|
-
} catch {
|
|
1358
|
-
return false;
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
1786
|
async function diffCommand(blockName, options = {}) {
|
|
1362
1787
|
const { json = false } = options;
|
|
1363
1788
|
if (!json) {
|
|
@@ -1368,7 +1793,7 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1368
1793
|
return { block: "", files: [], error: "Block name is required. Usage: blockend diff <block>" };
|
|
1369
1794
|
}
|
|
1370
1795
|
const cwd = process.cwd();
|
|
1371
|
-
const configPath = await
|
|
1796
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
1372
1797
|
if (!configPath) {
|
|
1373
1798
|
const error = "blockend.json not found. Run 'npx blockend init' first.";
|
|
1374
1799
|
if (json) {
|
|
@@ -1378,10 +1803,10 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1378
1803
|
}
|
|
1379
1804
|
return { block: blockName, files: [], error };
|
|
1380
1805
|
}
|
|
1381
|
-
const rootDir =
|
|
1806
|
+
const rootDir = path9.dirname(configPath);
|
|
1382
1807
|
let config;
|
|
1383
1808
|
try {
|
|
1384
|
-
config = JSON.parse(await
|
|
1809
|
+
config = JSON.parse(await fs9.readFile(configPath, "utf-8"));
|
|
1385
1810
|
} catch {
|
|
1386
1811
|
const error = "Failed to parse blockend.json.";
|
|
1387
1812
|
if (json) {
|
|
@@ -1392,15 +1817,14 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1392
1817
|
return { block: blockName, files: [], error };
|
|
1393
1818
|
}
|
|
1394
1819
|
const physicalBlocksPath = config.paths.blocks;
|
|
1395
|
-
const blocksRootAbsolute =
|
|
1820
|
+
const blocksRootAbsolute = path9.resolve(rootDir, physicalBlocksPath);
|
|
1396
1821
|
const envKey = config.environment;
|
|
1822
|
+
const source = createGitHubRegistrySource();
|
|
1397
1823
|
const s = spinner5();
|
|
1398
1824
|
if (!json) s.start("Fetching registry...");
|
|
1399
1825
|
let registry;
|
|
1400
1826
|
try {
|
|
1401
|
-
|
|
1402
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1403
|
-
registry = await res.json();
|
|
1827
|
+
registry = await loadRegistry(source);
|
|
1404
1828
|
if (!json) s.stop("Registry synced.");
|
|
1405
1829
|
} catch (err) {
|
|
1406
1830
|
const error = `Failed to fetch registry: ${String(err)}`;
|
|
@@ -1410,97 +1834,32 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1410
1834
|
}
|
|
1411
1835
|
return { block: blockName, files: [], error };
|
|
1412
1836
|
}
|
|
1413
|
-
const
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
outro6(pc7.dim("Block not found."));
|
|
1422
|
-
}
|
|
1423
|
-
return { block: blockName, files: [], error };
|
|
1424
|
-
}
|
|
1425
|
-
if (!blockSupportsEnv2(blockMeta, envKey)) {
|
|
1426
|
-
const error = `Block "${blockName}" does not support environment: ${envKey}`;
|
|
1427
|
-
if (json) {
|
|
1428
|
-
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1429
|
-
} else {
|
|
1430
|
-
log3.error(error);
|
|
1431
|
-
}
|
|
1432
|
-
return { block: blockName, files: [], error };
|
|
1433
|
-
}
|
|
1434
|
-
const adapterContext = resolveAdapterContext2(blockMeta, envKey);
|
|
1435
|
-
if (!adapterContext) {
|
|
1436
|
-
const error = `Block "${blockName}" has no adapter for: ${envKey}`;
|
|
1437
|
-
if (json) {
|
|
1438
|
-
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1439
|
-
} else {
|
|
1440
|
-
log3.error(error);
|
|
1441
|
-
}
|
|
1442
|
-
return { block: blockName, files: [], error };
|
|
1443
|
-
}
|
|
1444
|
-
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
1445
|
-
if (variantKeys.length === 0) {
|
|
1446
|
-
const error = `No variants found for "${blockName}" / "${envKey}".`;
|
|
1837
|
+
const targetFolder = path9.resolve(blocksRootAbsolute, blockName);
|
|
1838
|
+
const installedRecord = config.installed?.find((block) => block.name === blockName);
|
|
1839
|
+
const planResult = await planBlock(registry, blockName, envKey, {
|
|
1840
|
+
recordedVariant: installedRecord?.variant,
|
|
1841
|
+
targetFolder
|
|
1842
|
+
});
|
|
1843
|
+
if (!planResult.ok) {
|
|
1844
|
+
const error = planResult.error.code === "no-adapter" ? `Block "${blockName}" has no adapter for: ${envKey}` : planResult.error.message;
|
|
1447
1845
|
if (json) {
|
|
1448
1846
|
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1449
1847
|
} else {
|
|
1450
1848
|
log3.error(error);
|
|
1849
|
+
if (planResult.error.code === "not-found") outro6(pc7.dim("Block not found."));
|
|
1451
1850
|
}
|
|
1452
1851
|
return { block: blockName, files: [], error };
|
|
1453
1852
|
}
|
|
1454
|
-
const
|
|
1455
|
-
const variantMeta = adapterContext.variants[selectedVariant];
|
|
1456
|
-
const targetFolder = path4.resolve(blocksRootAbsolute, blockName);
|
|
1457
|
-
const filesToDownload = [];
|
|
1458
|
-
if (blockMeta.baseFiles?.length) {
|
|
1459
|
-
filesToDownload.push(...blockMeta.baseFiles);
|
|
1460
|
-
}
|
|
1461
|
-
if (adapterContext.core) {
|
|
1462
|
-
filesToDownload.push({
|
|
1463
|
-
source: adapterContext.core,
|
|
1464
|
-
target: path4.basename(adapterContext.core)
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
1468
|
-
for (const entry of variantMeta.files) {
|
|
1469
|
-
filesToDownload.push(
|
|
1470
|
-
typeof entry === "string" ? { source: entry, target: path4.basename(entry) } : entry
|
|
1471
|
-
);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1853
|
+
const plan = planResult.plan;
|
|
1474
1854
|
if (!json) s.start(`Comparing ${blockName} files...`);
|
|
1475
1855
|
const diffFiles = [];
|
|
1476
1856
|
try {
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (!res.ok) throw new Error(`Download failed: ${fm.source} (HTTP ${res.status})`);
|
|
1480
|
-
const newContent = await res.text();
|
|
1481
|
-
const targetPath = path4.join(targetFolder, fm.target);
|
|
1482
|
-
let existingContent = null;
|
|
1483
|
-
let status = "new";
|
|
1484
|
-
if (await fileExists(targetPath)) {
|
|
1485
|
-
try {
|
|
1486
|
-
existingContent = await fs5.readFile(targetPath, "utf-8");
|
|
1487
|
-
status = existingContent === newContent ? "unchanged" : "modified";
|
|
1488
|
-
} catch {
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
diffFiles.push({
|
|
1492
|
-
name: fm.target,
|
|
1493
|
-
sourcePath: fm.source,
|
|
1494
|
-
targetPath,
|
|
1495
|
-
existingContent,
|
|
1496
|
-
newContent,
|
|
1497
|
-
status
|
|
1498
|
-
});
|
|
1499
|
-
}
|
|
1857
|
+
const previews = await previewBlock(plan, { source, targetFolder });
|
|
1858
|
+
diffFiles.push(...previews);
|
|
1500
1859
|
if (!json) s.stop("Comparison complete.");
|
|
1501
1860
|
if (!json) {
|
|
1502
1861
|
console.log(`
|
|
1503
|
-
${pc7.bold(pc7.cyan(blockName))} \u2014 ${pc7.dim(
|
|
1862
|
+
${pc7.bold(pc7.cyan(blockName))} \u2014 ${pc7.dim(plan.variant)} variant
|
|
1504
1863
|
`);
|
|
1505
1864
|
for (const file of diffFiles) {
|
|
1506
1865
|
const icon = file.status === "new" ? pc7.green("+") : file.status === "modified" ? pc7.yellow("~") : pc7.dim("=");
|
|
@@ -1528,11 +1887,13 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1528
1887
|
}
|
|
1529
1888
|
}
|
|
1530
1889
|
console.log("");
|
|
1531
|
-
const newCount = diffFiles.filter((
|
|
1532
|
-
const modCount = diffFiles.filter((
|
|
1533
|
-
const unchangedCount = diffFiles.filter((
|
|
1890
|
+
const newCount = diffFiles.filter((file) => file.status === "new").length;
|
|
1891
|
+
const modCount = diffFiles.filter((file) => file.status === "modified").length;
|
|
1892
|
+
const unchangedCount = diffFiles.filter((file) => file.status === "unchanged").length;
|
|
1534
1893
|
outro6(
|
|
1535
|
-
`${pc7.green(`${newCount} new`)} | ${pc7.yellow(`${modCount} modified`)} | ${pc7.dim(
|
|
1894
|
+
`${pc7.green(`${newCount} new`)} | ${pc7.yellow(`${modCount} modified`)} | ${pc7.dim(
|
|
1895
|
+
`${unchangedCount} unchanged`
|
|
1896
|
+
)}`
|
|
1536
1897
|
);
|
|
1537
1898
|
}
|
|
1538
1899
|
const result = { block: blockName, files: diffFiles };
|
|
@@ -1555,8 +1916,8 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1555
1916
|
}
|
|
1556
1917
|
|
|
1557
1918
|
// src/commands/doctor.ts
|
|
1558
|
-
import
|
|
1559
|
-
import
|
|
1919
|
+
import path10, { join as join11, dirname as dirname5 } from "path";
|
|
1920
|
+
import fs10 from "fs/promises";
|
|
1560
1921
|
import { existsSync as existsSync5 } from "fs";
|
|
1561
1922
|
import { intro as intro4, outro as outro7, spinner as spinner6 } from "@clack/prompts";
|
|
1562
1923
|
import pc8 from "picocolors";
|
|
@@ -1587,15 +1948,15 @@ function outputDoctorResult(json, result) {
|
|
|
1587
1948
|
}
|
|
1588
1949
|
}
|
|
1589
1950
|
}
|
|
1590
|
-
async function
|
|
1951
|
+
async function findUp2(filename, startDir) {
|
|
1591
1952
|
let dir = startDir;
|
|
1592
1953
|
while (true) {
|
|
1593
1954
|
const candidate = join11(dir, filename);
|
|
1594
1955
|
try {
|
|
1595
|
-
await
|
|
1956
|
+
await fs10.access(candidate);
|
|
1596
1957
|
return candidate;
|
|
1597
1958
|
} catch {
|
|
1598
|
-
const parent =
|
|
1959
|
+
const parent = dirname5(dir);
|
|
1599
1960
|
if (parent === dir) break;
|
|
1600
1961
|
dir = parent;
|
|
1601
1962
|
}
|
|
@@ -1619,7 +1980,7 @@ async function doctorCommand(options = {}) {
|
|
|
1619
1980
|
}
|
|
1620
1981
|
const s = spinner6();
|
|
1621
1982
|
if (!json) s.start("Scanning project health...");
|
|
1622
|
-
const packageJsonPath = await
|
|
1983
|
+
const packageJsonPath = await findUp2("package.json", cwd);
|
|
1623
1984
|
if (packageJsonPath) {
|
|
1624
1985
|
addCheck({ name: "package.json", status: "ok", message: "Found" });
|
|
1625
1986
|
} else {
|
|
@@ -1629,7 +1990,7 @@ async function doctorCommand(options = {}) {
|
|
|
1629
1990
|
message: "Not found. Run 'npm init' or 'pnpm init' first."
|
|
1630
1991
|
});
|
|
1631
1992
|
}
|
|
1632
|
-
const configPath = await
|
|
1993
|
+
const configPath = await findUp2("blockend.json", cwd);
|
|
1633
1994
|
if (configPath) {
|
|
1634
1995
|
addCheck({ name: "blockend.json", status: "ok", message: "Found" });
|
|
1635
1996
|
} else {
|
|
@@ -1639,7 +2000,7 @@ async function doctorCommand(options = {}) {
|
|
|
1639
2000
|
message: "Not found. Run 'npx blockend init' first."
|
|
1640
2001
|
});
|
|
1641
2002
|
}
|
|
1642
|
-
const tsConfigPath = await
|
|
2003
|
+
const tsConfigPath = await findUp2("tsconfig.json", cwd);
|
|
1643
2004
|
if (tsConfigPath) {
|
|
1644
2005
|
addCheck({ name: "tsconfig.json", status: "ok", message: "Found" });
|
|
1645
2006
|
} else {
|
|
@@ -1701,7 +2062,7 @@ async function doctorCommand(options = {}) {
|
|
|
1701
2062
|
}
|
|
1702
2063
|
if (configPath) {
|
|
1703
2064
|
try {
|
|
1704
|
-
const raw = await
|
|
2065
|
+
const raw = await fs10.readFile(configPath, "utf-8");
|
|
1705
2066
|
const config = JSON.parse(raw);
|
|
1706
2067
|
if (!config.environment) {
|
|
1707
2068
|
addCheck({
|
|
@@ -1723,7 +2084,7 @@ async function doctorCommand(options = {}) {
|
|
|
1723
2084
|
message: "Missing 'paths.blocks' in blockend.json"
|
|
1724
2085
|
});
|
|
1725
2086
|
} else {
|
|
1726
|
-
const blocksDir =
|
|
2087
|
+
const blocksDir = path10.resolve(dirname5(configPath), config.paths.blocks);
|
|
1727
2088
|
if (existsSync5(blocksDir)) {
|
|
1728
2089
|
addCheck({
|
|
1729
2090
|
name: "Config: blocks directory",
|
|
@@ -1756,39 +2117,10 @@ async function doctorCommand(options = {}) {
|
|
|
1756
2117
|
}
|
|
1757
2118
|
|
|
1758
2119
|
// src/commands/update.ts
|
|
1759
|
-
import
|
|
1760
|
-
import
|
|
2120
|
+
import path11 from "path";
|
|
2121
|
+
import fs11 from "fs/promises";
|
|
1761
2122
|
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";
|
|
1762
2123
|
import pc9 from "picocolors";
|
|
1763
|
-
var REPO_OWNER4 = "codewithnuh";
|
|
1764
|
-
var REPO_NAME4 = "blockend";
|
|
1765
|
-
var BRANCH4 = "master";
|
|
1766
|
-
var RAW_CDN_BASE3 = `https://raw.githubusercontent.com/${REPO_OWNER4}/${REPO_NAME4}/${BRANCH4}`;
|
|
1767
|
-
var MANIFEST_URL4 = `${RAW_CDN_BASE3}/registry/index.json`;
|
|
1768
|
-
function blockSupportsEnv3(block, envKey) {
|
|
1769
|
-
if (block.frameworks && block.frameworks.length > 0) {
|
|
1770
|
-
return block.frameworks.includes("*") || block.frameworks.includes(envKey);
|
|
1771
|
-
}
|
|
1772
|
-
return block.adapters?.[envKey] !== void 0 || block.environments?.[envKey] !== void 0;
|
|
1773
|
-
}
|
|
1774
|
-
function resolveAdapterContext3(block, envKey) {
|
|
1775
|
-
return block.adapters?.[envKey] ?? block.adapters?.["*"] ?? block.environments?.[envKey] ?? block.environments?.["*"] ?? null;
|
|
1776
|
-
}
|
|
1777
|
-
async function findUp5(filename, startDir) {
|
|
1778
|
-
let dir = startDir;
|
|
1779
|
-
while (true) {
|
|
1780
|
-
const candidate = join12(dir, filename);
|
|
1781
|
-
try {
|
|
1782
|
-
await fs7.access(candidate);
|
|
1783
|
-
return candidate;
|
|
1784
|
-
} catch {
|
|
1785
|
-
const parent = dirname5(dir);
|
|
1786
|
-
if (parent === dir) break;
|
|
1787
|
-
dir = parent;
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
return null;
|
|
1791
|
-
}
|
|
1792
2124
|
function computeLineDiff(oldContent, newContent) {
|
|
1793
2125
|
const oldLines = oldContent.split("\n");
|
|
1794
2126
|
const newLines = newContent.split("\n");
|
|
@@ -1880,7 +2212,9 @@ function outputUpdateResult(json, result) {
|
|
|
1880
2212
|
`);
|
|
1881
2213
|
for (const block of updates) {
|
|
1882
2214
|
console.log(
|
|
1883
|
-
` ${pc9.yellow("~")} ${pc9.bold(block.name)} ${pc9.dim(
|
|
2215
|
+
` ${pc9.yellow("~")} ${pc9.bold(block.name)} ${pc9.dim(
|
|
2216
|
+
`${block.localVersion} \u2192 ${block.remoteVersion}`
|
|
2217
|
+
)}`
|
|
1884
2218
|
);
|
|
1885
2219
|
}
|
|
1886
2220
|
console.log("");
|
|
@@ -1909,27 +2243,6 @@ function outputUpdateResult(json, result) {
|
|
|
1909
2243
|
outro8(pc9.green("All tracked blocks are up to date."));
|
|
1910
2244
|
}
|
|
1911
2245
|
}
|
|
1912
|
-
function resolveBlockFiles(blockMeta, envKey) {
|
|
1913
|
-
const adapterContext = resolveAdapterContext3(blockMeta, envKey);
|
|
1914
|
-
if (!adapterContext) return null;
|
|
1915
|
-
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
1916
|
-
if (variantKeys.length === 0) return null;
|
|
1917
|
-
const selectedVariant = variantKeys[0];
|
|
1918
|
-
const variantMeta = adapterContext.variants[selectedVariant];
|
|
1919
|
-
const expectedFiles = [];
|
|
1920
|
-
if (blockMeta.baseFiles?.length) expectedFiles.push(...blockMeta.baseFiles);
|
|
1921
|
-
if (adapterContext.core) {
|
|
1922
|
-
expectedFiles.push({ source: adapterContext.core, target: path6.basename(adapterContext.core) });
|
|
1923
|
-
}
|
|
1924
|
-
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
1925
|
-
for (const entry of variantMeta.files) {
|
|
1926
|
-
expectedFiles.push(
|
|
1927
|
-
typeof entry === "string" ? { source: entry, target: path6.basename(entry) } : entry
|
|
1928
|
-
);
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
return { files: expectedFiles, variant: selectedVariant };
|
|
1932
|
-
}
|
|
1933
2246
|
async function updateCommand(options = {}) {
|
|
1934
2247
|
const { json = false, diff: diff2 = true, apply = false } = options;
|
|
1935
2248
|
if (!json) {
|
|
@@ -1937,17 +2250,17 @@ async function updateCommand(options = {}) {
|
|
|
1937
2250
|
intro5(`${pc9.bgCyan(pc9.black(" blockend "))} ${pc9.dim("update")}`);
|
|
1938
2251
|
}
|
|
1939
2252
|
const cwd = process.cwd();
|
|
1940
|
-
const configPath = await
|
|
2253
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
1941
2254
|
if (!configPath) {
|
|
1942
2255
|
const error = "blockend.json not found. Run 'npx blockend init' first.";
|
|
1943
2256
|
if (json) process.stdout.write(JSON.stringify({ blocks: [], error }) + "\n");
|
|
1944
2257
|
else log4.error(error);
|
|
1945
2258
|
return { blocks: [], error };
|
|
1946
2259
|
}
|
|
1947
|
-
const rootDir =
|
|
2260
|
+
const rootDir = path11.dirname(configPath);
|
|
1948
2261
|
let config;
|
|
1949
2262
|
try {
|
|
1950
|
-
config = JSON.parse(await
|
|
2263
|
+
config = JSON.parse(await fs11.readFile(configPath, "utf-8"));
|
|
1951
2264
|
} catch {
|
|
1952
2265
|
const error = "Failed to parse blockend.json.";
|
|
1953
2266
|
if (json) process.stdout.write(JSON.stringify({ blocks: [], error }) + "\n");
|
|
@@ -1955,18 +2268,17 @@ async function updateCommand(options = {}) {
|
|
|
1955
2268
|
return { blocks: [], error };
|
|
1956
2269
|
}
|
|
1957
2270
|
const physicalBlocksPath = config.paths.blocks;
|
|
1958
|
-
const blocksRootAbsolute =
|
|
2271
|
+
const blocksRootAbsolute = path11.resolve(rootDir, physicalBlocksPath);
|
|
1959
2272
|
const envKey = config.environment;
|
|
1960
2273
|
const aliasBase = config.aliases.blocks;
|
|
1961
2274
|
const rewriteStrategy = config.importRewriteStrategy ?? "remove";
|
|
1962
2275
|
const installedRecords = Array.isArray(config.installed) ? config.installed : [];
|
|
2276
|
+
const source = createGitHubRegistrySource();
|
|
1963
2277
|
const s = spinner7();
|
|
1964
2278
|
if (!json) s.start("Fetching registry...");
|
|
1965
2279
|
let registry;
|
|
1966
2280
|
try {
|
|
1967
|
-
|
|
1968
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1969
|
-
registry = await res.json();
|
|
2281
|
+
registry = await loadRegistry(source);
|
|
1970
2282
|
if (!json) s.stop("Registry synced.");
|
|
1971
2283
|
} catch (err) {
|
|
1972
2284
|
const error = `Failed to fetch registry: ${String(err)}`;
|
|
@@ -1976,14 +2288,17 @@ async function updateCommand(options = {}) {
|
|
|
1976
2288
|
}
|
|
1977
2289
|
return { blocks: [], error };
|
|
1978
2290
|
}
|
|
1979
|
-
const blockMap = registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks) ? registry.blocks : registry;
|
|
1980
2291
|
if (!json) s.start("Comparing versions...");
|
|
1981
2292
|
const blockResults = [];
|
|
1982
2293
|
const diffs = {};
|
|
2294
|
+
const plans = {};
|
|
1983
2295
|
for (const record of installedRecords) {
|
|
1984
|
-
const
|
|
1985
|
-
const
|
|
1986
|
-
|
|
2296
|
+
const targetFolder = path11.resolve(blocksRootAbsolute, record.name);
|
|
2297
|
+
const planResult = await planBlock(registry, record.name, envKey, {
|
|
2298
|
+
recordedVariant: record.variant,
|
|
2299
|
+
targetFolder
|
|
2300
|
+
});
|
|
2301
|
+
if (!planResult.ok) {
|
|
1987
2302
|
blockResults.push({
|
|
1988
2303
|
name: record.name,
|
|
1989
2304
|
localVersion: record.version,
|
|
@@ -1992,6 +2307,9 @@ async function updateCommand(options = {}) {
|
|
|
1992
2307
|
});
|
|
1993
2308
|
continue;
|
|
1994
2309
|
}
|
|
2310
|
+
const plan = planResult.plan;
|
|
2311
|
+
plans[record.name] = plan;
|
|
2312
|
+
const remoteVersion = plan.block.version ?? "0.0.0";
|
|
1995
2313
|
const hasUpdate = record.version !== remoteVersion || record.contentHash === "0";
|
|
1996
2314
|
blockResults.push({
|
|
1997
2315
|
name: record.name,
|
|
@@ -2000,55 +2318,39 @@ async function updateCommand(options = {}) {
|
|
|
2000
2318
|
status: hasUpdate ? "update-available" : "up-to-date"
|
|
2001
2319
|
});
|
|
2002
2320
|
if (hasUpdate && diff2) {
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
localExists = true;
|
|
2014
|
-
} catch {
|
|
2015
|
-
localContent = "";
|
|
2016
|
-
}
|
|
2017
|
-
let remoteContent = "";
|
|
2018
|
-
try {
|
|
2019
|
-
const res = await fetch(`${RAW_CDN_BASE3}/${fm.source}`);
|
|
2020
|
-
if (res.ok) remoteContent = await res.text();
|
|
2021
|
-
} catch {
|
|
2022
|
-
remoteContent = "";
|
|
2023
|
-
}
|
|
2024
|
-
if (!localExists) {
|
|
2025
|
-
fileDiffs.push({
|
|
2026
|
-
name: fm.target,
|
|
2027
|
-
file: fm.target,
|
|
2321
|
+
const previews = await previewBlock(plan, {
|
|
2322
|
+
source,
|
|
2323
|
+
targetFolder,
|
|
2324
|
+
onDownloadError: "empty"
|
|
2325
|
+
});
|
|
2326
|
+
diffs[record.name] = previews.map((preview) => {
|
|
2327
|
+
if (preview.existingContent === null) {
|
|
2328
|
+
return {
|
|
2329
|
+
name: preview.name,
|
|
2330
|
+
file: preview.name,
|
|
2028
2331
|
status: "added",
|
|
2029
|
-
lines:
|
|
2332
|
+
lines: preview.newContent.split("\n").map((line, index) => ({
|
|
2030
2333
|
type: "added",
|
|
2031
|
-
content:
|
|
2032
|
-
newLineNo:
|
|
2334
|
+
content: line,
|
|
2335
|
+
newLineNo: index + 1
|
|
2033
2336
|
}))
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
if (preview.existingContent !== preview.newContent) {
|
|
2340
|
+
return {
|
|
2341
|
+
name: preview.name,
|
|
2342
|
+
file: preview.name,
|
|
2039
2343
|
status: "modified",
|
|
2040
|
-
lines: computeLineDiff(
|
|
2041
|
-
}
|
|
2042
|
-
} else {
|
|
2043
|
-
fileDiffs.push({
|
|
2044
|
-
name: fm.target,
|
|
2045
|
-
file: fm.target,
|
|
2046
|
-
status: "unchanged",
|
|
2047
|
-
lines: []
|
|
2048
|
-
});
|
|
2344
|
+
lines: computeLineDiff(preview.existingContent, preview.newContent)
|
|
2345
|
+
};
|
|
2049
2346
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2347
|
+
return {
|
|
2348
|
+
name: preview.name,
|
|
2349
|
+
file: preview.name,
|
|
2350
|
+
status: "unchanged",
|
|
2351
|
+
lines: []
|
|
2352
|
+
};
|
|
2353
|
+
});
|
|
2052
2354
|
}
|
|
2053
2355
|
}
|
|
2054
2356
|
if (!json) s.stop("Comparison complete.");
|
|
@@ -2085,58 +2387,19 @@ async function updateCommand(options = {}) {
|
|
|
2085
2387
|
}
|
|
2086
2388
|
const applied = [];
|
|
2087
2389
|
for (const blockName of selectedNames) {
|
|
2088
|
-
const
|
|
2089
|
-
if (!
|
|
2090
|
-
const resolved = resolveBlockFiles(blockMeta, envKey);
|
|
2091
|
-
if (!resolved) continue;
|
|
2092
|
-
const targetFolder = path6.resolve(blocksRootAbsolute, blockName);
|
|
2093
|
-
const downloadedTargets = /* @__PURE__ */ new Set();
|
|
2390
|
+
const plan = plans[blockName];
|
|
2391
|
+
if (!plan) continue;
|
|
2094
2392
|
try {
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
}
|
|
2102
|
-
let contentHash = 0;
|
|
2103
|
-
for (const { fm, raw } of fileBatches) {
|
|
2104
|
-
const writtenFilePath = path6.join(targetFolder, fm.target);
|
|
2105
|
-
const content = rewriteFileImports(
|
|
2106
|
-
raw,
|
|
2107
|
-
writtenFilePath,
|
|
2108
|
-
blocksRootAbsolute,
|
|
2109
|
-
aliasBase,
|
|
2110
|
-
rewriteStrategy,
|
|
2111
|
-
downloadedTargets
|
|
2112
|
-
);
|
|
2113
|
-
await fs7.mkdir(dirname5(writtenFilePath), { recursive: true });
|
|
2114
|
-
await fs7.writeFile(writtenFilePath, content, "utf-8");
|
|
2115
|
-
for (let i = 0; i < content.length; i++) {
|
|
2116
|
-
contentHash = (contentHash << 5) - contentHash + content.charCodeAt(i) | 0;
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
const blockVersion = blockMeta.version ?? "0.0.0";
|
|
2120
|
-
const installedRecord = {
|
|
2121
|
-
name: blockName,
|
|
2122
|
-
version: blockVersion,
|
|
2123
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2124
|
-
files: resolved.files.map((f) => f.target),
|
|
2125
|
-
contentHash: contentHash.toString(16)
|
|
2126
|
-
};
|
|
2127
|
-
try {
|
|
2128
|
-
const cfgRaw = await fs7.readFile(configPath, "utf-8");
|
|
2129
|
-
const cfgData = JSON.parse(cfgRaw);
|
|
2130
|
-
if (!Array.isArray(cfgData.installed)) cfgData.installed = [];
|
|
2131
|
-
cfgData.installed = cfgData.installed.filter(
|
|
2132
|
-
(b) => b.name !== blockName
|
|
2133
|
-
);
|
|
2134
|
-
cfgData.installed.push(installedRecord);
|
|
2135
|
-
await fs7.writeFile(configPath, JSON.stringify(cfgData, null, 2), "utf-8");
|
|
2136
|
-
} catch {
|
|
2137
|
-
}
|
|
2393
|
+
await applyBlock(plan, {
|
|
2394
|
+
source,
|
|
2395
|
+
blocksRoot: blocksRootAbsolute,
|
|
2396
|
+
aliasBase,
|
|
2397
|
+
rewriteStrategy,
|
|
2398
|
+
configPath
|
|
2399
|
+
});
|
|
2138
2400
|
applied.push(blockName);
|
|
2139
|
-
if (!json)
|
|
2401
|
+
if (!json)
|
|
2402
|
+
log4.info(` ${pc9.green("\u2714")} ${blockName} updated to v${plan.block.version ?? "0.0.0"}`);
|
|
2140
2403
|
} catch (err) {
|
|
2141
2404
|
if (!json) log4.error(` ${pc9.red("\u2716")} ${blockName}: ${String(err)}`);
|
|
2142
2405
|
}
|