blockend-cli 2.1.0 → 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 -41
- package/dist/commands/init.d.ts +0 -23
- 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 +933 -711
- package/dist/types/types.d.ts +1 -0
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -4,22 +4,232 @@
|
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
5
|
|
|
6
6
|
// src/commands/add.ts
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import path5, { dirname as dirname4, join as join3 } from "path";
|
|
8
|
+
import fs5 from "fs/promises";
|
|
9
9
|
import { exec } from "child_process";
|
|
10
10
|
import { intro, outro, select, multiselect, spinner, confirm, isCancel, log } from "@clack/prompts";
|
|
11
11
|
import pc from "picocolors";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
var
|
|
16
|
-
var
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
|
|
13
|
+
// ../registry/dist/index.js
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
var FRAMEWORK_KEYS = ["express", "fastify", "hono", "next", "*"];
|
|
16
|
+
var FRAMEWORK_NAMES = ["express", "fastify", "hono", "next"];
|
|
17
|
+
var BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
18
|
+
var DEPENDENCY_PATTERN = /^[a-z@][a-z0-9._/-]*(@[~^>=<]*[0-9]+(\.[0-9]+)*)?$/;
|
|
19
|
+
var RELEASED_AT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
20
|
+
var FrameworkKeySchema = z.enum(FRAMEWORK_KEYS);
|
|
21
|
+
var FrameworkNameSchema = z.enum(FRAMEWORK_NAMES);
|
|
22
|
+
var nonEmptyString = z.string().min(1, "must not be empty");
|
|
23
|
+
function isValidCalendarDate(value) {
|
|
24
|
+
if (!RELEASED_AT_PATTERN.test(value)) return false;
|
|
25
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
26
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
27
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
28
|
+
}
|
|
29
|
+
var AssetMappingSchema = z.object({
|
|
30
|
+
source: z.string().min(1, "source path must not be empty").describe("Repo-relative source path."),
|
|
31
|
+
target: z.string().min(1, "target path must not be empty").describe("Block-folder-relative destination path.")
|
|
32
|
+
}).strict();
|
|
33
|
+
var AssetReferenceSchema = z.union([z.string().min(1), AssetMappingSchema]);
|
|
34
|
+
var VariantSchema = z.object({
|
|
35
|
+
dependencies: z.array(z.string()).describe("Variant-specific production dependencies.").optional(),
|
|
36
|
+
devDependencies: z.array(z.string()).describe("Variant-specific development dependencies.").optional(),
|
|
37
|
+
files: z.array(AssetReferenceSchema).describe("Files copied for this variant.")
|
|
38
|
+
}).strict();
|
|
39
|
+
var FrameworkMappingSchema = z.object({
|
|
40
|
+
core: z.string().describe("Optional fallback template.").optional(),
|
|
41
|
+
dependencies: z.array(z.string()).describe("Dependencies for this framework mapping.").optional(),
|
|
42
|
+
devDependencies: z.array(z.string()).describe("Development dependencies for this framework mapping.").optional(),
|
|
43
|
+
variants: z.record(z.string(), VariantSchema).describe("Storage or implementation variants.")
|
|
44
|
+
}).strict();
|
|
45
|
+
function blockSlugArray(label) {
|
|
46
|
+
return z.array(
|
|
47
|
+
z.string().regex(BLOCK_SLUG_PATTERN, `${label} entries must be valid registry slugs`).min(1, `${label} entries must not be empty`)
|
|
48
|
+
).meta({ uniqueItems: true }).optional();
|
|
49
|
+
}
|
|
50
|
+
var BlockSchema = z.object({
|
|
51
|
+
name: z.string().min(1, "block name must not be empty").describe("Human-readable block name."),
|
|
52
|
+
description: z.string().min(1, "block description must not be empty").describe("Short description of the block."),
|
|
53
|
+
version: z.string().describe('Semantic version of this block (e.g. "1.0.0"). Used for update tracking.').optional(),
|
|
54
|
+
frameworks: z.array(FrameworkKeySchema).meta({ uniqueItems: true }).refine(
|
|
55
|
+
(frameworks) => new Set(frameworks).size === frameworks.length,
|
|
56
|
+
"frameworks must not contain duplicates"
|
|
57
|
+
).describe(
|
|
58
|
+
'Explicit framework support list. Named keys mean the block only supports that environment; "*" means framework-agnostic.'
|
|
59
|
+
).optional(),
|
|
60
|
+
releasedAt: z.string().regex(RELEASED_AT_PATTERN, "releasedAt must be a valid ISO date (YYYY-MM-DD)").refine(isValidCalendarDate, "releasedAt must be a valid ISO date (YYYY-MM-DD)").describe("ISO calendar date (YYYY-MM-DD) when this block was first released.").optional(),
|
|
61
|
+
runtimes: z.record(z.enum(["node"]), z.string().min(1, "runtime version range must not be empty")).describe('Supported runtime engine ranges, e.g. {"node": ">=20"}.').optional(),
|
|
62
|
+
requires: blockSlugArray("requires").describe(
|
|
63
|
+
"Registry slugs that must be installed alongside this block."
|
|
64
|
+
),
|
|
65
|
+
related: blockSlugArray("related").describe("Registry slugs that pair well with this block."),
|
|
66
|
+
conflicts: blockSlugArray("conflicts").describe(
|
|
67
|
+
"Registry slugs that must not be installed together with this block."
|
|
68
|
+
),
|
|
69
|
+
dependencies: z.array(z.string().regex(DEPENDENCY_PATTERN, "invalid dependency format")).describe("Production dependencies, optionally with version ranges.").optional(),
|
|
70
|
+
devDependencies: z.array(z.string().regex(DEPENDENCY_PATTERN, "invalid devDependency format")).describe("Development dependencies, optionally with version ranges.").optional(),
|
|
71
|
+
baseFiles: z.array(AssetMappingSchema).describe("Files copied for every install of this block.").optional(),
|
|
72
|
+
adapters: z.partialRecord(FrameworkKeySchema, FrameworkMappingSchema).describe("Framework adapter mappings.").optional(),
|
|
73
|
+
environments: z.partialRecord(FrameworkKeySchema, FrameworkMappingSchema).describe("Legacy framework mappings.").optional()
|
|
74
|
+
}).strict().refine(
|
|
75
|
+
(block) => block.baseFiles || block.adapters || block.environments,
|
|
76
|
+
"block must define at least one of: baseFiles, adapters, or environments"
|
|
77
|
+
);
|
|
78
|
+
var BlockDeclarationSchema = BlockSchema.safeExtend({
|
|
79
|
+
$schema: z.string().optional().describe("JSON Schema reference for editor support.")
|
|
80
|
+
});
|
|
81
|
+
var RegistrySchema = z.object({
|
|
82
|
+
$schema: z.string().optional(),
|
|
83
|
+
version: z.string().min(1, "registry version must not be empty").describe("Semantic version of the registry schema layout."),
|
|
84
|
+
blocks: z.record(z.string().min(1, "block key must not be empty"), BlockSchema).describe("Dictionary lookup table of all available ingestible component blocks.")
|
|
85
|
+
}).strict();
|
|
86
|
+
var blockSlug = z.string().regex(BLOCK_SLUG_PATTERN, "must be a valid registry block slug");
|
|
87
|
+
var uniqueBlockSlugs = z.array(blockSlug).meta({ uniqueItems: true }).refine(
|
|
88
|
+
(slugs) => new Set(slugs).size === slugs.length,
|
|
89
|
+
"must not contain duplicate block slugs"
|
|
90
|
+
);
|
|
91
|
+
var requiredBlockSlugs = z.array(blockSlug).min(1).meta({ uniqueItems: true }).refine(
|
|
92
|
+
(slugs) => new Set(slugs).size === slugs.length,
|
|
93
|
+
"must not contain duplicate block slugs"
|
|
94
|
+
);
|
|
95
|
+
var RecipeSchema = z.object({
|
|
96
|
+
slug: blockSlug.describe("Stable URL-safe recipe identifier."),
|
|
97
|
+
title: nonEmptyString,
|
|
98
|
+
outcome: nonEmptyString.describe("What a developer will have after following the recipe."),
|
|
99
|
+
includedBlocks: requiredBlockSlugs,
|
|
100
|
+
optionalBlocks: uniqueBlockSlugs,
|
|
101
|
+
integrationOrder: uniqueBlockSlugs.describe(
|
|
102
|
+
"A complete ordering of included and optional blocks, respecting registry requirements."
|
|
103
|
+
),
|
|
104
|
+
architecture: z.object({
|
|
105
|
+
overview: nonEmptyString,
|
|
106
|
+
components: z.array(
|
|
107
|
+
z.object({
|
|
108
|
+
name: nonEmptyString,
|
|
109
|
+
description: nonEmptyString,
|
|
110
|
+
block: blockSlug.optional()
|
|
111
|
+
}).strict()
|
|
112
|
+
)
|
|
113
|
+
}).strict(),
|
|
114
|
+
assumptions: z.array(nonEmptyString),
|
|
115
|
+
configuration: z.array(
|
|
116
|
+
z.object({
|
|
117
|
+
name: nonEmptyString,
|
|
118
|
+
description: nonEmptyString,
|
|
119
|
+
required: z.boolean(),
|
|
120
|
+
default: z.string().optional()
|
|
121
|
+
}).strict()
|
|
122
|
+
),
|
|
123
|
+
tradeoffs: z.array(
|
|
124
|
+
z.object({
|
|
125
|
+
choice: nonEmptyString,
|
|
126
|
+
tradeoff: nonEmptyString
|
|
127
|
+
}).strict()
|
|
128
|
+
),
|
|
129
|
+
alternatives: z.array(
|
|
130
|
+
z.object({
|
|
131
|
+
title: nonEmptyString,
|
|
132
|
+
description: nonEmptyString
|
|
133
|
+
}).strict()
|
|
134
|
+
).optional(),
|
|
135
|
+
failureBehavior: z.array(
|
|
136
|
+
z.object({
|
|
137
|
+
scenario: nonEmptyString,
|
|
138
|
+
behavior: nonEmptyString
|
|
139
|
+
}).strict()
|
|
140
|
+
),
|
|
141
|
+
commands: z.array(
|
|
142
|
+
z.object({
|
|
143
|
+
label: nonEmptyString,
|
|
144
|
+
command: nonEmptyString
|
|
145
|
+
}).strict()
|
|
146
|
+
),
|
|
147
|
+
relatedContent: z.array(
|
|
148
|
+
z.object({
|
|
149
|
+
title: nonEmptyString,
|
|
150
|
+
href: z.string().regex(/^(https:\/\/|\/)/, "must be an HTTPS URL or site-relative path")
|
|
151
|
+
}).strict()
|
|
152
|
+
)
|
|
153
|
+
}).strict();
|
|
154
|
+
var RecipeCollectionSchema = z.object({
|
|
155
|
+
$schema: z.string().optional(),
|
|
156
|
+
version: nonEmptyString.describe("Version of the recipe collection format."),
|
|
157
|
+
recipes: z.array(RecipeSchema).describe("Production-ready backend recipes published by Blockend.")
|
|
158
|
+
}).strict();
|
|
159
|
+
var RegistryParseError = class extends Error {
|
|
160
|
+
constructor(message, issues) {
|
|
161
|
+
super(message);
|
|
162
|
+
this.issues = issues;
|
|
163
|
+
this.name = "RegistryParseError";
|
|
164
|
+
}
|
|
165
|
+
issues;
|
|
166
|
+
};
|
|
167
|
+
function formatIssues(issues) {
|
|
168
|
+
return issues.map((issue) => {
|
|
169
|
+
const path22 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
170
|
+
return `${path22}${issue.message}`;
|
|
171
|
+
}).join("\n");
|
|
172
|
+
}
|
|
173
|
+
function isRecord(value) {
|
|
174
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
175
|
+
}
|
|
176
|
+
function isLegacyRegistry(input) {
|
|
177
|
+
if (!isRecord(input)) return false;
|
|
178
|
+
const blocks = input.blocks;
|
|
179
|
+
if (!isRecord(blocks)) {
|
|
180
|
+
return typeof input.version !== "string";
|
|
20
181
|
}
|
|
21
|
-
return
|
|
182
|
+
return "name" in blocks || "description" in blocks;
|
|
22
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";
|
|
23
233
|
function selectVariant(variantKeys, preferredVariant) {
|
|
24
234
|
if (preferredVariant && variantKeys.includes(preferredVariant)) {
|
|
25
235
|
return preferredVariant;
|
|
@@ -48,6 +258,283 @@ async function resolvePreferredVariant(variantKeys, recordedVariant, targetFolde
|
|
|
48
258
|
}
|
|
49
259
|
return void 0;
|
|
50
260
|
}
|
|
261
|
+
|
|
262
|
+
// src/block-install/plan.ts
|
|
263
|
+
function blockSupportsEnv(block, environment) {
|
|
264
|
+
if (block.frameworks && block.frameworks.length > 0) {
|
|
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
|
+
});
|
|
407
|
+
}
|
|
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
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/block-install/deps.ts
|
|
51
538
|
function parseDepSpec(dep) {
|
|
52
539
|
const atIndex = dep.lastIndexOf("@");
|
|
53
540
|
if (atIndex > 0) {
|
|
@@ -105,49 +592,8 @@ var getMissingDependencies = (deps, installed, forceLatest = false) => {
|
|
|
105
592
|
}
|
|
106
593
|
return result;
|
|
107
594
|
};
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
function resolveIndexImport(importPath, sourceFileDir, downloadedFiles) {
|
|
112
|
-
const resolved = path.resolve(sourceFileDir, importPath);
|
|
113
|
-
for (const downloaded of downloadedFiles) {
|
|
114
|
-
const downloadedAbs = path.resolve(sourceFileDir, downloaded);
|
|
115
|
-
const dirOfDownloaded = dirname(downloadedAbs);
|
|
116
|
-
const basenameOfDownloaded = path.basename(downloadedAbs);
|
|
117
|
-
if (dirOfDownloaded === resolved && basenameOfDownloaded.startsWith("index.")) {
|
|
118
|
-
return `${importPath}/index`;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
function rewriteFileImports(fileContent, writtenFilePath, blocksRoot, aliasBase, strategy, downloadedFiles = /* @__PURE__ */ new Set()) {
|
|
124
|
-
const importRegex = /((?:from|import)\s*)(['"`])(\.\.?\/[^'"` \n\r]+)(['"`])/g;
|
|
125
|
-
const fileDir = dirname(writtenFilePath);
|
|
126
|
-
return fileContent.replace(importRegex, (match, keyword, openQuote, importPath, closeQuote) => {
|
|
127
|
-
let targetPath = importPath;
|
|
128
|
-
if (aliasBase && !aliasBase.startsWith(".")) {
|
|
129
|
-
const absoluteTarget = path.resolve(fileDir, importPath);
|
|
130
|
-
const fromBlocksRoot = path.relative(blocksRoot, absoluteTarget).replace(/\\/g, "/");
|
|
131
|
-
if (!fromBlocksRoot.startsWith("..")) {
|
|
132
|
-
targetPath = `${aliasBase}/${fromBlocksRoot}`.replace(/\/+/g, "/");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
const hasJsExt = targetPath.endsWith(".js");
|
|
136
|
-
if (strategy === "rewrite" && !hasJsExt) {
|
|
137
|
-
targetPath = targetPath.replace(/\.(ts|tsx|mts|cts)$/, "");
|
|
138
|
-
if (downloadedFiles.size > 0) {
|
|
139
|
-
const indexResolved = resolveIndexImport(targetPath, fileDir, downloadedFiles);
|
|
140
|
-
if (indexResolved) {
|
|
141
|
-
targetPath = indexResolved;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
targetPath = `${targetPath}.js`;
|
|
145
|
-
} else if (strategy === "remove" && hasJsExt) {
|
|
146
|
-
targetPath = targetPath.slice(0, -3);
|
|
147
|
-
}
|
|
148
|
-
return `${keyword}${openQuote}${targetPath}${closeQuote}`;
|
|
149
|
-
});
|
|
150
|
-
}
|
|
595
|
+
|
|
596
|
+
// src/commands/add.ts
|
|
151
597
|
function outputError(json, message) {
|
|
152
598
|
if (json) {
|
|
153
599
|
process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
|
|
@@ -168,46 +614,37 @@ function handleCancel(value) {
|
|
|
168
614
|
process.exit(0);
|
|
169
615
|
}
|
|
170
616
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await fs.access(candidate);
|
|
177
|
-
return candidate;
|
|
178
|
-
} catch {
|
|
179
|
-
const parent = dirname(dir);
|
|
180
|
-
if (parent === dir) break;
|
|
181
|
-
dir = parent;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return null;
|
|
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}"`;
|
|
185
622
|
}
|
|
186
|
-
async function addSingleBlock(targetBlock,
|
|
187
|
-
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;
|
|
188
637
|
const s = spinner();
|
|
189
|
-
const
|
|
190
|
-
if (!
|
|
191
|
-
outputError(json,
|
|
192
|
-
return {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
const adapterContext = resolveAdapterContext(blockMeta, envKey);
|
|
199
|
-
if (!adapterContext) {
|
|
200
|
-
outputError(
|
|
201
|
-
json,
|
|
202
|
-
`Block "${targetBlock}" has no adapter or environment entry for: ${envKey}. This is a registry configuration issue \u2014 please open an issue.`
|
|
203
|
-
);
|
|
204
|
-
return { success: false, filesWritten: [], message: `No adapter for "${targetBlock}"` };
|
|
205
|
-
}
|
|
206
|
-
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
207
|
-
if (variantKeys.length === 0) {
|
|
208
|
-
outputError(json, `No variants found for "${targetBlock}" / "${envKey}".`);
|
|
209
|
-
return { success: false, filesWritten: [], message: `No variants for "${targetBlock}"` };
|
|
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
|
+
};
|
|
210
646
|
}
|
|
647
|
+
const variantKeys = initialPlan.plan.variantKeys;
|
|
211
648
|
let selectedVariant;
|
|
212
649
|
if (yes) {
|
|
213
650
|
selectedVariant = variantKeys.includes("memory") ? "memory" : variantKeys[0];
|
|
@@ -218,22 +655,36 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
218
655
|
} else {
|
|
219
656
|
const variantPrompt = await select({
|
|
220
657
|
message: `Select a storage variant for ${targetBlock}:`,
|
|
221
|
-
options: variantKeys.map((
|
|
658
|
+
options: variantKeys.map((variant) => ({ value: variant, label: variant.toUpperCase() }))
|
|
222
659
|
});
|
|
223
660
|
handleCancel(variantPrompt);
|
|
224
661
|
selectedVariant = variantPrompt;
|
|
225
662
|
}
|
|
226
|
-
|
|
227
|
-
|
|
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);
|
|
228
679
|
const packageJsonPath = await findUp("package.json", rootDir);
|
|
229
680
|
if (!packageJsonPath) {
|
|
230
681
|
outputError(json, "Could not locate package.json.");
|
|
231
682
|
return { success: false, filesWritten: [], message: "package.json not found" };
|
|
232
683
|
}
|
|
233
|
-
const packageJsonDir =
|
|
684
|
+
const packageJsonDir = dirname4(packageJsonPath);
|
|
234
685
|
let packageJson;
|
|
235
686
|
try {
|
|
236
|
-
packageJson = JSON.parse(await
|
|
687
|
+
packageJson = JSON.parse(await fs5.readFile(packageJsonPath, "utf-8"));
|
|
237
688
|
} catch (err) {
|
|
238
689
|
outputError(json, "Failed to parse package.json.");
|
|
239
690
|
if (!json) log.error(pc.dim(String(err)));
|
|
@@ -243,20 +694,10 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
243
694
|
...packageJson.dependencies ?? {},
|
|
244
695
|
...packageJson.devDependencies ?? {}
|
|
245
696
|
};
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
];
|
|
251
|
-
const allBlockDevDeps = [
|
|
252
|
-
...blockMeta.devDependencies ?? [],
|
|
253
|
-
...adapterContext.devDependencies ?? [],
|
|
254
|
-
...variantMeta?.devDependencies ?? []
|
|
255
|
-
];
|
|
256
|
-
const missingProd = getMissingDependencies(allBlockDeps, installedDeps, true);
|
|
257
|
-
const missingDev = getMissingDependencies(allBlockDevDeps, installedDeps, true);
|
|
258
|
-
const prodConflicts = getVersionConflicts(allBlockDeps, installedDeps);
|
|
259
|
-
const devConflicts = getVersionConflicts(allBlockDevDeps, installedDeps);
|
|
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);
|
|
260
701
|
const allConflicts = [...prodConflicts, ...devConflicts];
|
|
261
702
|
if (allConflicts.length > 0 && !json) {
|
|
262
703
|
log.warn(`Version conflicts detected:`);
|
|
@@ -272,7 +713,7 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
272
713
|
});
|
|
273
714
|
if (!yes) handleCancel(shouldInstall);
|
|
274
715
|
if (shouldInstall) {
|
|
275
|
-
const pm =
|
|
716
|
+
const pm = config.packageManager || "npm";
|
|
276
717
|
if (!json) s.start(`Installing via ${pm}...`);
|
|
277
718
|
const tasks = [];
|
|
278
719
|
if (missingProd.length > 0) {
|
|
@@ -312,27 +753,10 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
312
753
|
}
|
|
313
754
|
}
|
|
314
755
|
}
|
|
315
|
-
const filesToDownload = [];
|
|
316
|
-
if (blockMeta.baseFiles?.length) {
|
|
317
|
-
filesToDownload.push(...blockMeta.baseFiles);
|
|
318
|
-
}
|
|
319
|
-
if (adapterContext.core) {
|
|
320
|
-
filesToDownload.push({
|
|
321
|
-
source: adapterContext.core,
|
|
322
|
-
target: path.basename(adapterContext.core)
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
326
|
-
for (const entry of variantMeta.files) {
|
|
327
|
-
filesToDownload.push(
|
|
328
|
-
typeof entry === "string" ? { source: entry, target: path.basename(entry) } : entry
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
756
|
let hasConflict = false;
|
|
333
|
-
for (const
|
|
757
|
+
for (const file of plan.files) {
|
|
334
758
|
try {
|
|
335
|
-
await
|
|
759
|
+
await fs5.access(join3(targetFolder, file.target));
|
|
336
760
|
hasConflict = true;
|
|
337
761
|
break;
|
|
338
762
|
} catch {
|
|
@@ -349,57 +773,18 @@ async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBl
|
|
|
349
773
|
}
|
|
350
774
|
}
|
|
351
775
|
if (!json) s.start(`Downloading ${targetBlock}...`);
|
|
352
|
-
const filesWritten = [];
|
|
353
|
-
const downloadedTargets = /* @__PURE__ */ new Set();
|
|
354
776
|
try {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
let contentHash = 0;
|
|
363
|
-
for (const { fm, raw } of fileBatches) {
|
|
364
|
-
const writtenFilePath = path.join(targetFolder, fm.target);
|
|
365
|
-
const content = rewriteFileImports(
|
|
366
|
-
raw,
|
|
367
|
-
writtenFilePath,
|
|
368
|
-
blocksRootAbsolute,
|
|
369
|
-
aliasBase,
|
|
370
|
-
rewriteStrategy,
|
|
371
|
-
downloadedTargets
|
|
372
|
-
);
|
|
373
|
-
await fs.mkdir(dirname(writtenFilePath), { recursive: true });
|
|
374
|
-
await fs.writeFile(writtenFilePath, content, "utf-8");
|
|
375
|
-
filesWritten.push(writtenFilePath);
|
|
376
|
-
for (let i = 0; i < content.length; i++) {
|
|
377
|
-
contentHash = (contentHash << 5) - contentHash + content.charCodeAt(i) | 0;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
const blockVersion = blockMeta.version ?? "0.0.0";
|
|
381
|
-
const installedRecord = {
|
|
382
|
-
name: targetBlock,
|
|
383
|
-
version: blockVersion,
|
|
384
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
385
|
-
files: filesWritten.map((f) => path.relative(targetFolder, f).replace(/\\/g, "/")),
|
|
386
|
-
contentHash: contentHash.toString(16),
|
|
387
|
-
variant: selectedVariant
|
|
388
|
-
};
|
|
389
|
-
try {
|
|
390
|
-
const blockendPath = join(rootDir, "blockend.json");
|
|
391
|
-
const cfgRaw = await fs.readFile(blockendPath, "utf-8");
|
|
392
|
-
const cfgData = JSON.parse(cfgRaw);
|
|
393
|
-
if (!Array.isArray(cfgData.installed)) cfgData.installed = [];
|
|
394
|
-
cfgData.installed = cfgData.installed.filter((b) => b.name !== targetBlock);
|
|
395
|
-
cfgData.installed.push(installedRecord);
|
|
396
|
-
await fs.writeFile(blockendPath, JSON.stringify(cfgData, null, 2), "utf-8");
|
|
397
|
-
} catch {
|
|
398
|
-
}
|
|
777
|
+
const applied = await applyBlock(plan, {
|
|
778
|
+
source,
|
|
779
|
+
blocksRoot: blocksRootAbsolute,
|
|
780
|
+
aliasBase,
|
|
781
|
+
rewriteStrategy,
|
|
782
|
+
configPath
|
|
783
|
+
});
|
|
399
784
|
if (!json) s.stop("Files written.");
|
|
400
785
|
return {
|
|
401
786
|
success: true,
|
|
402
|
-
filesWritten,
|
|
787
|
+
filesWritten: applied.files,
|
|
403
788
|
message: `${targetBlock} added to ${physicalBlocksPath}/${targetBlock}`
|
|
404
789
|
};
|
|
405
790
|
} catch (err) {
|
|
@@ -422,10 +807,10 @@ async function addCommand(blockName, options = {}) {
|
|
|
422
807
|
outputError(json, "blockend.json not found. Run 'npx blockend init' first.");
|
|
423
808
|
return;
|
|
424
809
|
}
|
|
425
|
-
const rootDir =
|
|
810
|
+
const rootDir = dirname4(configPath);
|
|
426
811
|
let config;
|
|
427
812
|
try {
|
|
428
|
-
config = JSON.parse(await
|
|
813
|
+
config = JSON.parse(await fs5.readFile(configPath, "utf-8"));
|
|
429
814
|
} catch (err) {
|
|
430
815
|
outputError(json, "Failed to parse blockend.json.");
|
|
431
816
|
if (!json) log.error(pc.dim(String(err)));
|
|
@@ -436,17 +821,16 @@ async function addCommand(blockName, options = {}) {
|
|
|
436
821
|
return;
|
|
437
822
|
}
|
|
438
823
|
const physicalBlocksPath = config.paths.blocks;
|
|
439
|
-
const blocksRootAbsolute =
|
|
824
|
+
const blocksRootAbsolute = path5.resolve(rootDir, physicalBlocksPath);
|
|
440
825
|
const aliasBase = config.aliases.blocks;
|
|
441
826
|
const envKey = config.environment;
|
|
442
827
|
const rewriteStrategy = config.importRewriteStrategy ?? "remove";
|
|
828
|
+
const source = createGitHubRegistrySource();
|
|
443
829
|
const s = spinner();
|
|
444
830
|
if (!json) s.start("Fetching registry...");
|
|
445
831
|
let registry;
|
|
446
832
|
try {
|
|
447
|
-
|
|
448
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
449
|
-
registry = await res.json();
|
|
833
|
+
registry = await loadRegistry(source);
|
|
450
834
|
if (!json) s.stop("Registry synced.");
|
|
451
835
|
} catch (err) {
|
|
452
836
|
if (!json) {
|
|
@@ -455,26 +839,38 @@ async function addCommand(blockName, options = {}) {
|
|
|
455
839
|
} else outputError(json, "Failed to fetch registry from GitHub.");
|
|
456
840
|
return;
|
|
457
841
|
}
|
|
458
|
-
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
|
+
};
|
|
459
855
|
if (yes && !blockName) {
|
|
460
856
|
outputError(json, "Block name required with --yes. Usage: blockend add <block> --yes");
|
|
461
857
|
return;
|
|
462
858
|
}
|
|
463
859
|
if (multi && !blockName) {
|
|
464
|
-
const available =
|
|
465
|
-
([, block]) => block != null && blockSupportsEnv(block, envKey)
|
|
466
|
-
);
|
|
860
|
+
const available = await listAvailableBlocks(registry, envKey);
|
|
467
861
|
if (available.length === 0) {
|
|
468
862
|
outputError(json, `No blocks available for environment: ${envKey}`);
|
|
469
863
|
if (!json) outro(pc.dim("Exiting."));
|
|
470
864
|
return;
|
|
471
865
|
}
|
|
472
|
-
const maxKeyLen = Math.max(...available.map((
|
|
866
|
+
const maxKeyLen = Math.max(...available.map((block) => block.name.length), 0);
|
|
473
867
|
const multiSelectPrompt = await multiselect({
|
|
474
868
|
message: "Select blocks to add (space to select, enter to confirm):",
|
|
475
|
-
options: available.map((
|
|
476
|
-
value:
|
|
477
|
-
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(
|
|
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(
|
|
478
874
|
block.description.length > 60 ? `${block.description.slice(0, 60)}...` : block.description
|
|
479
875
|
)}`
|
|
480
876
|
}))
|
|
@@ -490,18 +886,7 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
490
886
|
`);
|
|
491
887
|
const results = [];
|
|
492
888
|
for (const block of selectedBlocks) {
|
|
493
|
-
const result2 = await addSingleBlock(
|
|
494
|
-
block,
|
|
495
|
-
blockMap,
|
|
496
|
-
config,
|
|
497
|
-
rootDir,
|
|
498
|
-
physicalBlocksPath,
|
|
499
|
-
blocksRootAbsolute,
|
|
500
|
-
aliasBase,
|
|
501
|
-
envKey,
|
|
502
|
-
rewriteStrategy,
|
|
503
|
-
options
|
|
504
|
-
);
|
|
889
|
+
const result2 = await addSingleBlock(block, context);
|
|
505
890
|
if (result2.success) {
|
|
506
891
|
results.push(result2.message);
|
|
507
892
|
}
|
|
@@ -516,20 +901,20 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
516
901
|
}
|
|
517
902
|
let targetBlock = blockName;
|
|
518
903
|
if (!targetBlock) {
|
|
519
|
-
const available =
|
|
520
|
-
([, block]) => block != null && blockSupportsEnv(block, envKey)
|
|
521
|
-
);
|
|
904
|
+
const available = await listAvailableBlocks(registry, envKey);
|
|
522
905
|
if (available.length === 0) {
|
|
523
906
|
outputError(json, `No blocks available for environment: ${envKey}`);
|
|
524
907
|
if (!json) outro(pc.dim("Exiting."));
|
|
525
908
|
return;
|
|
526
909
|
}
|
|
527
|
-
const maxKeyLen = Math.max(...available.map((
|
|
910
|
+
const maxKeyLen = Math.max(...available.map((block) => block.name.length), 0);
|
|
528
911
|
const selectPrompt = await select({
|
|
529
912
|
message: "Which block would you like to add?",
|
|
530
|
-
options: available.map((
|
|
531
|
-
value:
|
|
532
|
-
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(
|
|
533
918
|
block.description.length > 60 ? `${block.description.slice(0, 60)}...` : block.description
|
|
534
919
|
)}`
|
|
535
920
|
}))
|
|
@@ -537,18 +922,7 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
537
922
|
handleCancel(selectPrompt);
|
|
538
923
|
targetBlock = selectPrompt;
|
|
539
924
|
}
|
|
540
|
-
const result = await addSingleBlock(
|
|
541
|
-
targetBlock,
|
|
542
|
-
blockMap,
|
|
543
|
-
config,
|
|
544
|
-
rootDir,
|
|
545
|
-
physicalBlocksPath,
|
|
546
|
-
blocksRootAbsolute,
|
|
547
|
-
aliasBase,
|
|
548
|
-
envKey,
|
|
549
|
-
rewriteStrategy,
|
|
550
|
-
options
|
|
551
|
-
);
|
|
925
|
+
const result = await addSingleBlock(targetBlock, context);
|
|
552
926
|
outputResult(json, {
|
|
553
927
|
success: result.success,
|
|
554
928
|
block: targetBlock,
|
|
@@ -558,20 +932,20 @@ Adding ${selectedBlocks.length} block(s)...
|
|
|
558
932
|
}
|
|
559
933
|
|
|
560
934
|
// src/commands/init.ts
|
|
561
|
-
import
|
|
562
|
-
import
|
|
935
|
+
import path7, { join as join9 } from "path";
|
|
936
|
+
import fs6 from "fs/promises";
|
|
563
937
|
import { existsSync as existsSync4 } from "fs";
|
|
564
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";
|
|
565
939
|
import pc3 from "picocolors";
|
|
566
940
|
|
|
567
941
|
// src/detectors/index.ts
|
|
568
|
-
import { join as
|
|
942
|
+
import path6, { join as join8 } from "path";
|
|
569
943
|
|
|
570
944
|
// src/detectors/readers/package.ts
|
|
571
945
|
import { readFile } from "fs/promises";
|
|
572
|
-
import { join as
|
|
946
|
+
import { join as join4 } from "path";
|
|
573
947
|
async function readPackageJson(cwd) {
|
|
574
|
-
const pkgPath =
|
|
948
|
+
const pkgPath = join4(cwd, "package.json");
|
|
575
949
|
try {
|
|
576
950
|
const content = await readFile(pkgPath, "utf-8");
|
|
577
951
|
return JSON.parse(content);
|
|
@@ -583,30 +957,122 @@ async function readPackageJson(cwd) {
|
|
|
583
957
|
// src/detectors/readers/tsconfig.ts
|
|
584
958
|
import { readFile as readFile2 } from "fs/promises";
|
|
585
959
|
import { existsSync } from "fs";
|
|
586
|
-
import { join as
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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;
|
|
599
1064
|
}
|
|
1065
|
+
return output;
|
|
600
1066
|
}
|
|
601
1067
|
|
|
602
1068
|
// src/detectors/readers/filesystem.ts
|
|
603
1069
|
import { existsSync as existsSync2 } from "fs";
|
|
604
|
-
import { join as
|
|
1070
|
+
import { join as join6 } from "path";
|
|
605
1071
|
async function inferSrcDir(cwd) {
|
|
606
1072
|
const candidates = ["src", "app", "lib"];
|
|
607
1073
|
for (const dir of candidates) {
|
|
608
|
-
if (existsSync2(
|
|
609
|
-
return
|
|
1074
|
+
if (existsSync2(join6(cwd, dir))) {
|
|
1075
|
+
return join6(cwd, dir);
|
|
610
1076
|
}
|
|
611
1077
|
}
|
|
612
1078
|
return cwd;
|
|
@@ -629,9 +1095,9 @@ function detectRuntime(deps) {
|
|
|
629
1095
|
|
|
630
1096
|
// src/detectors/package-manager.ts
|
|
631
1097
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
632
|
-
import { join as
|
|
1098
|
+
import { join as join7 } from "path";
|
|
633
1099
|
function detectPackageManager(cwd) {
|
|
634
|
-
const pkgJsonPath =
|
|
1100
|
+
const pkgJsonPath = join7(cwd, "package.json");
|
|
635
1101
|
if (existsSync3(pkgJsonPath)) {
|
|
636
1102
|
try {
|
|
637
1103
|
const packageJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
@@ -644,52 +1110,76 @@ function detectPackageManager(cwd) {
|
|
|
644
1110
|
} catch {
|
|
645
1111
|
}
|
|
646
1112
|
}
|
|
647
|
-
if (existsSync3(
|
|
648
|
-
if (existsSync3(
|
|
649
|
-
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";
|
|
650
1116
|
return "npm";
|
|
651
1117
|
}
|
|
652
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
|
+
|
|
653
1148
|
// src/detectors/index.ts
|
|
654
|
-
async function detectProject(cwd) {
|
|
655
|
-
const [pkg,
|
|
1149
|
+
async function detectProject(cwd, options = {}) {
|
|
1150
|
+
const [pkg, compilerConfig] = await Promise.all([readPackageJson(cwd), readCompilerConfig(cwd)]);
|
|
656
1151
|
const allDeps = {
|
|
657
1152
|
...pkg.dependencies,
|
|
658
1153
|
...pkg.devDependencies,
|
|
659
1154
|
...pkg.peerDependencies
|
|
660
1155
|
};
|
|
661
1156
|
const srcDir = await inferSrcDir(cwd);
|
|
662
|
-
const
|
|
663
|
-
const
|
|
1157
|
+
const compilerOptions = compilerConfig?.config.compilerOptions;
|
|
1158
|
+
const moduleOpt = compilerOptions?.module?.toLocaleLowerCase();
|
|
1159
|
+
const resolutionOpt = compilerOptions?.moduleResolution?.toLocaleLowerCase();
|
|
664
1160
|
let importRewriteStrategy = "remove";
|
|
665
1161
|
if (moduleOpt === "nodenext" || moduleOpt === "node16" || resolutionOpt === "nodenext" || resolutionOpt === "node16" || pkg.type === "module" && !resolutionOpt) {
|
|
666
1162
|
importRewriteStrategy = "rewrite";
|
|
667
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;
|
|
668
1167
|
return {
|
|
669
1168
|
root: cwd,
|
|
670
|
-
language:
|
|
1169
|
+
language: compilerConfig?.kind === "tsconfig" ? "typescript" : "javascript",
|
|
671
1170
|
runtime: detectRuntime(allDeps),
|
|
672
1171
|
framework: detectFramework(allDeps),
|
|
673
1172
|
packageManager: detectPackageManager(cwd),
|
|
674
1173
|
hasRedis: "ioredis" in allDeps || "redis" in allDeps,
|
|
675
1174
|
hasPrisma: "@prisma/client" in allDeps,
|
|
676
1175
|
hasDrizzle: "drizzle-orm" in allDeps,
|
|
677
|
-
aliasMap:
|
|
1176
|
+
aliasMap: tsPaths ? flattenTsPaths(tsPaths) : {},
|
|
678
1177
|
srcDir,
|
|
679
|
-
|
|
680
|
-
|
|
1178
|
+
blocksDir,
|
|
1179
|
+
blocksAlias: tsPaths ? deriveBlocksAlias(tsPaths, relativeBlocksDir) : void 0,
|
|
681
1180
|
importRewriteStrategy
|
|
682
1181
|
};
|
|
683
1182
|
}
|
|
684
|
-
function flattenTsPaths(paths) {
|
|
685
|
-
const result = {};
|
|
686
|
-
for (const [alias, targets] of Object.entries(paths)) {
|
|
687
|
-
if (targets[0]) {
|
|
688
|
-
result[alias.replace("/*", "/")] = targets[0].replace("/*", "/");
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
return result;
|
|
692
|
-
}
|
|
693
1183
|
|
|
694
1184
|
// src/ui/theme.ts
|
|
695
1185
|
import pc2 from "picocolors";
|
|
@@ -728,41 +1218,6 @@ var format = {
|
|
|
728
1218
|
};
|
|
729
1219
|
|
|
730
1220
|
// src/commands/init.ts
|
|
731
|
-
async function resolveTsConfigPaths(cwd) {
|
|
732
|
-
const candidates = [join7(cwd, "tsconfig.json"), join7(cwd, "jsconfig.json")];
|
|
733
|
-
for (const configPath of candidates) {
|
|
734
|
-
if (!existsSync4(configPath)) continue;
|
|
735
|
-
try {
|
|
736
|
-
const raw = await fs2.readFile(configPath, "utf-8");
|
|
737
|
-
const clean = raw.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
|
|
738
|
-
const parsed = JSON.parse(clean);
|
|
739
|
-
return {
|
|
740
|
-
baseUrl: parsed.compilerOptions?.baseUrl ?? ".",
|
|
741
|
-
paths: parsed.compilerOptions?.paths ?? {}
|
|
742
|
-
};
|
|
743
|
-
} catch {
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
return { baseUrl: ".", paths: {} };
|
|
747
|
-
}
|
|
748
|
-
function deriveBlockAlias(tsPaths, blocksPhysicalPath) {
|
|
749
|
-
const normalizedBlocksPath = blocksPhysicalPath.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
750
|
-
for (const [aliasKey, targets] of Object.entries(tsPaths)) {
|
|
751
|
-
if (!Array.isArray(targets) || targets.length === 0) continue;
|
|
752
|
-
const rawTarget = targets[0].replace(/\\/g, "/");
|
|
753
|
-
const targetRoot = rawTarget.replace(/\*$/, "").replace(/\/$/, "").replace(/^\.\//, "");
|
|
754
|
-
const aliasRoot = aliasKey.replace(/\/\*$/, "").replace(/\*$/, "");
|
|
755
|
-
if (targetRoot === "") {
|
|
756
|
-
return `${aliasRoot}/${normalizedBlocksPath}`.replace(/\/+/g, "/");
|
|
757
|
-
}
|
|
758
|
-
if (normalizedBlocksPath === targetRoot || normalizedBlocksPath.startsWith(`${targetRoot}/`)) {
|
|
759
|
-
const suffix = normalizedBlocksPath.slice(targetRoot.length).replace(/^\//, "");
|
|
760
|
-
const alias = suffix ? `${aliasRoot}/${suffix}` : aliasRoot;
|
|
761
|
-
return alias.replace(/\/+/g, "/");
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return void 0;
|
|
765
|
-
}
|
|
766
1221
|
function outputInitError(json, message) {
|
|
767
1222
|
if (json) {
|
|
768
1223
|
process.stdout.write(JSON.stringify({ success: false, error: message }) + "\n");
|
|
@@ -781,7 +1236,7 @@ function outputInitResult(json, result) {
|
|
|
781
1236
|
async function initCommand(options = {}) {
|
|
782
1237
|
const { yes = false, json = false } = options;
|
|
783
1238
|
const cwd = process.cwd();
|
|
784
|
-
const configPath =
|
|
1239
|
+
const configPath = join9(cwd, "blockend.json");
|
|
785
1240
|
if (!json) {
|
|
786
1241
|
console.log("");
|
|
787
1242
|
intro2(`${pc3.bgCyan(pc3.black(" blockend "))} ${theme.text.muted("setup")}`);
|
|
@@ -813,15 +1268,35 @@ async function initCommand(options = {}) {
|
|
|
813
1268
|
action = actionPrompt;
|
|
814
1269
|
}
|
|
815
1270
|
if (action === "regenerate") {
|
|
816
|
-
await
|
|
1271
|
+
await fs6.unlink(configPath);
|
|
817
1272
|
if (!json) log2.warn("Existing configuration deleted.");
|
|
818
1273
|
}
|
|
819
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
|
+
}
|
|
820
1297
|
const s = spinner2();
|
|
821
1298
|
if (!json) s.start("Scanning project architecture...");
|
|
822
|
-
const context = await detectProject(cwd);
|
|
823
|
-
const hasSrcDir = existsSync4(join7(cwd, "src"));
|
|
824
|
-
const tsConfig = await resolveTsConfigPaths(cwd);
|
|
1299
|
+
const context = await detectProject(cwd, { blocksDir: rawPhysicalInput });
|
|
825
1300
|
if (!json) s.stop("Project scanned.");
|
|
826
1301
|
let framework = context.framework;
|
|
827
1302
|
if (!framework) {
|
|
@@ -850,27 +1325,9 @@ async function initCommand(options = {}) {
|
|
|
850
1325
|
} else if (!json) {
|
|
851
1326
|
log2.info(`Framework detected: ${theme.state.info(framework)}`);
|
|
852
1327
|
}
|
|
853
|
-
const
|
|
854
|
-
let rawPhysicalInput = defaultDir;
|
|
855
|
-
if (!yes) {
|
|
856
|
-
const directoryPrompt = await text({
|
|
857
|
-
message: "Where would you like to install blocks?",
|
|
858
|
-
placeholder: defaultDir,
|
|
859
|
-
initialValue: defaultDir,
|
|
860
|
-
validate(value) {
|
|
861
|
-
if (!value || value.trim().length === 0) return "Directory path cannot be empty.";
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
if (isCancel2(directoryPrompt)) {
|
|
865
|
-
outro2(format.muted("Setup cancelled."));
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
rawPhysicalInput = String(directoryPrompt).trim();
|
|
869
|
-
}
|
|
870
|
-
const relativeBlocksPath = path2.relative(cwd, path2.resolve(cwd, rawPhysicalInput)).replace(/\\/g, "/");
|
|
1328
|
+
const relativeBlocksPath = path7.relative(cwd, context.blocksDir).replace(/\\/g, "/");
|
|
871
1329
|
const finalPath = relativeBlocksPath.startsWith(".") ? relativeBlocksPath : `./${relativeBlocksPath}`;
|
|
872
|
-
const
|
|
873
|
-
const finalBlockAlias = hasTsPaths ? deriveBlockAlias(tsConfig.paths, relativeBlocksPath) : void 0;
|
|
1330
|
+
const finalBlockAlias = context.blocksAlias;
|
|
874
1331
|
if (!json) {
|
|
875
1332
|
log2.info(
|
|
876
1333
|
`Import alias: ${theme.state.info(finalBlockAlias === void 0 ? " relative imports" : finalBlockAlias)}`
|
|
@@ -909,7 +1366,7 @@ async function initCommand(options = {}) {
|
|
|
909
1366
|
};
|
|
910
1367
|
if (!json) s.start("Writing configuration...");
|
|
911
1368
|
try {
|
|
912
|
-
await
|
|
1369
|
+
await fs6.writeFile(configPath, JSON.stringify(configPayload, null, 2), "utf-8");
|
|
913
1370
|
if (!json) s.stop("Configuration saved.");
|
|
914
1371
|
outputInitResult(json, {
|
|
915
1372
|
success: true,
|
|
@@ -967,33 +1424,13 @@ async function detectCommand(options = {}) {
|
|
|
967
1424
|
}
|
|
968
1425
|
|
|
969
1426
|
// src/commands/list.ts
|
|
970
|
-
import
|
|
971
|
-
import fs3 from "fs/promises";
|
|
1427
|
+
import fs7 from "fs/promises";
|
|
972
1428
|
import pc5 from "picocolors";
|
|
973
1429
|
import { outro as outro4, spinner as spinner3 } from "@clack/prompts";
|
|
974
|
-
var REPO_OWNER2 = "codewithnuh";
|
|
975
|
-
var REPO_NAME2 = "blockend";
|
|
976
|
-
var BRANCH2 = "master";
|
|
977
|
-
var MANIFEST_URL2 = `https://raw.githubusercontent.com/${REPO_OWNER2}/${REPO_NAME2}/${BRANCH2}/registry/index.json`;
|
|
978
|
-
async function findUp2(filename, startDir) {
|
|
979
|
-
let dir = startDir;
|
|
980
|
-
while (true) {
|
|
981
|
-
const checkPath = join8(dir, filename);
|
|
982
|
-
try {
|
|
983
|
-
await fs3.access(checkPath);
|
|
984
|
-
return checkPath;
|
|
985
|
-
} catch {
|
|
986
|
-
const parent = dirname2(dir);
|
|
987
|
-
if (parent === dir) break;
|
|
988
|
-
dir = parent;
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
return null;
|
|
992
|
-
}
|
|
993
1430
|
async function listCommand(options = {}) {
|
|
994
1431
|
const { json = false } = options;
|
|
995
1432
|
const cwd = process.cwd();
|
|
996
|
-
const configPath = await
|
|
1433
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
997
1434
|
if (!configPath) {
|
|
998
1435
|
if (json) {
|
|
999
1436
|
process.stdout.write(
|
|
@@ -1006,7 +1443,7 @@ async function listCommand(options = {}) {
|
|
|
1006
1443
|
}
|
|
1007
1444
|
let config;
|
|
1008
1445
|
try {
|
|
1009
|
-
const configFile = await
|
|
1446
|
+
const configFile = await fs7.readFile(configPath, "utf-8");
|
|
1010
1447
|
config = JSON.parse(configFile);
|
|
1011
1448
|
} catch {
|
|
1012
1449
|
if (json) {
|
|
@@ -1019,15 +1456,14 @@ async function listCommand(options = {}) {
|
|
|
1019
1456
|
return;
|
|
1020
1457
|
}
|
|
1021
1458
|
const envKey = String(config.environment);
|
|
1459
|
+
const source = createGitHubRegistrySource();
|
|
1022
1460
|
const s = spinner3();
|
|
1023
1461
|
if (!json) {
|
|
1024
1462
|
s.start("Syncing registry metadata blocks...");
|
|
1025
1463
|
}
|
|
1026
1464
|
let registry;
|
|
1027
1465
|
try {
|
|
1028
|
-
|
|
1029
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1030
|
-
registry = await response.json();
|
|
1466
|
+
registry = await loadRegistry(source);
|
|
1031
1467
|
if (!json) s.stop(pc5.green("\u2714 Synced available block registries."));
|
|
1032
1468
|
} catch {
|
|
1033
1469
|
if (json) {
|
|
@@ -1039,25 +1475,12 @@ async function listCommand(options = {}) {
|
|
|
1039
1475
|
}
|
|
1040
1476
|
return;
|
|
1041
1477
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
const blocksForEnv = Object.entries(blockMap).filter(([_, block]) => {
|
|
1049
|
-
if (!block) return false;
|
|
1050
|
-
const hasAdapter = block.adapters?.[envKey] !== void 0;
|
|
1051
|
-
const hasEnvironment = block.environments?.[envKey] !== void 0;
|
|
1052
|
-
return hasAdapter || hasEnvironment;
|
|
1053
|
-
}).map(([key, block]) => {
|
|
1054
|
-
const context = block.adapters?.[envKey] ?? block.environments?.[envKey];
|
|
1055
|
-
return {
|
|
1056
|
-
name: key,
|
|
1057
|
-
description: block.description,
|
|
1058
|
-
variants: Object.keys(context?.variants || {})
|
|
1059
|
-
};
|
|
1060
|
-
});
|
|
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
|
+
}));
|
|
1061
1484
|
if (json) {
|
|
1062
1485
|
process.stdout.write(
|
|
1063
1486
|
JSON.stringify({ success: true, environment: envKey, blocks: blocksForEnv }) + "\n"
|
|
@@ -1068,11 +1491,13 @@ Available backend blocks for ${pc5.magenta(envKey)}:`);
|
|
|
1068
1491
|
if (blocksForEnv.length === 0) {
|
|
1069
1492
|
console.log(pc5.dim(" No blocks found for this framework."));
|
|
1070
1493
|
} else {
|
|
1071
|
-
blocksForEnv.forEach((
|
|
1494
|
+
blocksForEnv.forEach((block) => {
|
|
1072
1495
|
console.log(`
|
|
1073
|
-
${pc5.cyan(
|
|
1074
|
-
console.log(` ${pc5.dim(
|
|
1075
|
-
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
|
+
);
|
|
1076
1501
|
});
|
|
1077
1502
|
console.log();
|
|
1078
1503
|
}
|
|
@@ -1080,8 +1505,8 @@ Available backend blocks for ${pc5.magenta(envKey)}:`);
|
|
|
1080
1505
|
}
|
|
1081
1506
|
|
|
1082
1507
|
// src/commands/mcp.ts
|
|
1083
|
-
import
|
|
1084
|
-
import
|
|
1508
|
+
import path8, { join as join10 } from "path";
|
|
1509
|
+
import fs8 from "fs/promises";
|
|
1085
1510
|
import pc6 from "picocolors";
|
|
1086
1511
|
import { outro as outro5, spinner as spinner4, select as select3, confirm as confirm3 } from "@clack/prompts";
|
|
1087
1512
|
var CLIENT_MATRIX = {
|
|
@@ -1127,7 +1552,7 @@ args = [${args}]
|
|
|
1127
1552
|
async function mcpStartCommand() {
|
|
1128
1553
|
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
1129
1554
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1130
|
-
const { z } = await import("zod");
|
|
1555
|
+
const { z: z2 } = await import("zod");
|
|
1131
1556
|
const { execa } = await import("execa");
|
|
1132
1557
|
const server = new McpServer({
|
|
1133
1558
|
name: "blockend",
|
|
@@ -1146,7 +1571,7 @@ async function mcpStartCommand() {
|
|
|
1146
1571
|
}
|
|
1147
1572
|
server.tool(
|
|
1148
1573
|
"list_blocks",
|
|
1149
|
-
"Lists all available Blockend blocks with names, descriptions, and
|
|
1574
|
+
"Lists all available Blockend blocks with names, descriptions, and variants",
|
|
1150
1575
|
{},
|
|
1151
1576
|
async () => {
|
|
1152
1577
|
const result = await safeExec("npx", ["--no-install", "blockend", "list", "--json"]);
|
|
@@ -1157,8 +1582,8 @@ async function mcpStartCommand() {
|
|
|
1157
1582
|
"add_block",
|
|
1158
1583
|
"Installs a Blockend block into a project.",
|
|
1159
1584
|
{
|
|
1160
|
-
blockName:
|
|
1161
|
-
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")
|
|
1162
1587
|
},
|
|
1163
1588
|
async ({ blockName, projectPath }) => {
|
|
1164
1589
|
const result = await safeExec(
|
|
@@ -1173,7 +1598,7 @@ async function mcpStartCommand() {
|
|
|
1173
1598
|
"detect_project",
|
|
1174
1599
|
"Analyzes a project context: framework, language, package manager, etc.",
|
|
1175
1600
|
{
|
|
1176
|
-
projectPath:
|
|
1601
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1177
1602
|
},
|
|
1178
1603
|
async ({ projectPath }) => {
|
|
1179
1604
|
const result = await safeExec("npx", ["--no-install", "blockend", "detect", "--json"], {
|
|
@@ -1186,8 +1611,8 @@ async function mcpStartCommand() {
|
|
|
1186
1611
|
"diff_block",
|
|
1187
1612
|
"Preview what files a block would generate without writing to disk. Shows new/modified/unchanged files.",
|
|
1188
1613
|
{
|
|
1189
|
-
blockName:
|
|
1190
|
-
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")
|
|
1191
1616
|
},
|
|
1192
1617
|
async ({ blockName, projectPath }) => {
|
|
1193
1618
|
const result = await safeExec(
|
|
@@ -1202,7 +1627,7 @@ async function mcpStartCommand() {
|
|
|
1202
1627
|
"doctor",
|
|
1203
1628
|
"Check project health: validates blockend.json, detects framework, checks for issues.",
|
|
1204
1629
|
{
|
|
1205
|
-
projectPath:
|
|
1630
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1206
1631
|
},
|
|
1207
1632
|
async ({ projectPath }) => {
|
|
1208
1633
|
const result = await safeExec("npx", ["--no-install", "blockend", "doctor", "--json"], {
|
|
@@ -1215,7 +1640,7 @@ async function mcpStartCommand() {
|
|
|
1215
1640
|
"update_check",
|
|
1216
1641
|
"Check for available updates to installed blocks. Shows version comparison and diffs.",
|
|
1217
1642
|
{
|
|
1218
|
-
projectPath:
|
|
1643
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1219
1644
|
},
|
|
1220
1645
|
async ({ projectPath }) => {
|
|
1221
1646
|
const result = await safeExec("npx", ["--no-install", "blockend", "update", "--json"], {
|
|
@@ -1228,12 +1653,12 @@ async function mcpStartCommand() {
|
|
|
1228
1653
|
"update_apply",
|
|
1229
1654
|
"Apply updates to all installed blocks. Downloads new versions, rewrites imports, and updates blockend.json.",
|
|
1230
1655
|
{
|
|
1231
|
-
projectPath:
|
|
1656
|
+
projectPath: z2.string().describe("Absolute path to the project root")
|
|
1232
1657
|
},
|
|
1233
1658
|
async ({ projectPath }) => {
|
|
1234
1659
|
const result = await safeExec(
|
|
1235
1660
|
"npx",
|
|
1236
|
-
["--no-install", "blockend", "update", "--apply", "--
|
|
1661
|
+
["--no-install", "blockend", "update", "--apply", "--json"],
|
|
1237
1662
|
{ cwd: projectPath }
|
|
1238
1663
|
);
|
|
1239
1664
|
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
@@ -1241,17 +1666,25 @@ async function mcpStartCommand() {
|
|
|
1241
1666
|
);
|
|
1242
1667
|
server.tool(
|
|
1243
1668
|
"add_blocks",
|
|
1244
|
-
"Install multiple blocks
|
|
1669
|
+
"Install multiple blocks into a project.",
|
|
1245
1670
|
{
|
|
1246
|
-
|
|
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")
|
|
1247
1673
|
},
|
|
1248
|
-
async ({ projectPath }) => {
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
+
};
|
|
1255
1688
|
}
|
|
1256
1689
|
);
|
|
1257
1690
|
const transport = new StdioServerTransport();
|
|
@@ -1280,7 +1713,7 @@ async function mcpInitCommand(options) {
|
|
|
1280
1713
|
outro5(pc6.red(`\u2716 Unsupported client identifier profile: ${options.client}`));
|
|
1281
1714
|
return;
|
|
1282
1715
|
}
|
|
1283
|
-
const absoluteConfigTarget =
|
|
1716
|
+
const absoluteConfigTarget = join10(cwd, meta.relativePath);
|
|
1284
1717
|
if (options.dryRun) {
|
|
1285
1718
|
console.log(
|
|
1286
1719
|
pc6.cyan(`
|
|
@@ -1297,7 +1730,7 @@ async function mcpInitCommand(options) {
|
|
|
1297
1730
|
}
|
|
1298
1731
|
let fileConflict = false;
|
|
1299
1732
|
try {
|
|
1300
|
-
await
|
|
1733
|
+
await fs8.access(absoluteConfigTarget);
|
|
1301
1734
|
fileConflict = true;
|
|
1302
1735
|
} catch {
|
|
1303
1736
|
}
|
|
@@ -1314,12 +1747,12 @@ async function mcpInitCommand(options) {
|
|
|
1314
1747
|
const s = spinner4();
|
|
1315
1748
|
s.start(`Writing localized project configurations inside ${pc6.dim(meta.relativePath)}...`);
|
|
1316
1749
|
try {
|
|
1317
|
-
await
|
|
1750
|
+
await fs8.mkdir(path8.dirname(absoluteConfigTarget), { recursive: true });
|
|
1318
1751
|
if (meta.format === "json") {
|
|
1319
1752
|
let workingConfig = {};
|
|
1320
1753
|
if (fileConflict) {
|
|
1321
1754
|
try {
|
|
1322
|
-
const rawContent = await
|
|
1755
|
+
const rawContent = await fs8.readFile(absoluteConfigTarget, "utf-8");
|
|
1323
1756
|
workingConfig = JSON.parse(rawContent);
|
|
1324
1757
|
} catch {
|
|
1325
1758
|
workingConfig = {};
|
|
@@ -1332,9 +1765,9 @@ async function mcpInitCommand(options) {
|
|
|
1332
1765
|
const existingServers = workingConfig.mcpServers || {};
|
|
1333
1766
|
workingConfig.mcpServers = { ...existingServers, blockend: BLOCKEND_SERVER };
|
|
1334
1767
|
}
|
|
1335
|
-
await
|
|
1768
|
+
await fs8.writeFile(absoluteConfigTarget, JSON.stringify(workingConfig, null, 2), "utf-8");
|
|
1336
1769
|
} else {
|
|
1337
|
-
await
|
|
1770
|
+
await fs8.writeFile(absoluteConfigTarget, generateToml(BLOCKEND_SERVER), "utf-8");
|
|
1338
1771
|
}
|
|
1339
1772
|
s.stop(
|
|
1340
1773
|
pc6.green(`\u2714 Local workspace integration fully complete! File target: ${meta.relativePath}`)
|
|
@@ -1346,47 +1779,10 @@ async function mcpInitCommand(options) {
|
|
|
1346
1779
|
}
|
|
1347
1780
|
|
|
1348
1781
|
// src/commands/diff.ts
|
|
1349
|
-
import
|
|
1350
|
-
import
|
|
1782
|
+
import path9 from "path";
|
|
1783
|
+
import fs9 from "fs/promises";
|
|
1351
1784
|
import { intro as intro3, outro as outro6, spinner as spinner5, log as log3 } from "@clack/prompts";
|
|
1352
1785
|
import pc7 from "picocolors";
|
|
1353
|
-
var REPO_OWNER3 = "codewithnuh";
|
|
1354
|
-
var REPO_NAME3 = "blockend";
|
|
1355
|
-
var BRANCH3 = "master";
|
|
1356
|
-
var RAW_CDN_BASE2 = `https://raw.githubusercontent.com/${REPO_OWNER3}/${REPO_NAME3}/${BRANCH3}`;
|
|
1357
|
-
var MANIFEST_URL3 = `${RAW_CDN_BASE2}/registry/index.json`;
|
|
1358
|
-
function blockSupportsEnv2(block, envKey) {
|
|
1359
|
-
if (block.frameworks && block.frameworks.length > 0) {
|
|
1360
|
-
return block.frameworks.includes("*") || block.frameworks.includes(envKey);
|
|
1361
|
-
}
|
|
1362
|
-
return block.adapters?.[envKey] !== void 0 || block.environments?.[envKey] !== void 0;
|
|
1363
|
-
}
|
|
1364
|
-
function resolveAdapterContext2(block, envKey) {
|
|
1365
|
-
return block.adapters?.[envKey] ?? block.adapters?.["*"] ?? block.environments?.[envKey] ?? block.environments?.["*"] ?? null;
|
|
1366
|
-
}
|
|
1367
|
-
async function findUp3(filename, startDir) {
|
|
1368
|
-
let dir = startDir;
|
|
1369
|
-
while (true) {
|
|
1370
|
-
const candidate = join10(dir, filename);
|
|
1371
|
-
try {
|
|
1372
|
-
await fs5.access(candidate);
|
|
1373
|
-
return candidate;
|
|
1374
|
-
} catch {
|
|
1375
|
-
const parent = dirname3(dir);
|
|
1376
|
-
if (parent === dir) break;
|
|
1377
|
-
dir = parent;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
return null;
|
|
1381
|
-
}
|
|
1382
|
-
async function fileExists(filePath) {
|
|
1383
|
-
try {
|
|
1384
|
-
await fs5.access(filePath);
|
|
1385
|
-
return true;
|
|
1386
|
-
} catch {
|
|
1387
|
-
return false;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
1786
|
async function diffCommand(blockName, options = {}) {
|
|
1391
1787
|
const { json = false } = options;
|
|
1392
1788
|
if (!json) {
|
|
@@ -1397,7 +1793,7 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1397
1793
|
return { block: "", files: [], error: "Block name is required. Usage: blockend diff <block>" };
|
|
1398
1794
|
}
|
|
1399
1795
|
const cwd = process.cwd();
|
|
1400
|
-
const configPath = await
|
|
1796
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
1401
1797
|
if (!configPath) {
|
|
1402
1798
|
const error = "blockend.json not found. Run 'npx blockend init' first.";
|
|
1403
1799
|
if (json) {
|
|
@@ -1407,10 +1803,10 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1407
1803
|
}
|
|
1408
1804
|
return { block: blockName, files: [], error };
|
|
1409
1805
|
}
|
|
1410
|
-
const rootDir =
|
|
1806
|
+
const rootDir = path9.dirname(configPath);
|
|
1411
1807
|
let config;
|
|
1412
1808
|
try {
|
|
1413
|
-
config = JSON.parse(await
|
|
1809
|
+
config = JSON.parse(await fs9.readFile(configPath, "utf-8"));
|
|
1414
1810
|
} catch {
|
|
1415
1811
|
const error = "Failed to parse blockend.json.";
|
|
1416
1812
|
if (json) {
|
|
@@ -1421,15 +1817,14 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1421
1817
|
return { block: blockName, files: [], error };
|
|
1422
1818
|
}
|
|
1423
1819
|
const physicalBlocksPath = config.paths.blocks;
|
|
1424
|
-
const blocksRootAbsolute =
|
|
1820
|
+
const blocksRootAbsolute = path9.resolve(rootDir, physicalBlocksPath);
|
|
1425
1821
|
const envKey = config.environment;
|
|
1822
|
+
const source = createGitHubRegistrySource();
|
|
1426
1823
|
const s = spinner5();
|
|
1427
1824
|
if (!json) s.start("Fetching registry...");
|
|
1428
1825
|
let registry;
|
|
1429
1826
|
try {
|
|
1430
|
-
|
|
1431
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1432
|
-
registry = await res.json();
|
|
1827
|
+
registry = await loadRegistry(source);
|
|
1433
1828
|
if (!json) s.stop("Registry synced.");
|
|
1434
1829
|
} catch (err) {
|
|
1435
1830
|
const error = `Failed to fetch registry: ${String(err)}`;
|
|
@@ -1439,103 +1834,32 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1439
1834
|
}
|
|
1440
1835
|
return { block: blockName, files: [], error };
|
|
1441
1836
|
}
|
|
1442
|
-
const
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
outro6(pc7.dim("Block not found."));
|
|
1451
|
-
}
|
|
1452
|
-
return { block: blockName, files: [], error };
|
|
1453
|
-
}
|
|
1454
|
-
if (!blockSupportsEnv2(blockMeta, envKey)) {
|
|
1455
|
-
const error = `Block "${blockName}" does not support environment: ${envKey}`;
|
|
1456
|
-
if (json) {
|
|
1457
|
-
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1458
|
-
} else {
|
|
1459
|
-
log3.error(error);
|
|
1460
|
-
}
|
|
1461
|
-
return { block: blockName, files: [], error };
|
|
1462
|
-
}
|
|
1463
|
-
const adapterContext = resolveAdapterContext2(blockMeta, envKey);
|
|
1464
|
-
if (!adapterContext) {
|
|
1465
|
-
const error = `Block "${blockName}" has no adapter for: ${envKey}`;
|
|
1466
|
-
if (json) {
|
|
1467
|
-
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1468
|
-
} else {
|
|
1469
|
-
log3.error(error);
|
|
1470
|
-
}
|
|
1471
|
-
return { block: blockName, files: [], error };
|
|
1472
|
-
}
|
|
1473
|
-
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
1474
|
-
if (variantKeys.length === 0) {
|
|
1475
|
-
const error = `No variants found for "${blockName}" / "${envKey}".`;
|
|
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;
|
|
1476
1845
|
if (json) {
|
|
1477
1846
|
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1478
1847
|
} else {
|
|
1479
1848
|
log3.error(error);
|
|
1849
|
+
if (planResult.error.code === "not-found") outro6(pc7.dim("Block not found."));
|
|
1480
1850
|
}
|
|
1481
1851
|
return { block: blockName, files: [], error };
|
|
1482
1852
|
}
|
|
1483
|
-
const
|
|
1484
|
-
const installedRecord = config.installed?.find((b) => b.name === blockName);
|
|
1485
|
-
const preferred = await resolvePreferredVariant(
|
|
1486
|
-
variantKeys,
|
|
1487
|
-
installedRecord?.variant,
|
|
1488
|
-
targetFolder
|
|
1489
|
-
);
|
|
1490
|
-
const selectedVariant = selectVariant(variantKeys, preferred);
|
|
1491
|
-
const variantMeta = adapterContext.variants[selectedVariant];
|
|
1492
|
-
const filesToDownload = [];
|
|
1493
|
-
if (blockMeta.baseFiles?.length) {
|
|
1494
|
-
filesToDownload.push(...blockMeta.baseFiles);
|
|
1495
|
-
}
|
|
1496
|
-
if (adapterContext.core) {
|
|
1497
|
-
filesToDownload.push({
|
|
1498
|
-
source: adapterContext.core,
|
|
1499
|
-
target: path4.basename(adapterContext.core)
|
|
1500
|
-
});
|
|
1501
|
-
}
|
|
1502
|
-
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
1503
|
-
for (const entry of variantMeta.files) {
|
|
1504
|
-
filesToDownload.push(
|
|
1505
|
-
typeof entry === "string" ? { source: entry, target: path4.basename(entry) } : entry
|
|
1506
|
-
);
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1853
|
+
const plan = planResult.plan;
|
|
1509
1854
|
if (!json) s.start(`Comparing ${blockName} files...`);
|
|
1510
1855
|
const diffFiles = [];
|
|
1511
1856
|
try {
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
if (!res.ok) throw new Error(`Download failed: ${fm.source} (HTTP ${res.status})`);
|
|
1515
|
-
const newContent = await res.text();
|
|
1516
|
-
const targetPath = path4.join(targetFolder, fm.target);
|
|
1517
|
-
let existingContent = null;
|
|
1518
|
-
let status = "new";
|
|
1519
|
-
if (await fileExists(targetPath)) {
|
|
1520
|
-
try {
|
|
1521
|
-
existingContent = await fs5.readFile(targetPath, "utf-8");
|
|
1522
|
-
status = existingContent === newContent ? "unchanged" : "modified";
|
|
1523
|
-
} catch {
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
diffFiles.push({
|
|
1527
|
-
name: fm.target,
|
|
1528
|
-
sourcePath: fm.source,
|
|
1529
|
-
targetPath,
|
|
1530
|
-
existingContent,
|
|
1531
|
-
newContent,
|
|
1532
|
-
status
|
|
1533
|
-
});
|
|
1534
|
-
}
|
|
1857
|
+
const previews = await previewBlock(plan, { source, targetFolder });
|
|
1858
|
+
diffFiles.push(...previews);
|
|
1535
1859
|
if (!json) s.stop("Comparison complete.");
|
|
1536
1860
|
if (!json) {
|
|
1537
1861
|
console.log(`
|
|
1538
|
-
${pc7.bold(pc7.cyan(blockName))} \u2014 ${pc7.dim(
|
|
1862
|
+
${pc7.bold(pc7.cyan(blockName))} \u2014 ${pc7.dim(plan.variant)} variant
|
|
1539
1863
|
`);
|
|
1540
1864
|
for (const file of diffFiles) {
|
|
1541
1865
|
const icon = file.status === "new" ? pc7.green("+") : file.status === "modified" ? pc7.yellow("~") : pc7.dim("=");
|
|
@@ -1563,11 +1887,13 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1563
1887
|
}
|
|
1564
1888
|
}
|
|
1565
1889
|
console.log("");
|
|
1566
|
-
const newCount = diffFiles.filter((
|
|
1567
|
-
const modCount = diffFiles.filter((
|
|
1568
|
-
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;
|
|
1569
1893
|
outro6(
|
|
1570
|
-
`${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
|
+
)}`
|
|
1571
1897
|
);
|
|
1572
1898
|
}
|
|
1573
1899
|
const result = { block: blockName, files: diffFiles };
|
|
@@ -1590,8 +1916,8 @@ async function diffCommand(blockName, options = {}) {
|
|
|
1590
1916
|
}
|
|
1591
1917
|
|
|
1592
1918
|
// src/commands/doctor.ts
|
|
1593
|
-
import
|
|
1594
|
-
import
|
|
1919
|
+
import path10, { join as join11, dirname as dirname5 } from "path";
|
|
1920
|
+
import fs10 from "fs/promises";
|
|
1595
1921
|
import { existsSync as existsSync5 } from "fs";
|
|
1596
1922
|
import { intro as intro4, outro as outro7, spinner as spinner6 } from "@clack/prompts";
|
|
1597
1923
|
import pc8 from "picocolors";
|
|
@@ -1622,15 +1948,15 @@ function outputDoctorResult(json, result) {
|
|
|
1622
1948
|
}
|
|
1623
1949
|
}
|
|
1624
1950
|
}
|
|
1625
|
-
async function
|
|
1951
|
+
async function findUp2(filename, startDir) {
|
|
1626
1952
|
let dir = startDir;
|
|
1627
1953
|
while (true) {
|
|
1628
1954
|
const candidate = join11(dir, filename);
|
|
1629
1955
|
try {
|
|
1630
|
-
await
|
|
1956
|
+
await fs10.access(candidate);
|
|
1631
1957
|
return candidate;
|
|
1632
1958
|
} catch {
|
|
1633
|
-
const parent =
|
|
1959
|
+
const parent = dirname5(dir);
|
|
1634
1960
|
if (parent === dir) break;
|
|
1635
1961
|
dir = parent;
|
|
1636
1962
|
}
|
|
@@ -1654,7 +1980,7 @@ async function doctorCommand(options = {}) {
|
|
|
1654
1980
|
}
|
|
1655
1981
|
const s = spinner6();
|
|
1656
1982
|
if (!json) s.start("Scanning project health...");
|
|
1657
|
-
const packageJsonPath = await
|
|
1983
|
+
const packageJsonPath = await findUp2("package.json", cwd);
|
|
1658
1984
|
if (packageJsonPath) {
|
|
1659
1985
|
addCheck({ name: "package.json", status: "ok", message: "Found" });
|
|
1660
1986
|
} else {
|
|
@@ -1664,7 +1990,7 @@ async function doctorCommand(options = {}) {
|
|
|
1664
1990
|
message: "Not found. Run 'npm init' or 'pnpm init' first."
|
|
1665
1991
|
});
|
|
1666
1992
|
}
|
|
1667
|
-
const configPath = await
|
|
1993
|
+
const configPath = await findUp2("blockend.json", cwd);
|
|
1668
1994
|
if (configPath) {
|
|
1669
1995
|
addCheck({ name: "blockend.json", status: "ok", message: "Found" });
|
|
1670
1996
|
} else {
|
|
@@ -1674,7 +2000,7 @@ async function doctorCommand(options = {}) {
|
|
|
1674
2000
|
message: "Not found. Run 'npx blockend init' first."
|
|
1675
2001
|
});
|
|
1676
2002
|
}
|
|
1677
|
-
const tsConfigPath = await
|
|
2003
|
+
const tsConfigPath = await findUp2("tsconfig.json", cwd);
|
|
1678
2004
|
if (tsConfigPath) {
|
|
1679
2005
|
addCheck({ name: "tsconfig.json", status: "ok", message: "Found" });
|
|
1680
2006
|
} else {
|
|
@@ -1736,7 +2062,7 @@ async function doctorCommand(options = {}) {
|
|
|
1736
2062
|
}
|
|
1737
2063
|
if (configPath) {
|
|
1738
2064
|
try {
|
|
1739
|
-
const raw = await
|
|
2065
|
+
const raw = await fs10.readFile(configPath, "utf-8");
|
|
1740
2066
|
const config = JSON.parse(raw);
|
|
1741
2067
|
if (!config.environment) {
|
|
1742
2068
|
addCheck({
|
|
@@ -1758,7 +2084,7 @@ async function doctorCommand(options = {}) {
|
|
|
1758
2084
|
message: "Missing 'paths.blocks' in blockend.json"
|
|
1759
2085
|
});
|
|
1760
2086
|
} else {
|
|
1761
|
-
const blocksDir =
|
|
2087
|
+
const blocksDir = path10.resolve(dirname5(configPath), config.paths.blocks);
|
|
1762
2088
|
if (existsSync5(blocksDir)) {
|
|
1763
2089
|
addCheck({
|
|
1764
2090
|
name: "Config: blocks directory",
|
|
@@ -1791,39 +2117,10 @@ async function doctorCommand(options = {}) {
|
|
|
1791
2117
|
}
|
|
1792
2118
|
|
|
1793
2119
|
// src/commands/update.ts
|
|
1794
|
-
import
|
|
1795
|
-
import
|
|
2120
|
+
import path11 from "path";
|
|
2121
|
+
import fs11 from "fs/promises";
|
|
1796
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";
|
|
1797
2123
|
import pc9 from "picocolors";
|
|
1798
|
-
var REPO_OWNER4 = "codewithnuh";
|
|
1799
|
-
var REPO_NAME4 = "blockend";
|
|
1800
|
-
var BRANCH4 = "master";
|
|
1801
|
-
var RAW_CDN_BASE3 = `https://raw.githubusercontent.com/${REPO_OWNER4}/${REPO_NAME4}/${BRANCH4}`;
|
|
1802
|
-
var MANIFEST_URL4 = `${RAW_CDN_BASE3}/registry/index.json`;
|
|
1803
|
-
function blockSupportsEnv3(block, envKey) {
|
|
1804
|
-
if (block.frameworks && block.frameworks.length > 0) {
|
|
1805
|
-
return block.frameworks.includes("*") || block.frameworks.includes(envKey);
|
|
1806
|
-
}
|
|
1807
|
-
return block.adapters?.[envKey] !== void 0 || block.environments?.[envKey] !== void 0;
|
|
1808
|
-
}
|
|
1809
|
-
function resolveAdapterContext3(block, envKey) {
|
|
1810
|
-
return block.adapters?.[envKey] ?? block.adapters?.["*"] ?? block.environments?.[envKey] ?? block.environments?.["*"] ?? null;
|
|
1811
|
-
}
|
|
1812
|
-
async function findUp5(filename, startDir) {
|
|
1813
|
-
let dir = startDir;
|
|
1814
|
-
while (true) {
|
|
1815
|
-
const candidate = join12(dir, filename);
|
|
1816
|
-
try {
|
|
1817
|
-
await fs7.access(candidate);
|
|
1818
|
-
return candidate;
|
|
1819
|
-
} catch {
|
|
1820
|
-
const parent = dirname5(dir);
|
|
1821
|
-
if (parent === dir) break;
|
|
1822
|
-
dir = parent;
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
return null;
|
|
1826
|
-
}
|
|
1827
2124
|
function computeLineDiff(oldContent, newContent) {
|
|
1828
2125
|
const oldLines = oldContent.split("\n");
|
|
1829
2126
|
const newLines = newContent.split("\n");
|
|
@@ -1915,7 +2212,9 @@ function outputUpdateResult(json, result) {
|
|
|
1915
2212
|
`);
|
|
1916
2213
|
for (const block of updates) {
|
|
1917
2214
|
console.log(
|
|
1918
|
-
` ${pc9.yellow("~")} ${pc9.bold(block.name)} ${pc9.dim(
|
|
2215
|
+
` ${pc9.yellow("~")} ${pc9.bold(block.name)} ${pc9.dim(
|
|
2216
|
+
`${block.localVersion} \u2192 ${block.remoteVersion}`
|
|
2217
|
+
)}`
|
|
1919
2218
|
);
|
|
1920
2219
|
}
|
|
1921
2220
|
console.log("");
|
|
@@ -1944,31 +2243,6 @@ function outputUpdateResult(json, result) {
|
|
|
1944
2243
|
outro8(pc9.green("All tracked blocks are up to date."));
|
|
1945
2244
|
}
|
|
1946
2245
|
}
|
|
1947
|
-
async function resolveBlockFiles(blockMeta, envKey, preferredVariant, targetFolder) {
|
|
1948
|
-
const adapterContext = resolveAdapterContext3(blockMeta, envKey);
|
|
1949
|
-
if (!adapterContext) return null;
|
|
1950
|
-
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
1951
|
-
if (variantKeys.length === 0) return null;
|
|
1952
|
-
let preferred = preferredVariant;
|
|
1953
|
-
if (targetFolder && (!preferred || !variantKeys.includes(preferred))) {
|
|
1954
|
-
preferred = await resolvePreferredVariant(variantKeys, preferred, targetFolder) ?? preferred;
|
|
1955
|
-
}
|
|
1956
|
-
const selectedVariant = selectVariant(variantKeys, preferred);
|
|
1957
|
-
const variantMeta = adapterContext.variants[selectedVariant];
|
|
1958
|
-
const expectedFiles = [];
|
|
1959
|
-
if (blockMeta.baseFiles?.length) expectedFiles.push(...blockMeta.baseFiles);
|
|
1960
|
-
if (adapterContext.core) {
|
|
1961
|
-
expectedFiles.push({ source: adapterContext.core, target: path6.basename(adapterContext.core) });
|
|
1962
|
-
}
|
|
1963
|
-
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
1964
|
-
for (const entry of variantMeta.files) {
|
|
1965
|
-
expectedFiles.push(
|
|
1966
|
-
typeof entry === "string" ? { source: entry, target: path6.basename(entry) } : entry
|
|
1967
|
-
);
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
return { files: expectedFiles, variant: selectedVariant };
|
|
1971
|
-
}
|
|
1972
2246
|
async function updateCommand(options = {}) {
|
|
1973
2247
|
const { json = false, diff: diff2 = true, apply = false } = options;
|
|
1974
2248
|
if (!json) {
|
|
@@ -1976,17 +2250,17 @@ async function updateCommand(options = {}) {
|
|
|
1976
2250
|
intro5(`${pc9.bgCyan(pc9.black(" blockend "))} ${pc9.dim("update")}`);
|
|
1977
2251
|
}
|
|
1978
2252
|
const cwd = process.cwd();
|
|
1979
|
-
const configPath = await
|
|
2253
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
1980
2254
|
if (!configPath) {
|
|
1981
2255
|
const error = "blockend.json not found. Run 'npx blockend init' first.";
|
|
1982
2256
|
if (json) process.stdout.write(JSON.stringify({ blocks: [], error }) + "\n");
|
|
1983
2257
|
else log4.error(error);
|
|
1984
2258
|
return { blocks: [], error };
|
|
1985
2259
|
}
|
|
1986
|
-
const rootDir =
|
|
2260
|
+
const rootDir = path11.dirname(configPath);
|
|
1987
2261
|
let config;
|
|
1988
2262
|
try {
|
|
1989
|
-
config = JSON.parse(await
|
|
2263
|
+
config = JSON.parse(await fs11.readFile(configPath, "utf-8"));
|
|
1990
2264
|
} catch {
|
|
1991
2265
|
const error = "Failed to parse blockend.json.";
|
|
1992
2266
|
if (json) process.stdout.write(JSON.stringify({ blocks: [], error }) + "\n");
|
|
@@ -1994,18 +2268,17 @@ async function updateCommand(options = {}) {
|
|
|
1994
2268
|
return { blocks: [], error };
|
|
1995
2269
|
}
|
|
1996
2270
|
const physicalBlocksPath = config.paths.blocks;
|
|
1997
|
-
const blocksRootAbsolute =
|
|
2271
|
+
const blocksRootAbsolute = path11.resolve(rootDir, physicalBlocksPath);
|
|
1998
2272
|
const envKey = config.environment;
|
|
1999
2273
|
const aliasBase = config.aliases.blocks;
|
|
2000
2274
|
const rewriteStrategy = config.importRewriteStrategy ?? "remove";
|
|
2001
2275
|
const installedRecords = Array.isArray(config.installed) ? config.installed : [];
|
|
2276
|
+
const source = createGitHubRegistrySource();
|
|
2002
2277
|
const s = spinner7();
|
|
2003
2278
|
if (!json) s.start("Fetching registry...");
|
|
2004
2279
|
let registry;
|
|
2005
2280
|
try {
|
|
2006
|
-
|
|
2007
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2008
|
-
registry = await res.json();
|
|
2281
|
+
registry = await loadRegistry(source);
|
|
2009
2282
|
if (!json) s.stop("Registry synced.");
|
|
2010
2283
|
} catch (err) {
|
|
2011
2284
|
const error = `Failed to fetch registry: ${String(err)}`;
|
|
@@ -2015,14 +2288,17 @@ async function updateCommand(options = {}) {
|
|
|
2015
2288
|
}
|
|
2016
2289
|
return { blocks: [], error };
|
|
2017
2290
|
}
|
|
2018
|
-
const blockMap = registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks) ? registry.blocks : registry;
|
|
2019
2291
|
if (!json) s.start("Comparing versions...");
|
|
2020
2292
|
const blockResults = [];
|
|
2021
2293
|
const diffs = {};
|
|
2294
|
+
const plans = {};
|
|
2022
2295
|
for (const record of installedRecords) {
|
|
2023
|
-
const
|
|
2024
|
-
const
|
|
2025
|
-
|
|
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) {
|
|
2026
2302
|
blockResults.push({
|
|
2027
2303
|
name: record.name,
|
|
2028
2304
|
localVersion: record.version,
|
|
@@ -2031,6 +2307,9 @@ async function updateCommand(options = {}) {
|
|
|
2031
2307
|
});
|
|
2032
2308
|
continue;
|
|
2033
2309
|
}
|
|
2310
|
+
const plan = planResult.plan;
|
|
2311
|
+
plans[record.name] = plan;
|
|
2312
|
+
const remoteVersion = plan.block.version ?? "0.0.0";
|
|
2034
2313
|
const hasUpdate = record.version !== remoteVersion || record.contentHash === "0";
|
|
2035
2314
|
blockResults.push({
|
|
2036
2315
|
name: record.name,
|
|
@@ -2039,55 +2318,39 @@ async function updateCommand(options = {}) {
|
|
|
2039
2318
|
status: hasUpdate ? "update-available" : "up-to-date"
|
|
2040
2319
|
});
|
|
2041
2320
|
if (hasUpdate && diff2) {
|
|
2042
|
-
const
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
localExists = true;
|
|
2053
|
-
} catch {
|
|
2054
|
-
localContent = "";
|
|
2055
|
-
}
|
|
2056
|
-
let remoteContent = "";
|
|
2057
|
-
try {
|
|
2058
|
-
const res = await fetch(`${RAW_CDN_BASE3}/${fm.source}`);
|
|
2059
|
-
if (res.ok) remoteContent = await res.text();
|
|
2060
|
-
} catch {
|
|
2061
|
-
remoteContent = "";
|
|
2062
|
-
}
|
|
2063
|
-
if (!localExists) {
|
|
2064
|
-
fileDiffs.push({
|
|
2065
|
-
name: fm.target,
|
|
2066
|
-
file: fm.target,
|
|
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,
|
|
2067
2331
|
status: "added",
|
|
2068
|
-
lines:
|
|
2332
|
+
lines: preview.newContent.split("\n").map((line, index) => ({
|
|
2069
2333
|
type: "added",
|
|
2070
|
-
content:
|
|
2071
|
-
newLineNo:
|
|
2334
|
+
content: line,
|
|
2335
|
+
newLineNo: index + 1
|
|
2072
2336
|
}))
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
if (preview.existingContent !== preview.newContent) {
|
|
2340
|
+
return {
|
|
2341
|
+
name: preview.name,
|
|
2342
|
+
file: preview.name,
|
|
2078
2343
|
status: "modified",
|
|
2079
|
-
lines: computeLineDiff(
|
|
2080
|
-
}
|
|
2081
|
-
} else {
|
|
2082
|
-
fileDiffs.push({
|
|
2083
|
-
name: fm.target,
|
|
2084
|
-
file: fm.target,
|
|
2085
|
-
status: "unchanged",
|
|
2086
|
-
lines: []
|
|
2087
|
-
});
|
|
2344
|
+
lines: computeLineDiff(preview.existingContent, preview.newContent)
|
|
2345
|
+
};
|
|
2088
2346
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2347
|
+
return {
|
|
2348
|
+
name: preview.name,
|
|
2349
|
+
file: preview.name,
|
|
2350
|
+
status: "unchanged",
|
|
2351
|
+
lines: []
|
|
2352
|
+
};
|
|
2353
|
+
});
|
|
2091
2354
|
}
|
|
2092
2355
|
}
|
|
2093
2356
|
if (!json) s.stop("Comparison complete.");
|
|
@@ -2124,60 +2387,19 @@ async function updateCommand(options = {}) {
|
|
|
2124
2387
|
}
|
|
2125
2388
|
const applied = [];
|
|
2126
2389
|
for (const blockName of selectedNames) {
|
|
2127
|
-
const
|
|
2128
|
-
if (!
|
|
2129
|
-
const record = installedRecords.find((r) => r.name === blockName);
|
|
2130
|
-
const targetFolder = path6.resolve(blocksRootAbsolute, blockName);
|
|
2131
|
-
const resolved = await resolveBlockFiles(blockMeta, envKey, record?.variant, targetFolder);
|
|
2132
|
-
if (!resolved) continue;
|
|
2133
|
-
const downloadedTargets = /* @__PURE__ */ new Set();
|
|
2390
|
+
const plan = plans[blockName];
|
|
2391
|
+
if (!plan) continue;
|
|
2134
2392
|
try {
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
}
|
|
2142
|
-
let contentHash = 0;
|
|
2143
|
-
for (const { fm, raw } of fileBatches) {
|
|
2144
|
-
const writtenFilePath = path6.join(targetFolder, fm.target);
|
|
2145
|
-
const content = rewriteFileImports(
|
|
2146
|
-
raw,
|
|
2147
|
-
writtenFilePath,
|
|
2148
|
-
blocksRootAbsolute,
|
|
2149
|
-
aliasBase,
|
|
2150
|
-
rewriteStrategy,
|
|
2151
|
-
downloadedTargets
|
|
2152
|
-
);
|
|
2153
|
-
await fs7.mkdir(dirname5(writtenFilePath), { recursive: true });
|
|
2154
|
-
await fs7.writeFile(writtenFilePath, content, "utf-8");
|
|
2155
|
-
for (let i = 0; i < content.length; i++) {
|
|
2156
|
-
contentHash = (contentHash << 5) - contentHash + content.charCodeAt(i) | 0;
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
const blockVersion = blockMeta.version ?? "0.0.0";
|
|
2160
|
-
const installedRecord = {
|
|
2161
|
-
name: blockName,
|
|
2162
|
-
version: blockVersion,
|
|
2163
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2164
|
-
files: resolved.files.map((f) => f.target),
|
|
2165
|
-
contentHash: contentHash.toString(16),
|
|
2166
|
-
variant: resolved.variant
|
|
2167
|
-
};
|
|
2168
|
-
try {
|
|
2169
|
-
const cfgRaw = await fs7.readFile(configPath, "utf-8");
|
|
2170
|
-
const cfgData = JSON.parse(cfgRaw);
|
|
2171
|
-
if (!Array.isArray(cfgData.installed)) cfgData.installed = [];
|
|
2172
|
-
cfgData.installed = cfgData.installed.filter(
|
|
2173
|
-
(b) => b.name !== blockName
|
|
2174
|
-
);
|
|
2175
|
-
cfgData.installed.push(installedRecord);
|
|
2176
|
-
await fs7.writeFile(configPath, JSON.stringify(cfgData, null, 2), "utf-8");
|
|
2177
|
-
} catch {
|
|
2178
|
-
}
|
|
2393
|
+
await applyBlock(plan, {
|
|
2394
|
+
source,
|
|
2395
|
+
blocksRoot: blocksRootAbsolute,
|
|
2396
|
+
aliasBase,
|
|
2397
|
+
rewriteStrategy,
|
|
2398
|
+
configPath
|
|
2399
|
+
});
|
|
2179
2400
|
applied.push(blockName);
|
|
2180
|
-
if (!json)
|
|
2401
|
+
if (!json)
|
|
2402
|
+
log4.info(` ${pc9.green("\u2714")} ${blockName} updated to v${plan.block.version ?? "0.0.0"}`);
|
|
2181
2403
|
} catch (err) {
|
|
2182
2404
|
if (!json) log4.error(` ${pc9.red("\u2716")} ${blockName}: ${String(err)}`);
|
|
2183
2405
|
}
|