blockend-cli 1.5.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/add.d.ts +44 -0
- package/dist/commands/detect.d.ts +3 -0
- package/dist/commands/diff.d.ts +16 -0
- package/dist/commands/doctor.d.ts +13 -0
- package/dist/commands/init.d.ts +26 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/mcp.d.ts +8 -0
- package/dist/commands/update.d.ts +29 -0
- package/dist/detectors/framework.d.ts +2 -0
- package/dist/detectors/index.d.ts +2 -0
- package/dist/detectors/package-manager.d.ts +2 -0
- package/dist/detectors/readers/filesystem.d.ts +1 -0
- package/dist/detectors/readers/package.d.ts +10 -0
- package/dist/detectors/readers/read-template.d.ts +1 -0
- package/dist/detectors/readers/tsconfig.d.ts +10 -0
- package/dist/detectors/runtime.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1298 -139
- package/dist/types/index.d.ts +1 -0
- package/dist/types/types.d.ts +45 -0
- package/dist/ui/format.d.ts +9 -0
- package/dist/ui/theme.d.ts +22 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,9 +5,9 @@ import { defineCommand, runMain } from "citty";
|
|
|
5
5
|
|
|
6
6
|
// src/commands/add.ts
|
|
7
7
|
import path, { join, dirname } from "path";
|
|
8
|
-
import fs
|
|
8
|
+
import fs from "fs/promises";
|
|
9
9
|
import { exec } from "child_process";
|
|
10
|
-
import { intro, outro, select, spinner, confirm, isCancel, log } from "@clack/prompts";
|
|
10
|
+
import { intro, outro, select, multiselect, spinner, confirm, isCancel, log } from "@clack/prompts";
|
|
11
11
|
import pc from "picocolors";
|
|
12
12
|
var REPO_OWNER = "codewithnuh";
|
|
13
13
|
var REPO_NAME = "blockend";
|
|
@@ -20,17 +20,85 @@ function blockSupportsEnv(block, envKey) {
|
|
|
20
20
|
}
|
|
21
21
|
return block.adapters?.[envKey] !== void 0 || block.environments?.[envKey] !== void 0;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
function parseDepSpec(dep) {
|
|
24
|
+
const atIndex = dep.lastIndexOf("@");
|
|
25
|
+
if (atIndex > 0) {
|
|
26
|
+
return { name: dep.slice(0, atIndex), versionRange: dep.slice(atIndex + 1) };
|
|
27
|
+
}
|
|
28
|
+
return { name: dep, versionRange: null };
|
|
29
|
+
}
|
|
30
|
+
function semverSatisfies(installed, range) {
|
|
31
|
+
const parse = (v) => {
|
|
32
|
+
const clean = v.replace(/^[~^>=<]+/, "");
|
|
33
|
+
const parts = clean.split(".").map(Number);
|
|
34
|
+
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
|
|
35
|
+
};
|
|
36
|
+
const [iMajor, iMinor, iPatch] = parse(installed);
|
|
37
|
+
const raw = range.replace(/^[~^>=<]+/, "");
|
|
38
|
+
const [rMajor, rMinor, rPatch] = parse(raw);
|
|
39
|
+
if (range.startsWith("^")) {
|
|
40
|
+
return (iMajor > rMajor || iMajor === rMajor && (iMinor > rMinor || iMinor === rMinor && iPatch >= rPatch)) && iMajor === rMajor;
|
|
41
|
+
}
|
|
42
|
+
if (range.startsWith("~")) {
|
|
43
|
+
return iMajor === rMajor && iMinor === rMinor && iPatch >= rPatch;
|
|
44
|
+
}
|
|
45
|
+
if (range.startsWith(">=")) {
|
|
46
|
+
return iMajor > rMajor || iMajor === rMajor && (iMinor > rMinor || iMinor === rMinor && iPatch >= rPatch);
|
|
47
|
+
}
|
|
48
|
+
return installed === raw;
|
|
49
|
+
}
|
|
50
|
+
function getVersionConflicts(deps, installed) {
|
|
51
|
+
const conflicts = [];
|
|
52
|
+
for (const dep of deps) {
|
|
53
|
+
const { name, versionRange } = parseDepSpec(dep);
|
|
54
|
+
if (versionRange && name in installed) {
|
|
55
|
+
const installedVersion = installed[name];
|
|
56
|
+
if (!semverSatisfies(installedVersion, versionRange)) {
|
|
57
|
+
conflicts.push(`${name}: block requires ${versionRange}, you have ${installedVersion}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return conflicts;
|
|
62
|
+
}
|
|
63
|
+
var getMissingDependencies = (deps, installed, forceLatest = false) => {
|
|
64
|
+
const seen = /* @__PURE__ */ new Set();
|
|
65
|
+
const result = [];
|
|
66
|
+
for (const dep of deps) {
|
|
67
|
+
const { name, versionRange } = parseDepSpec(dep);
|
|
68
|
+
if (seen.has(name)) continue;
|
|
69
|
+
seen.add(name);
|
|
70
|
+
if (!(name in installed)) {
|
|
71
|
+
result.push(dep);
|
|
72
|
+
} else if (versionRange && !semverSatisfies(installed[name], versionRange)) {
|
|
73
|
+
result.push(dep);
|
|
74
|
+
} else if (!versionRange && forceLatest) {
|
|
75
|
+
result.push(`${name}@latest`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
};
|
|
24
80
|
function resolveAdapterContext(block, envKey) {
|
|
25
81
|
return block.adapters?.[envKey] ?? block.adapters?.["*"] ?? block.environments?.[envKey] ?? block.environments?.["*"] ?? null;
|
|
26
82
|
}
|
|
27
|
-
function
|
|
83
|
+
function resolveIndexImport(importPath, sourceFileDir, downloadedFiles) {
|
|
84
|
+
const resolved = path.resolve(sourceFileDir, importPath);
|
|
85
|
+
for (const downloaded of downloadedFiles) {
|
|
86
|
+
const downloadedAbs = path.resolve(sourceFileDir, downloaded);
|
|
87
|
+
const dirOfDownloaded = dirname(downloadedAbs);
|
|
88
|
+
const basenameOfDownloaded = path.basename(downloadedAbs);
|
|
89
|
+
if (dirOfDownloaded === resolved && basenameOfDownloaded.startsWith("index.")) {
|
|
90
|
+
return `${importPath}/index`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function rewriteFileImports(fileContent, writtenFilePath, blocksRoot, aliasBase, strategy, downloadedFiles = /* @__PURE__ */ new Set()) {
|
|
28
96
|
const importRegex = /((?:from|import)\s*)(['"`])(\.\.?\/[^'"` \n\r]+)(['"`])/g;
|
|
29
97
|
const fileDir = dirname(writtenFilePath);
|
|
30
|
-
return fileContent.replace(importRegex, (match, keyword, openQuote,
|
|
31
|
-
let targetPath =
|
|
98
|
+
return fileContent.replace(importRegex, (match, keyword, openQuote, importPath, closeQuote) => {
|
|
99
|
+
let targetPath = importPath;
|
|
32
100
|
if (aliasBase && !aliasBase.startsWith(".")) {
|
|
33
|
-
const absoluteTarget = path.resolve(fileDir,
|
|
101
|
+
const absoluteTarget = path.resolve(fileDir, importPath);
|
|
34
102
|
const fromBlocksRoot = path.relative(blocksRoot, absoluteTarget).replace(/\\/g, "/");
|
|
35
103
|
if (!fromBlocksRoot.startsWith("..")) {
|
|
36
104
|
targetPath = `${aliasBase}/${fromBlocksRoot}`.replace(/\/+/g, "/");
|
|
@@ -39,6 +107,12 @@ function rewriteFileImports(fileContent, writtenFilePath, blocksRoot, aliasBase,
|
|
|
39
107
|
const hasJsExt = targetPath.endsWith(".js");
|
|
40
108
|
if (strategy === "rewrite" && !hasJsExt) {
|
|
41
109
|
targetPath = targetPath.replace(/\.(ts|tsx|mts|cts)$/, "");
|
|
110
|
+
if (downloadedFiles.size > 0) {
|
|
111
|
+
const indexResolved = resolveIndexImport(targetPath, fileDir, downloadedFiles);
|
|
112
|
+
if (indexResolved) {
|
|
113
|
+
targetPath = indexResolved;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
42
116
|
targetPath = `${targetPath}.js`;
|
|
43
117
|
} else if (strategy === "remove" && hasJsExt) {
|
|
44
118
|
targetPath = targetPath.slice(0, -3);
|
|
@@ -81,87 +155,17 @@ async function findUp(filename, startDir) {
|
|
|
81
155
|
}
|
|
82
156
|
return null;
|
|
83
157
|
}
|
|
84
|
-
async function
|
|
158
|
+
async function addSingleBlock(targetBlock, blockMap, config, rootDir, physicalBlocksPath, blocksRootAbsolute, aliasBase, envKey, rewriteStrategy, options) {
|
|
85
159
|
const { yes = false, json = false } = options;
|
|
86
|
-
if (!json) {
|
|
87
|
-
console.log("");
|
|
88
|
-
intro(`${pc.bgCyan(pc.black(" blockend "))} ${pc.dim("add")}`);
|
|
89
|
-
}
|
|
90
|
-
const cwd = process.cwd();
|
|
91
|
-
const configPath = await findUp("blockend.json", cwd);
|
|
92
|
-
if (!configPath) {
|
|
93
|
-
outputError(json, "blockend.json not found. Run 'npx blockend init' first.");
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
const rootDir = dirname(configPath);
|
|
97
|
-
let config;
|
|
98
|
-
try {
|
|
99
|
-
config = JSON.parse(await fs.readFile(configPath, "utf-8"));
|
|
100
|
-
} catch (err) {
|
|
101
|
-
outputError(json, "Failed to parse blockend.json.");
|
|
102
|
-
if (!json) log.error(pc.dim(String(err)));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (config.language !== "typescript") {
|
|
106
|
-
outputError(json, "Blockend currently only supports TypeScript projects.");
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const physicalBlocksPath = config.paths.blocks;
|
|
110
|
-
const blocksRootAbsolute = path.resolve(rootDir, physicalBlocksPath);
|
|
111
|
-
const aliasBase = config.aliases.blocks;
|
|
112
|
-
const envKey = config.environment;
|
|
113
|
-
const rewriteStrategy = config.importRewriteStrategy ?? "remove";
|
|
114
160
|
const s = spinner();
|
|
115
|
-
if (!json) s.start("Fetching registry...");
|
|
116
|
-
let registry;
|
|
117
|
-
try {
|
|
118
|
-
const res = await fetch(MANIFEST_URL);
|
|
119
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
120
|
-
registry = await res.json();
|
|
121
|
-
if (!json) s.stop("Registry synced.");
|
|
122
|
-
} catch (err) {
|
|
123
|
-
if (!json) {
|
|
124
|
-
s.stop("Failed to fetch registry.");
|
|
125
|
-
log.error(pc.dim(String(err)));
|
|
126
|
-
} else outputError(json, "Failed to fetch registry from GitHub.");
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const blockMap = registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks) ? registry.blocks : registry;
|
|
130
|
-
if (yes && !blockName) {
|
|
131
|
-
outputError(json, "Block name required with --yes. Usage: blockend add <block> --yes");
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
let targetBlock = blockName;
|
|
135
|
-
if (!targetBlock) {
|
|
136
|
-
const available = Object.entries(blockMap).filter(
|
|
137
|
-
([, block]) => block != null && blockSupportsEnv(block, envKey)
|
|
138
|
-
);
|
|
139
|
-
if (available.length === 0) {
|
|
140
|
-
outputError(json, `No blocks available for environment: ${envKey}`);
|
|
141
|
-
if (!json) outro(pc.dim("Exiting."));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const maxKeyLen = Math.max(...available.map(([k]) => k.length), 0);
|
|
145
|
-
const selectPrompt = await select({
|
|
146
|
-
message: "Which block would you like to add?",
|
|
147
|
-
options: available.map(([key, block], i) => ({
|
|
148
|
-
value: key,
|
|
149
|
-
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(pc.cyan(key.padEnd(maxKeyLen + 4)))}${pc.dim(
|
|
150
|
-
block.description.length > 60 ? `${block.description.slice(0, 60)}...` : block.description
|
|
151
|
-
)}`
|
|
152
|
-
}))
|
|
153
|
-
});
|
|
154
|
-
handleCancel(selectPrompt);
|
|
155
|
-
targetBlock = selectPrompt;
|
|
156
|
-
}
|
|
157
161
|
const blockMeta = blockMap[targetBlock];
|
|
158
162
|
if (!blockMeta) {
|
|
159
163
|
outputError(json, `Block "${targetBlock}" not found in the registry.`);
|
|
160
|
-
return;
|
|
164
|
+
return { success: false, filesWritten: [], message: `Block "${targetBlock}" not found` };
|
|
161
165
|
}
|
|
162
166
|
if (!blockSupportsEnv(blockMeta, envKey)) {
|
|
163
167
|
outputError(json, `Block "${targetBlock}" does not support environment: ${envKey}`);
|
|
164
|
-
return;
|
|
168
|
+
return { success: false, filesWritten: [], message: `Block "${targetBlock}" incompatible` };
|
|
165
169
|
}
|
|
166
170
|
const adapterContext = resolveAdapterContext(blockMeta, envKey);
|
|
167
171
|
if (!adapterContext) {
|
|
@@ -169,12 +173,12 @@ async function addCommand(blockName, options = {}) {
|
|
|
169
173
|
json,
|
|
170
174
|
`Block "${targetBlock}" has no adapter or environment entry for: ${envKey}. This is a registry configuration issue \u2014 please open an issue.`
|
|
171
175
|
);
|
|
172
|
-
return;
|
|
176
|
+
return { success: false, filesWritten: [], message: `No adapter for "${targetBlock}"` };
|
|
173
177
|
}
|
|
174
178
|
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
175
179
|
if (variantKeys.length === 0) {
|
|
176
180
|
outputError(json, `No variants found for "${targetBlock}" / "${envKey}".`);
|
|
177
|
-
return;
|
|
181
|
+
return { success: false, filesWritten: [], message: `No variants for "${targetBlock}"` };
|
|
178
182
|
}
|
|
179
183
|
let selectedVariant;
|
|
180
184
|
if (yes) {
|
|
@@ -185,7 +189,7 @@ async function addCommand(blockName, options = {}) {
|
|
|
185
189
|
selectedVariant = variantKeys[0];
|
|
186
190
|
} else {
|
|
187
191
|
const variantPrompt = await select({
|
|
188
|
-
message:
|
|
192
|
+
message: `Select a storage variant for ${targetBlock}:`,
|
|
189
193
|
options: variantKeys.map((v) => ({ value: v, label: v.toUpperCase() }))
|
|
190
194
|
});
|
|
191
195
|
handleCancel(variantPrompt);
|
|
@@ -196,7 +200,7 @@ async function addCommand(blockName, options = {}) {
|
|
|
196
200
|
const packageJsonPath = await findUp("package.json", rootDir);
|
|
197
201
|
if (!packageJsonPath) {
|
|
198
202
|
outputError(json, "Could not locate package.json.");
|
|
199
|
-
return;
|
|
203
|
+
return { success: false, filesWritten: [], message: "package.json not found" };
|
|
200
204
|
}
|
|
201
205
|
const packageJsonDir = dirname(packageJsonPath);
|
|
202
206
|
let packageJson;
|
|
@@ -205,34 +209,42 @@ async function addCommand(blockName, options = {}) {
|
|
|
205
209
|
} catch (err) {
|
|
206
210
|
outputError(json, "Failed to parse package.json.");
|
|
207
211
|
if (!json) log.error(pc.dim(String(err)));
|
|
208
|
-
return;
|
|
212
|
+
return { success: false, filesWritten: [], message: "Failed to parse package.json" };
|
|
209
213
|
}
|
|
210
214
|
const installedDeps = {
|
|
211
215
|
...packageJson.dependencies ?? {},
|
|
212
216
|
...packageJson.devDependencies ?? {}
|
|
213
217
|
};
|
|
214
|
-
const
|
|
215
|
-
[
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
218
|
+
const allBlockDeps = [
|
|
219
|
+
...blockMeta.dependencies ?? [],
|
|
220
|
+
...adapterContext.dependencies ?? [],
|
|
221
|
+
...variantMeta?.dependencies ?? []
|
|
222
|
+
];
|
|
223
|
+
const allBlockDevDeps = [
|
|
224
|
+
...blockMeta.devDependencies ?? [],
|
|
225
|
+
...adapterContext.devDependencies ?? [],
|
|
226
|
+
...variantMeta?.devDependencies ?? []
|
|
227
|
+
];
|
|
228
|
+
const missingProd = getMissingDependencies(allBlockDeps, installedDeps, true);
|
|
229
|
+
const missingDev = getMissingDependencies(allBlockDevDeps, installedDeps, true);
|
|
230
|
+
const prodConflicts = getVersionConflicts(allBlockDeps, installedDeps);
|
|
231
|
+
const devConflicts = getVersionConflicts(allBlockDevDeps, installedDeps);
|
|
232
|
+
const allConflicts = [...prodConflicts, ...devConflicts];
|
|
233
|
+
if (allConflicts.length > 0 && !json) {
|
|
234
|
+
log.warn(`Version conflicts detected:`);
|
|
235
|
+
for (const conflict of allConflicts) {
|
|
236
|
+
log.warn(` ${pc.yellow("!")} ${conflict}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
230
239
|
if (missingProd.length > 0 || missingDev.length > 0) {
|
|
231
240
|
if (!json) log.warn(`Missing: ${pc.cyan([...missingProd, ...missingDev].join(", "))}`);
|
|
232
|
-
const shouldInstall = yes ? true : await confirm({
|
|
241
|
+
const shouldInstall = yes ? true : await confirm({
|
|
242
|
+
message: `Install missing dependencies for ${targetBlock}?`,
|
|
243
|
+
initialValue: true
|
|
244
|
+
});
|
|
233
245
|
if (!yes) handleCancel(shouldInstall);
|
|
234
246
|
if (shouldInstall) {
|
|
235
|
-
const pm = await readFile(join(packageJsonDir, "blockend.json"), "utf8").then((data) => JSON.parse(data).packageManager).catch(() => "npm");
|
|
247
|
+
const pm = await fs.readFile(join(packageJsonDir, "blockend.json"), "utf8").then((data) => JSON.parse(data).packageManager).catch(() => "npm");
|
|
236
248
|
if (!json) s.start(`Installing via ${pm}...`);
|
|
237
249
|
const tasks = [];
|
|
238
250
|
if (missingProd.length > 0) {
|
|
@@ -268,7 +280,7 @@ async function addCommand(blockName, options = {}) {
|
|
|
268
280
|
s.stop("Installation failed.");
|
|
269
281
|
log.error(pc.dim(String(err)));
|
|
270
282
|
} else outputError(json, "Dependency installation failed.");
|
|
271
|
-
return;
|
|
283
|
+
return { success: false, filesWritten: [], message: "Dependency installation failed" };
|
|
272
284
|
}
|
|
273
285
|
}
|
|
274
286
|
}
|
|
@@ -305,43 +317,215 @@ async function addCommand(blockName, options = {}) {
|
|
|
305
317
|
});
|
|
306
318
|
if (!yes) handleCancel(overwrite);
|
|
307
319
|
if (!overwrite) {
|
|
308
|
-
|
|
309
|
-
return;
|
|
320
|
+
return { success: false, filesWritten: [], message: "Local files preserved." };
|
|
310
321
|
}
|
|
311
322
|
}
|
|
312
323
|
if (!json) s.start(`Downloading ${targetBlock}...`);
|
|
313
324
|
const filesWritten = [];
|
|
325
|
+
const downloadedTargets = /* @__PURE__ */ new Set();
|
|
314
326
|
try {
|
|
327
|
+
const fileBatches = [];
|
|
315
328
|
for (const fm of filesToDownload) {
|
|
316
329
|
const res = await fetch(`${RAW_CDN_BASE}/${fm.source}`);
|
|
317
330
|
if (!res.ok) throw new Error(`Download failed: ${fm.source} (HTTP ${res.status})`);
|
|
318
|
-
|
|
331
|
+
fileBatches.push({ fm, raw: await res.text() });
|
|
332
|
+
downloadedTargets.add(fm.target);
|
|
333
|
+
}
|
|
334
|
+
let contentHash = 0;
|
|
335
|
+
for (const { fm, raw } of fileBatches) {
|
|
319
336
|
const writtenFilePath = path.join(targetFolder, fm.target);
|
|
320
|
-
content = rewriteFileImports(
|
|
321
|
-
|
|
337
|
+
const content = rewriteFileImports(
|
|
338
|
+
raw,
|
|
322
339
|
writtenFilePath,
|
|
323
340
|
blocksRootAbsolute,
|
|
324
341
|
aliasBase,
|
|
325
|
-
rewriteStrategy
|
|
342
|
+
rewriteStrategy,
|
|
343
|
+
downloadedTargets
|
|
326
344
|
);
|
|
327
345
|
await fs.mkdir(dirname(writtenFilePath), { recursive: true });
|
|
328
346
|
await fs.writeFile(writtenFilePath, content, "utf-8");
|
|
329
347
|
filesWritten.push(writtenFilePath);
|
|
348
|
+
for (let i = 0; i < content.length; i++) {
|
|
349
|
+
contentHash = (contentHash << 5) - contentHash + content.charCodeAt(i) | 0;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const blockVersion = blockMeta.version ?? "0.0.0";
|
|
353
|
+
const installedRecord = {
|
|
354
|
+
name: targetBlock,
|
|
355
|
+
version: blockVersion,
|
|
356
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
357
|
+
files: filesWritten.map((f) => path.relative(targetFolder, f).replace(/\\/g, "/")),
|
|
358
|
+
contentHash: contentHash.toString(16)
|
|
359
|
+
};
|
|
360
|
+
try {
|
|
361
|
+
const blockendPath = join(rootDir, "blockend.json");
|
|
362
|
+
const cfgRaw = await fs.readFile(blockendPath, "utf-8");
|
|
363
|
+
const cfgData = JSON.parse(cfgRaw);
|
|
364
|
+
if (!Array.isArray(cfgData.installed)) cfgData.installed = [];
|
|
365
|
+
cfgData.installed = cfgData.installed.filter((b) => b.name !== targetBlock);
|
|
366
|
+
cfgData.installed.push(installedRecord);
|
|
367
|
+
await fs.writeFile(blockendPath, JSON.stringify(cfgData, null, 2), "utf-8");
|
|
368
|
+
} catch {
|
|
330
369
|
}
|
|
331
370
|
if (!json) s.stop("Files written.");
|
|
332
|
-
|
|
371
|
+
return {
|
|
333
372
|
success: true,
|
|
334
|
-
block: targetBlock,
|
|
335
373
|
filesWritten,
|
|
336
|
-
dependenciesInstalled: [...missingProd, ...missingDev],
|
|
337
374
|
message: `${targetBlock} added to ${physicalBlocksPath}/${targetBlock}`
|
|
338
|
-
}
|
|
375
|
+
};
|
|
339
376
|
} catch (err) {
|
|
340
377
|
if (!json) {
|
|
341
378
|
s.stop("Failed.");
|
|
342
379
|
log.error(pc.dim(String(err)));
|
|
343
380
|
} else outputError(json, "Fatal error while writing block files.");
|
|
381
|
+
return { success: false, filesWritten: [], message: `Failed to add ${targetBlock}` };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function addCommand(blockName, options = {}) {
|
|
385
|
+
const { yes = false, json = false, multi = false } = options;
|
|
386
|
+
if (!json) {
|
|
387
|
+
console.log("");
|
|
388
|
+
intro(`${pc.bgCyan(pc.black(" blockend "))} ${pc.dim("add")}`);
|
|
389
|
+
}
|
|
390
|
+
const cwd = process.cwd();
|
|
391
|
+
const configPath = await findUp("blockend.json", cwd);
|
|
392
|
+
if (!configPath) {
|
|
393
|
+
outputError(json, "blockend.json not found. Run 'npx blockend init' first.");
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const rootDir = dirname(configPath);
|
|
397
|
+
let config;
|
|
398
|
+
try {
|
|
399
|
+
config = JSON.parse(await fs.readFile(configPath, "utf-8"));
|
|
400
|
+
} catch (err) {
|
|
401
|
+
outputError(json, "Failed to parse blockend.json.");
|
|
402
|
+
if (!json) log.error(pc.dim(String(err)));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (config.language !== "typescript") {
|
|
406
|
+
outputError(json, "Blockend currently only supports TypeScript projects.");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const physicalBlocksPath = config.paths.blocks;
|
|
410
|
+
const blocksRootAbsolute = path.resolve(rootDir, physicalBlocksPath);
|
|
411
|
+
const aliasBase = config.aliases.blocks;
|
|
412
|
+
const envKey = config.environment;
|
|
413
|
+
const rewriteStrategy = config.importRewriteStrategy ?? "remove";
|
|
414
|
+
const s = spinner();
|
|
415
|
+
if (!json) s.start("Fetching registry...");
|
|
416
|
+
let registry;
|
|
417
|
+
try {
|
|
418
|
+
const res = await fetch(MANIFEST_URL);
|
|
419
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
420
|
+
registry = await res.json();
|
|
421
|
+
if (!json) s.stop("Registry synced.");
|
|
422
|
+
} catch (err) {
|
|
423
|
+
if (!json) {
|
|
424
|
+
s.stop("Failed to fetch registry.");
|
|
425
|
+
log.error(pc.dim(String(err)));
|
|
426
|
+
} else outputError(json, "Failed to fetch registry from GitHub.");
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const blockMap = registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks) ? registry.blocks : registry;
|
|
430
|
+
if (yes && !blockName) {
|
|
431
|
+
outputError(json, "Block name required with --yes. Usage: blockend add <block> --yes");
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (multi && !blockName) {
|
|
435
|
+
const available = Object.entries(blockMap).filter(
|
|
436
|
+
([, block]) => block != null && blockSupportsEnv(block, envKey)
|
|
437
|
+
);
|
|
438
|
+
if (available.length === 0) {
|
|
439
|
+
outputError(json, `No blocks available for environment: ${envKey}`);
|
|
440
|
+
if (!json) outro(pc.dim("Exiting."));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const maxKeyLen = Math.max(...available.map(([k]) => k.length), 0);
|
|
444
|
+
const multiSelectPrompt = await multiselect({
|
|
445
|
+
message: "Select blocks to add (space to select, enter to confirm):",
|
|
446
|
+
options: available.map(([key, block], i) => ({
|
|
447
|
+
value: key,
|
|
448
|
+
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(pc.cyan(key.padEnd(maxKeyLen + 4)))}${pc.dim(
|
|
449
|
+
block.description.length > 60 ? `${block.description.slice(0, 60)}...` : block.description
|
|
450
|
+
)}`
|
|
451
|
+
}))
|
|
452
|
+
});
|
|
453
|
+
handleCancel(multiSelectPrompt);
|
|
454
|
+
const selectedBlocks = multiSelectPrompt;
|
|
455
|
+
if (selectedBlocks.length === 0) {
|
|
456
|
+
outputError(json, "No blocks selected.");
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (!json) log.info(`
|
|
460
|
+
Adding ${selectedBlocks.length} block(s)...
|
|
461
|
+
`);
|
|
462
|
+
const results = [];
|
|
463
|
+
for (const block of selectedBlocks) {
|
|
464
|
+
const result2 = await addSingleBlock(
|
|
465
|
+
block,
|
|
466
|
+
blockMap,
|
|
467
|
+
config,
|
|
468
|
+
rootDir,
|
|
469
|
+
physicalBlocksPath,
|
|
470
|
+
blocksRootAbsolute,
|
|
471
|
+
aliasBase,
|
|
472
|
+
envKey,
|
|
473
|
+
rewriteStrategy,
|
|
474
|
+
options
|
|
475
|
+
);
|
|
476
|
+
if (result2.success) {
|
|
477
|
+
results.push(result2.message);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (json) {
|
|
481
|
+
process.stdout.write(JSON.stringify({ success: true, blocks: results }) + "\n");
|
|
482
|
+
} else {
|
|
483
|
+
outro(pc.green(`
|
|
484
|
+
\u2728 Added ${results.length} block(s) successfully!`));
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
344
487
|
}
|
|
488
|
+
let targetBlock = blockName;
|
|
489
|
+
if (!targetBlock) {
|
|
490
|
+
const available = Object.entries(blockMap).filter(
|
|
491
|
+
([, block]) => block != null && blockSupportsEnv(block, envKey)
|
|
492
|
+
);
|
|
493
|
+
if (available.length === 0) {
|
|
494
|
+
outputError(json, `No blocks available for environment: ${envKey}`);
|
|
495
|
+
if (!json) outro(pc.dim("Exiting."));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const maxKeyLen = Math.max(...available.map(([k]) => k.length), 0);
|
|
499
|
+
const selectPrompt = await select({
|
|
500
|
+
message: "Which block would you like to add?",
|
|
501
|
+
options: available.map(([key, block], i) => ({
|
|
502
|
+
value: key,
|
|
503
|
+
label: `${pc.dim(`${i + 1}.`)} ${pc.bold(pc.cyan(key.padEnd(maxKeyLen + 4)))}${pc.dim(
|
|
504
|
+
block.description.length > 60 ? `${block.description.slice(0, 60)}...` : block.description
|
|
505
|
+
)}`
|
|
506
|
+
}))
|
|
507
|
+
});
|
|
508
|
+
handleCancel(selectPrompt);
|
|
509
|
+
targetBlock = selectPrompt;
|
|
510
|
+
}
|
|
511
|
+
const result = await addSingleBlock(
|
|
512
|
+
targetBlock,
|
|
513
|
+
blockMap,
|
|
514
|
+
config,
|
|
515
|
+
rootDir,
|
|
516
|
+
physicalBlocksPath,
|
|
517
|
+
blocksRootAbsolute,
|
|
518
|
+
aliasBase,
|
|
519
|
+
envKey,
|
|
520
|
+
rewriteStrategy,
|
|
521
|
+
options
|
|
522
|
+
);
|
|
523
|
+
outputResult(json, {
|
|
524
|
+
success: result.success,
|
|
525
|
+
block: targetBlock,
|
|
526
|
+
filesWritten: result.filesWritten,
|
|
527
|
+
message: result.message
|
|
528
|
+
});
|
|
345
529
|
}
|
|
346
530
|
|
|
347
531
|
// src/commands/init.ts
|
|
@@ -355,12 +539,12 @@ import pc3 from "picocolors";
|
|
|
355
539
|
import { join as join6 } from "path";
|
|
356
540
|
|
|
357
541
|
// src/detectors/readers/package.ts
|
|
358
|
-
import { readFile
|
|
542
|
+
import { readFile } from "fs/promises";
|
|
359
543
|
import { join as join2 } from "path";
|
|
360
544
|
async function readPackageJson(cwd) {
|
|
361
545
|
const pkgPath = join2(cwd, "package.json");
|
|
362
546
|
try {
|
|
363
|
-
const content = await
|
|
547
|
+
const content = await readFile(pkgPath, "utf-8");
|
|
364
548
|
return JSON.parse(content);
|
|
365
549
|
} catch {
|
|
366
550
|
throw new Error(`No package.json found at ${pkgPath}. Run blockend from your project root.`);
|
|
@@ -368,14 +552,14 @@ async function readPackageJson(cwd) {
|
|
|
368
552
|
}
|
|
369
553
|
|
|
370
554
|
// src/detectors/readers/tsconfig.ts
|
|
371
|
-
import { readFile as
|
|
555
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
372
556
|
import { existsSync } from "fs";
|
|
373
557
|
import { join as join3 } from "path";
|
|
374
558
|
async function readTsConfig(cwd) {
|
|
375
559
|
const tsconfigPath = join3(cwd, "tsconfig.json");
|
|
376
560
|
if (!existsSync(tsconfigPath)) return null;
|
|
377
561
|
try {
|
|
378
|
-
const content = await
|
|
562
|
+
const content = await readFile2(tsconfigPath, "utf-8");
|
|
379
563
|
const cleanLines = content.split(/\r?\n/).filter((line) => {
|
|
380
564
|
const trimmed = line.trim();
|
|
381
565
|
return !trimmed.startsWith("//") && !trimmed.startsWith("/*");
|
|
@@ -958,7 +1142,7 @@ async function mcpStartCommand() {
|
|
|
958
1142
|
);
|
|
959
1143
|
server.tool(
|
|
960
1144
|
"detect_project",
|
|
961
|
-
"Analyzes a project context.",
|
|
1145
|
+
"Analyzes a project context: framework, language, package manager, etc.",
|
|
962
1146
|
{
|
|
963
1147
|
projectPath: z.string().describe("Absolute path to the project root")
|
|
964
1148
|
},
|
|
@@ -969,25 +1153,97 @@ async function mcpStartCommand() {
|
|
|
969
1153
|
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
970
1154
|
}
|
|
971
1155
|
);
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1156
|
+
server.tool(
|
|
1157
|
+
"diff_block",
|
|
1158
|
+
"Preview what files a block would generate without writing to disk. Shows new/modified/unchanged files.",
|
|
1159
|
+
{
|
|
1160
|
+
blockName: z.string().describe("Block name to preview, e.g. rate-limiter, pino-logger"),
|
|
1161
|
+
projectPath: z.string().describe("Absolute path to the project root")
|
|
1162
|
+
},
|
|
1163
|
+
async ({ blockName, projectPath }) => {
|
|
1164
|
+
const result = await safeExec(
|
|
1165
|
+
"npx",
|
|
1166
|
+
["--no-install", "blockend", "diff", blockName, "--json"],
|
|
1167
|
+
{ cwd: projectPath }
|
|
1168
|
+
);
|
|
1169
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
1170
|
+
}
|
|
1171
|
+
);
|
|
1172
|
+
server.tool(
|
|
1173
|
+
"doctor",
|
|
1174
|
+
"Check project health: validates blockend.json, detects framework, checks for issues.",
|
|
1175
|
+
{
|
|
1176
|
+
projectPath: z.string().describe("Absolute path to the project root")
|
|
1177
|
+
},
|
|
1178
|
+
async ({ projectPath }) => {
|
|
1179
|
+
const result = await safeExec("npx", ["--no-install", "blockend", "doctor", "--json"], {
|
|
1180
|
+
cwd: projectPath
|
|
988
1181
|
});
|
|
989
|
-
|
|
990
|
-
|
|
1182
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
1183
|
+
}
|
|
1184
|
+
);
|
|
1185
|
+
server.tool(
|
|
1186
|
+
"update_check",
|
|
1187
|
+
"Check for available updates to installed blocks. Shows version comparison and diffs.",
|
|
1188
|
+
{
|
|
1189
|
+
projectPath: z.string().describe("Absolute path to the project root")
|
|
1190
|
+
},
|
|
1191
|
+
async ({ projectPath }) => {
|
|
1192
|
+
const result = await safeExec("npx", ["--no-install", "blockend", "update", "--json"], {
|
|
1193
|
+
cwd: projectPath
|
|
1194
|
+
});
|
|
1195
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
1196
|
+
}
|
|
1197
|
+
);
|
|
1198
|
+
server.tool(
|
|
1199
|
+
"update_apply",
|
|
1200
|
+
"Apply updates to all installed blocks. Downloads new versions, rewrites imports, and updates blockend.json.",
|
|
1201
|
+
{
|
|
1202
|
+
projectPath: z.string().describe("Absolute path to the project root")
|
|
1203
|
+
},
|
|
1204
|
+
async ({ projectPath }) => {
|
|
1205
|
+
const result = await safeExec(
|
|
1206
|
+
"npx",
|
|
1207
|
+
["--no-install", "blockend", "update", "--apply", "--yes", "--json"],
|
|
1208
|
+
{ cwd: projectPath }
|
|
1209
|
+
);
|
|
1210
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
1211
|
+
}
|
|
1212
|
+
);
|
|
1213
|
+
server.tool(
|
|
1214
|
+
"add_blocks",
|
|
1215
|
+
"Install multiple blocks at once via interactive selection.",
|
|
1216
|
+
{
|
|
1217
|
+
projectPath: z.string().describe("Absolute path to the project root")
|
|
1218
|
+
},
|
|
1219
|
+
async ({ projectPath }) => {
|
|
1220
|
+
const result = await safeExec(
|
|
1221
|
+
"npx",
|
|
1222
|
+
["--no-install", "blockend", "add", "--multi", "--yes", "--json"],
|
|
1223
|
+
{ cwd: projectPath }
|
|
1224
|
+
);
|
|
1225
|
+
return { content: [{ type: "text", text: result.text }], isError: result.isError };
|
|
1226
|
+
}
|
|
1227
|
+
);
|
|
1228
|
+
const transport = new StdioServerTransport();
|
|
1229
|
+
await server.connect(transport);
|
|
1230
|
+
}
|
|
1231
|
+
async function mcpInitCommand(options) {
|
|
1232
|
+
const cwd = process.cwd();
|
|
1233
|
+
let chosenClient = options.client;
|
|
1234
|
+
if (!chosenClient) {
|
|
1235
|
+
if (options.yes) {
|
|
1236
|
+
chosenClient = "claude";
|
|
1237
|
+
} else {
|
|
1238
|
+
const selection = await select3({
|
|
1239
|
+
message: "Select target AI client layout profile:",
|
|
1240
|
+
options: Object.entries(CLIENT_MATRIX).map(([key, value]) => ({
|
|
1241
|
+
value: key,
|
|
1242
|
+
label: value.label
|
|
1243
|
+
}))
|
|
1244
|
+
});
|
|
1245
|
+
if (typeof selection === "symbol") return;
|
|
1246
|
+
chosenClient = selection;
|
|
991
1247
|
}
|
|
992
1248
|
}
|
|
993
1249
|
const meta = CLIENT_MATRIX[chosenClient];
|
|
@@ -1060,6 +1316,840 @@ async function mcpInitCommand(options) {
|
|
|
1060
1316
|
}
|
|
1061
1317
|
}
|
|
1062
1318
|
|
|
1319
|
+
// src/commands/diff.ts
|
|
1320
|
+
import path4, { join as join10, dirname as dirname3 } from "path";
|
|
1321
|
+
import fs5 from "fs/promises";
|
|
1322
|
+
import { intro as intro3, outro as outro6, spinner as spinner5, log as log3 } from "@clack/prompts";
|
|
1323
|
+
import pc7 from "picocolors";
|
|
1324
|
+
var REPO_OWNER3 = "codewithnuh";
|
|
1325
|
+
var REPO_NAME3 = "blockend";
|
|
1326
|
+
var BRANCH3 = "master";
|
|
1327
|
+
var RAW_CDN_BASE2 = `https://raw.githubusercontent.com/${REPO_OWNER3}/${REPO_NAME3}/${BRANCH3}`;
|
|
1328
|
+
var MANIFEST_URL3 = `${RAW_CDN_BASE2}/registry/index.json`;
|
|
1329
|
+
function blockSupportsEnv2(block, envKey) {
|
|
1330
|
+
if (block.frameworks && block.frameworks.length > 0) {
|
|
1331
|
+
return block.frameworks.includes("*") || block.frameworks.includes(envKey);
|
|
1332
|
+
}
|
|
1333
|
+
return block.adapters?.[envKey] !== void 0 || block.environments?.[envKey] !== void 0;
|
|
1334
|
+
}
|
|
1335
|
+
function resolveAdapterContext2(block, envKey) {
|
|
1336
|
+
return block.adapters?.[envKey] ?? block.adapters?.["*"] ?? block.environments?.[envKey] ?? block.environments?.["*"] ?? null;
|
|
1337
|
+
}
|
|
1338
|
+
async function findUp3(filename, startDir) {
|
|
1339
|
+
let dir = startDir;
|
|
1340
|
+
while (true) {
|
|
1341
|
+
const candidate = join10(dir, filename);
|
|
1342
|
+
try {
|
|
1343
|
+
await fs5.access(candidate);
|
|
1344
|
+
return candidate;
|
|
1345
|
+
} catch {
|
|
1346
|
+
const parent = dirname3(dir);
|
|
1347
|
+
if (parent === dir) break;
|
|
1348
|
+
dir = parent;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return null;
|
|
1352
|
+
}
|
|
1353
|
+
async function fileExists(filePath) {
|
|
1354
|
+
try {
|
|
1355
|
+
await fs5.access(filePath);
|
|
1356
|
+
return true;
|
|
1357
|
+
} catch {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
async function diffCommand(blockName, options = {}) {
|
|
1362
|
+
const { json = false } = options;
|
|
1363
|
+
if (!json) {
|
|
1364
|
+
console.log("");
|
|
1365
|
+
intro3(`${pc7.bgCyan(pc7.black(" blockend "))} ${pc7.dim("diff")}`);
|
|
1366
|
+
}
|
|
1367
|
+
if (!blockName) {
|
|
1368
|
+
return { block: "", files: [], error: "Block name is required. Usage: blockend diff <block>" };
|
|
1369
|
+
}
|
|
1370
|
+
const cwd = process.cwd();
|
|
1371
|
+
const configPath = await findUp3("blockend.json", cwd);
|
|
1372
|
+
if (!configPath) {
|
|
1373
|
+
const error = "blockend.json not found. Run 'npx blockend init' first.";
|
|
1374
|
+
if (json) {
|
|
1375
|
+
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1376
|
+
} else {
|
|
1377
|
+
log3.error(error);
|
|
1378
|
+
}
|
|
1379
|
+
return { block: blockName, files: [], error };
|
|
1380
|
+
}
|
|
1381
|
+
const rootDir = dirname3(configPath);
|
|
1382
|
+
let config;
|
|
1383
|
+
try {
|
|
1384
|
+
config = JSON.parse(await fs5.readFile(configPath, "utf-8"));
|
|
1385
|
+
} catch {
|
|
1386
|
+
const error = "Failed to parse blockend.json.";
|
|
1387
|
+
if (json) {
|
|
1388
|
+
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1389
|
+
} else {
|
|
1390
|
+
log3.error(error);
|
|
1391
|
+
}
|
|
1392
|
+
return { block: blockName, files: [], error };
|
|
1393
|
+
}
|
|
1394
|
+
const physicalBlocksPath = config.paths.blocks;
|
|
1395
|
+
const blocksRootAbsolute = path4.resolve(rootDir, physicalBlocksPath);
|
|
1396
|
+
const envKey = config.environment;
|
|
1397
|
+
const s = spinner5();
|
|
1398
|
+
if (!json) s.start("Fetching registry...");
|
|
1399
|
+
let registry;
|
|
1400
|
+
try {
|
|
1401
|
+
const res = await fetch(MANIFEST_URL3);
|
|
1402
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1403
|
+
registry = await res.json();
|
|
1404
|
+
if (!json) s.stop("Registry synced.");
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
const error = `Failed to fetch registry: ${String(err)}`;
|
|
1407
|
+
if (!json) {
|
|
1408
|
+
s.stop("Failed.");
|
|
1409
|
+
log3.error(error);
|
|
1410
|
+
}
|
|
1411
|
+
return { block: blockName, files: [], error };
|
|
1412
|
+
}
|
|
1413
|
+
const blockMap = registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks) ? registry.blocks : registry;
|
|
1414
|
+
const blockMeta = blockMap[blockName];
|
|
1415
|
+
if (!blockMeta) {
|
|
1416
|
+
const error = `Block "${blockName}" not found in the registry.`;
|
|
1417
|
+
if (json) {
|
|
1418
|
+
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1419
|
+
} else {
|
|
1420
|
+
log3.error(error);
|
|
1421
|
+
outro6(pc7.dim("Block not found."));
|
|
1422
|
+
}
|
|
1423
|
+
return { block: blockName, files: [], error };
|
|
1424
|
+
}
|
|
1425
|
+
if (!blockSupportsEnv2(blockMeta, envKey)) {
|
|
1426
|
+
const error = `Block "${blockName}" does not support environment: ${envKey}`;
|
|
1427
|
+
if (json) {
|
|
1428
|
+
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1429
|
+
} else {
|
|
1430
|
+
log3.error(error);
|
|
1431
|
+
}
|
|
1432
|
+
return { block: blockName, files: [], error };
|
|
1433
|
+
}
|
|
1434
|
+
const adapterContext = resolveAdapterContext2(blockMeta, envKey);
|
|
1435
|
+
if (!adapterContext) {
|
|
1436
|
+
const error = `Block "${blockName}" has no adapter for: ${envKey}`;
|
|
1437
|
+
if (json) {
|
|
1438
|
+
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1439
|
+
} else {
|
|
1440
|
+
log3.error(error);
|
|
1441
|
+
}
|
|
1442
|
+
return { block: blockName, files: [], error };
|
|
1443
|
+
}
|
|
1444
|
+
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
1445
|
+
if (variantKeys.length === 0) {
|
|
1446
|
+
const error = `No variants found for "${blockName}" / "${envKey}".`;
|
|
1447
|
+
if (json) {
|
|
1448
|
+
process.stdout.write(JSON.stringify({ block: blockName, files: [], error }) + "\n");
|
|
1449
|
+
} else {
|
|
1450
|
+
log3.error(error);
|
|
1451
|
+
}
|
|
1452
|
+
return { block: blockName, files: [], error };
|
|
1453
|
+
}
|
|
1454
|
+
const selectedVariant = variantKeys[0];
|
|
1455
|
+
const variantMeta = adapterContext.variants[selectedVariant];
|
|
1456
|
+
const targetFolder = path4.resolve(blocksRootAbsolute, blockName);
|
|
1457
|
+
const filesToDownload = [];
|
|
1458
|
+
if (blockMeta.baseFiles?.length) {
|
|
1459
|
+
filesToDownload.push(...blockMeta.baseFiles);
|
|
1460
|
+
}
|
|
1461
|
+
if (adapterContext.core) {
|
|
1462
|
+
filesToDownload.push({
|
|
1463
|
+
source: adapterContext.core,
|
|
1464
|
+
target: path4.basename(adapterContext.core)
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
1468
|
+
for (const entry of variantMeta.files) {
|
|
1469
|
+
filesToDownload.push(
|
|
1470
|
+
typeof entry === "string" ? { source: entry, target: path4.basename(entry) } : entry
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (!json) s.start(`Comparing ${blockName} files...`);
|
|
1475
|
+
const diffFiles = [];
|
|
1476
|
+
try {
|
|
1477
|
+
for (const fm of filesToDownload) {
|
|
1478
|
+
const res = await fetch(`${RAW_CDN_BASE2}/${fm.source}`);
|
|
1479
|
+
if (!res.ok) throw new Error(`Download failed: ${fm.source} (HTTP ${res.status})`);
|
|
1480
|
+
const newContent = await res.text();
|
|
1481
|
+
const targetPath = path4.join(targetFolder, fm.target);
|
|
1482
|
+
let existingContent = null;
|
|
1483
|
+
let status = "new";
|
|
1484
|
+
if (await fileExists(targetPath)) {
|
|
1485
|
+
try {
|
|
1486
|
+
existingContent = await fs5.readFile(targetPath, "utf-8");
|
|
1487
|
+
status = existingContent === newContent ? "unchanged" : "modified";
|
|
1488
|
+
} catch {
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
diffFiles.push({
|
|
1492
|
+
name: fm.target,
|
|
1493
|
+
sourcePath: fm.source,
|
|
1494
|
+
targetPath,
|
|
1495
|
+
existingContent,
|
|
1496
|
+
newContent,
|
|
1497
|
+
status
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
if (!json) s.stop("Comparison complete.");
|
|
1501
|
+
if (!json) {
|
|
1502
|
+
console.log(`
|
|
1503
|
+
${pc7.bold(pc7.cyan(blockName))} \u2014 ${pc7.dim(selectedVariant)} variant
|
|
1504
|
+
`);
|
|
1505
|
+
for (const file of diffFiles) {
|
|
1506
|
+
const icon = file.status === "new" ? pc7.green("+") : file.status === "modified" ? pc7.yellow("~") : pc7.dim("=");
|
|
1507
|
+
const label = file.status === "new" ? pc7.green("new") : file.status === "modified" ? pc7.yellow("modified") : pc7.dim("unchanged");
|
|
1508
|
+
console.log(` ${icon} ${pc7.bold(file.name)} ${pc7.dim(`(${label})`)}`);
|
|
1509
|
+
if (file.status === "modified" && file.existingContent !== null) {
|
|
1510
|
+
const oldLines = file.existingContent.split("\n");
|
|
1511
|
+
const newLines = file.newContent.split("\n");
|
|
1512
|
+
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
1513
|
+
for (let i = 0; i < Math.min(maxLines, 20); i++) {
|
|
1514
|
+
const oldLine = oldLines[i];
|
|
1515
|
+
const newLine = newLines[i];
|
|
1516
|
+
if (oldLine !== newLine) {
|
|
1517
|
+
if (oldLine !== void 0) {
|
|
1518
|
+
console.log(` ${pc7.red(`- ${oldLine}`)}`);
|
|
1519
|
+
}
|
|
1520
|
+
if (newLine !== void 0) {
|
|
1521
|
+
console.log(` ${pc7.green(`+ ${newLine}`)}`);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
if (maxLines > 20) {
|
|
1526
|
+
console.log(` ${pc7.dim(`... ${maxLines - 20} more lines`)}`);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
console.log("");
|
|
1531
|
+
const newCount = diffFiles.filter((f) => f.status === "new").length;
|
|
1532
|
+
const modCount = diffFiles.filter((f) => f.status === "modified").length;
|
|
1533
|
+
const unchangedCount = diffFiles.filter((f) => f.status === "unchanged").length;
|
|
1534
|
+
outro6(
|
|
1535
|
+
`${pc7.green(`${newCount} new`)} | ${pc7.yellow(`${modCount} modified`)} | ${pc7.dim(`${unchangedCount} unchanged`)}`
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
const result = { block: blockName, files: diffFiles };
|
|
1539
|
+
if (json) {
|
|
1540
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
1541
|
+
}
|
|
1542
|
+
return result;
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
const error = `Failed during diff: ${String(err)}`;
|
|
1545
|
+
if (!json) {
|
|
1546
|
+
s.stop("Failed.");
|
|
1547
|
+
log3.error(error);
|
|
1548
|
+
}
|
|
1549
|
+
const result = { block: blockName, files: diffFiles, error };
|
|
1550
|
+
if (json) {
|
|
1551
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
1552
|
+
}
|
|
1553
|
+
return result;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/commands/doctor.ts
|
|
1558
|
+
import path5, { join as join11, dirname as dirname4 } from "path";
|
|
1559
|
+
import fs6 from "fs/promises";
|
|
1560
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1561
|
+
import { intro as intro4, outro as outro7, spinner as spinner6 } from "@clack/prompts";
|
|
1562
|
+
import pc8 from "picocolors";
|
|
1563
|
+
function outputDoctorResult(json, result) {
|
|
1564
|
+
if (json) {
|
|
1565
|
+
process.stdout.write(
|
|
1566
|
+
JSON.stringify({
|
|
1567
|
+
success: result.errors.length === 0,
|
|
1568
|
+
checks: result.checks,
|
|
1569
|
+
warnings: result.warnings,
|
|
1570
|
+
errors: result.errors
|
|
1571
|
+
}) + "\n"
|
|
1572
|
+
);
|
|
1573
|
+
} else {
|
|
1574
|
+
console.log("");
|
|
1575
|
+
for (const check of result.checks) {
|
|
1576
|
+
const icon = check.status === "ok" ? pc8.green("\u2714") : check.status === "warning" ? pc8.yellow("\u26A0") : pc8.red("\u2716");
|
|
1577
|
+
const msg = check.status === "ok" ? pc8.dim(check.message) : check.status === "warning" ? pc8.yellow(check.message) : pc8.red(check.message);
|
|
1578
|
+
console.log(` ${icon} ${pc8.bold(check.name)}: ${msg}`);
|
|
1579
|
+
}
|
|
1580
|
+
console.log("");
|
|
1581
|
+
if (result.errors.length > 0) {
|
|
1582
|
+
outro7(pc8.red(`Found ${result.errors.length} error(s). Please fix the issues above.`));
|
|
1583
|
+
} else if (result.warnings.length > 0) {
|
|
1584
|
+
outro7(pc8.yellow(`Found ${result.warnings.length} warning(s). Everything looks functional.`));
|
|
1585
|
+
} else {
|
|
1586
|
+
outro7(pc8.green("All checks passed! Your project is healthy."));
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
async function findUp4(filename, startDir) {
|
|
1591
|
+
let dir = startDir;
|
|
1592
|
+
while (true) {
|
|
1593
|
+
const candidate = join11(dir, filename);
|
|
1594
|
+
try {
|
|
1595
|
+
await fs6.access(candidate);
|
|
1596
|
+
return candidate;
|
|
1597
|
+
} catch {
|
|
1598
|
+
const parent = dirname4(dir);
|
|
1599
|
+
if (parent === dir) break;
|
|
1600
|
+
dir = parent;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
async function doctorCommand(options = {}) {
|
|
1606
|
+
const { json = false } = options;
|
|
1607
|
+
if (!json) {
|
|
1608
|
+
console.log("");
|
|
1609
|
+
intro4(`${pc8.bgCyan(pc8.black(" blockend "))} ${pc8.dim("doctor")}`);
|
|
1610
|
+
}
|
|
1611
|
+
const cwd = process.cwd();
|
|
1612
|
+
const checks = [];
|
|
1613
|
+
const warnings = [];
|
|
1614
|
+
const errors = [];
|
|
1615
|
+
function addCheck(check) {
|
|
1616
|
+
checks.push(check);
|
|
1617
|
+
if (check.status === "warning") warnings.push(check);
|
|
1618
|
+
if (check.status === "error") errors.push(check);
|
|
1619
|
+
}
|
|
1620
|
+
const s = spinner6();
|
|
1621
|
+
if (!json) s.start("Scanning project health...");
|
|
1622
|
+
const packageJsonPath = await findUp4("package.json", cwd);
|
|
1623
|
+
if (packageJsonPath) {
|
|
1624
|
+
addCheck({ name: "package.json", status: "ok", message: "Found" });
|
|
1625
|
+
} else {
|
|
1626
|
+
addCheck({
|
|
1627
|
+
name: "package.json",
|
|
1628
|
+
status: "error",
|
|
1629
|
+
message: "Not found. Run 'npm init' or 'pnpm init' first."
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
const configPath = await findUp4("blockend.json", cwd);
|
|
1633
|
+
if (configPath) {
|
|
1634
|
+
addCheck({ name: "blockend.json", status: "ok", message: "Found" });
|
|
1635
|
+
} else {
|
|
1636
|
+
addCheck({
|
|
1637
|
+
name: "blockend.json",
|
|
1638
|
+
status: "error",
|
|
1639
|
+
message: "Not found. Run 'npx blockend init' first."
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
const tsConfigPath = await findUp4("tsconfig.json", cwd);
|
|
1643
|
+
if (tsConfigPath) {
|
|
1644
|
+
addCheck({ name: "tsconfig.json", status: "ok", message: "Found" });
|
|
1645
|
+
} else {
|
|
1646
|
+
addCheck({
|
|
1647
|
+
name: "tsconfig.json",
|
|
1648
|
+
status: "warning",
|
|
1649
|
+
message: "Not found. TypeScript projects typically need one."
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
const hasPnpmLock = existsSync5(join11(cwd, "pnpm-lock.yaml"));
|
|
1653
|
+
const hasYarnLock = existsSync5(join11(cwd, "yarn.lock"));
|
|
1654
|
+
const hasBunLock = existsSync5(join11(cwd, "bun.lockb"));
|
|
1655
|
+
const hasNpmLock = existsSync5(join11(cwd, "package-lock.json"));
|
|
1656
|
+
if (hasPnpmLock || hasYarnLock || hasBunLock || hasNpmLock) {
|
|
1657
|
+
const pmName = hasPnpmLock ? "pnpm" : hasYarnLock ? "yarn" : hasBunLock ? "bun" : "npm";
|
|
1658
|
+
addCheck({ name: "Lockfile", status: "ok", message: `${pmName} lockfile found` });
|
|
1659
|
+
} else {
|
|
1660
|
+
addCheck({
|
|
1661
|
+
name: "Lockfile",
|
|
1662
|
+
status: "warning",
|
|
1663
|
+
message: "No lockfile detected. Run your package manager to generate one."
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
try {
|
|
1667
|
+
const context = await detectProject(cwd);
|
|
1668
|
+
if (context.framework !== "none") {
|
|
1669
|
+
addCheck({
|
|
1670
|
+
name: "Framework",
|
|
1671
|
+
status: "ok",
|
|
1672
|
+
message: `Detected: ${context.framework}`
|
|
1673
|
+
});
|
|
1674
|
+
} else {
|
|
1675
|
+
addCheck({
|
|
1676
|
+
name: "Framework",
|
|
1677
|
+
status: "warning",
|
|
1678
|
+
message: "No framework detected. Blockend works best with Express, Fastify, Hono, or Next.js."
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
addCheck({
|
|
1682
|
+
name: "Package manager",
|
|
1683
|
+
status: "ok",
|
|
1684
|
+
message: `Detected: ${context.packageManager}`
|
|
1685
|
+
});
|
|
1686
|
+
if (context.hasRedis) {
|
|
1687
|
+
addCheck({ name: "Redis", status: "ok", message: "Redis client detected" });
|
|
1688
|
+
}
|
|
1689
|
+
if (context.hasPrisma) {
|
|
1690
|
+
addCheck({ name: "Prisma", status: "ok", message: "Prisma ORM detected" });
|
|
1691
|
+
}
|
|
1692
|
+
if (context.hasDrizzle) {
|
|
1693
|
+
addCheck({ name: "Drizzle", status: "ok", message: "Drizzle ORM detected" });
|
|
1694
|
+
}
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
addCheck({
|
|
1697
|
+
name: "Project detection",
|
|
1698
|
+
status: "error",
|
|
1699
|
+
message: `Failed to scan project: ${String(err)}`
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
if (configPath) {
|
|
1703
|
+
try {
|
|
1704
|
+
const raw = await fs6.readFile(configPath, "utf-8");
|
|
1705
|
+
const config = JSON.parse(raw);
|
|
1706
|
+
if (!config.environment) {
|
|
1707
|
+
addCheck({
|
|
1708
|
+
name: "Config: environment",
|
|
1709
|
+
status: "error",
|
|
1710
|
+
message: "Missing 'environment' field in blockend.json"
|
|
1711
|
+
});
|
|
1712
|
+
} else {
|
|
1713
|
+
addCheck({
|
|
1714
|
+
name: "Config: environment",
|
|
1715
|
+
status: "ok",
|
|
1716
|
+
message: `Set to '${config.environment}'`
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
if (!config.paths?.blocks) {
|
|
1720
|
+
addCheck({
|
|
1721
|
+
name: "Config: blocks path",
|
|
1722
|
+
status: "error",
|
|
1723
|
+
message: "Missing 'paths.blocks' in blockend.json"
|
|
1724
|
+
});
|
|
1725
|
+
} else {
|
|
1726
|
+
const blocksDir = path5.resolve(dirname4(configPath), config.paths.blocks);
|
|
1727
|
+
if (existsSync5(blocksDir)) {
|
|
1728
|
+
addCheck({
|
|
1729
|
+
name: "Config: blocks directory",
|
|
1730
|
+
status: "ok",
|
|
1731
|
+
message: `Exists: ${config.paths.blocks}`
|
|
1732
|
+
});
|
|
1733
|
+
} else {
|
|
1734
|
+
addCheck({
|
|
1735
|
+
name: "Config: blocks directory",
|
|
1736
|
+
status: "warning",
|
|
1737
|
+
message: `Directory '${config.paths.blocks}' does not exist yet. Blocks will create it on first add.`
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
} catch {
|
|
1742
|
+
addCheck({
|
|
1743
|
+
name: "Config: parse",
|
|
1744
|
+
status: "error",
|
|
1745
|
+
message: "Failed to parse blockend.json \u2014 file may be corrupted."
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
if (!json) s.stop("Scan complete.");
|
|
1750
|
+
const result = { checks, warnings, errors };
|
|
1751
|
+
outputDoctorResult(json, result);
|
|
1752
|
+
if (errors.length > 0 && !json) {
|
|
1753
|
+
process.exit(1);
|
|
1754
|
+
}
|
|
1755
|
+
return result;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// src/commands/update.ts
|
|
1759
|
+
import path6, { join as join12, dirname as dirname5 } from "path";
|
|
1760
|
+
import fs7 from "fs/promises";
|
|
1761
|
+
import { intro as intro5, outro as outro8, spinner as spinner7, log as log4, multiselect as multiselect2, confirm as confirm4, isCancel as isCancel3 } from "@clack/prompts";
|
|
1762
|
+
import pc9 from "picocolors";
|
|
1763
|
+
var REPO_OWNER4 = "codewithnuh";
|
|
1764
|
+
var REPO_NAME4 = "blockend";
|
|
1765
|
+
var BRANCH4 = "master";
|
|
1766
|
+
var RAW_CDN_BASE3 = `https://raw.githubusercontent.com/${REPO_OWNER4}/${REPO_NAME4}/${BRANCH4}`;
|
|
1767
|
+
var MANIFEST_URL4 = `${RAW_CDN_BASE3}/registry/index.json`;
|
|
1768
|
+
function blockSupportsEnv3(block, envKey) {
|
|
1769
|
+
if (block.frameworks && block.frameworks.length > 0) {
|
|
1770
|
+
return block.frameworks.includes("*") || block.frameworks.includes(envKey);
|
|
1771
|
+
}
|
|
1772
|
+
return block.adapters?.[envKey] !== void 0 || block.environments?.[envKey] !== void 0;
|
|
1773
|
+
}
|
|
1774
|
+
function resolveAdapterContext3(block, envKey) {
|
|
1775
|
+
return block.adapters?.[envKey] ?? block.adapters?.["*"] ?? block.environments?.[envKey] ?? block.environments?.["*"] ?? null;
|
|
1776
|
+
}
|
|
1777
|
+
async function findUp5(filename, startDir) {
|
|
1778
|
+
let dir = startDir;
|
|
1779
|
+
while (true) {
|
|
1780
|
+
const candidate = join12(dir, filename);
|
|
1781
|
+
try {
|
|
1782
|
+
await fs7.access(candidate);
|
|
1783
|
+
return candidate;
|
|
1784
|
+
} catch {
|
|
1785
|
+
const parent = dirname5(dir);
|
|
1786
|
+
if (parent === dir) break;
|
|
1787
|
+
dir = parent;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return null;
|
|
1791
|
+
}
|
|
1792
|
+
function computeLineDiff(oldContent, newContent) {
|
|
1793
|
+
const oldLines = oldContent.split("\n");
|
|
1794
|
+
const newLines = newContent.split("\n");
|
|
1795
|
+
const m = oldLines.length;
|
|
1796
|
+
const n = newLines.length;
|
|
1797
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
1798
|
+
for (let i2 = 1; i2 <= m; i2++) {
|
|
1799
|
+
for (let j2 = 1; j2 <= n; j2++) {
|
|
1800
|
+
if (oldLines[i2 - 1] === newLines[j2 - 1]) {
|
|
1801
|
+
dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
1802
|
+
} else {
|
|
1803
|
+
dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
let i = m, j = n;
|
|
1808
|
+
const raw = [];
|
|
1809
|
+
while (i > 0 || j > 0) {
|
|
1810
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
1811
|
+
raw.unshift({ type: "context", oldLine: i, newLine: j, content: oldLines[i - 1] });
|
|
1812
|
+
i--;
|
|
1813
|
+
j--;
|
|
1814
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
1815
|
+
raw.unshift({ type: "added", newLine: j, content: newLines[j - 1] });
|
|
1816
|
+
j--;
|
|
1817
|
+
} else {
|
|
1818
|
+
raw.unshift({ type: "removed", oldLine: i, content: oldLines[i - 1] });
|
|
1819
|
+
i--;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const result = [];
|
|
1823
|
+
for (let k = 0; k < raw.length; k++) {
|
|
1824
|
+
const r = raw[k];
|
|
1825
|
+
const nearChange = raw.slice(Math.max(0, k - 3), k + 4).some((x) => x.type !== "context");
|
|
1826
|
+
if (r.type !== "context" || nearChange) {
|
|
1827
|
+
result.push({
|
|
1828
|
+
type: r.type,
|
|
1829
|
+
content: r.content,
|
|
1830
|
+
oldLineNo: r.oldLine,
|
|
1831
|
+
newLineNo: r.newLine
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return result;
|
|
1836
|
+
}
|
|
1837
|
+
function handleCancel2(value) {
|
|
1838
|
+
if (isCancel3(value)) {
|
|
1839
|
+
outro8(pc9.dim("Operation cancelled."));
|
|
1840
|
+
process.exit(0);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
function printDiff(diff2) {
|
|
1844
|
+
for (const file of diff2) {
|
|
1845
|
+
if (file.status === "unchanged") continue;
|
|
1846
|
+
const icon = file.status === "added" ? pc9.green("+") : file.status === "removed" ? pc9.red("-") : pc9.yellow("~");
|
|
1847
|
+
const label = file.status === "added" ? pc9.green("new") : file.status === "removed" ? pc9.red("deleted") : pc9.yellow("modified");
|
|
1848
|
+
console.log(`
|
|
1849
|
+
${icon} ${pc9.bold(file.name)} ${pc9.dim(`(${label})`)}`);
|
|
1850
|
+
if (file.lines.length === 0) continue;
|
|
1851
|
+
let lastShownLine = -1;
|
|
1852
|
+
for (let idx = 0; idx < file.lines.length; idx++) {
|
|
1853
|
+
const line = file.lines[idx];
|
|
1854
|
+
if (lastShownLine >= 0 && idx - lastShownLine > 1) {
|
|
1855
|
+
console.log(` ${pc9.dim("...")}`);
|
|
1856
|
+
}
|
|
1857
|
+
lastShownLine = idx;
|
|
1858
|
+
const lineNo = line.type === "removed" ? String(line.oldLineNo ?? "").padStart(4) : String(line.newLineNo ?? "").padStart(4);
|
|
1859
|
+
const prefix = line.type === "added" ? "+" : line.type === "removed" ? "-" : " ";
|
|
1860
|
+
const color = line.type === "added" ? pc9.green : line.type === "removed" ? pc9.red : pc9.dim;
|
|
1861
|
+
console.log(` ${pc9.dim(lineNo)} ${color(`${prefix} ${line.content}`)}`);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
function outputUpdateResult(json, result) {
|
|
1866
|
+
if (json) {
|
|
1867
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
console.log("");
|
|
1871
|
+
if (result.blocks.length === 0) {
|
|
1872
|
+
console.log(pc9.dim(" No blocks tracked. Run 'npx blockend add <block>' to get started."));
|
|
1873
|
+
console.log("");
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
const upToDate = result.blocks.filter((b) => b.status === "up-to-date");
|
|
1877
|
+
const updates = result.blocks.filter((b) => b.status === "update-available");
|
|
1878
|
+
if (updates.length > 0) {
|
|
1879
|
+
console.log(` ${pc9.bold(pc9.yellow("Updates available:"))}
|
|
1880
|
+
`);
|
|
1881
|
+
for (const block of updates) {
|
|
1882
|
+
console.log(
|
|
1883
|
+
` ${pc9.yellow("~")} ${pc9.bold(block.name)} ${pc9.dim(`${block.localVersion} \u2192 ${block.remoteVersion}`)}`
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
console.log("");
|
|
1887
|
+
}
|
|
1888
|
+
if (upToDate.length > 0) {
|
|
1889
|
+
console.log(` ${pc9.bold(pc9.green("Up to date:"))}`);
|
|
1890
|
+
for (const block of upToDate) {
|
|
1891
|
+
console.log(` ${pc9.green("\u2714")} ${block.name} ${pc9.dim(`v${block.localVersion}`)}`);
|
|
1892
|
+
}
|
|
1893
|
+
console.log("");
|
|
1894
|
+
}
|
|
1895
|
+
if (result.diffs && Object.keys(result.diffs).length > 0) {
|
|
1896
|
+
console.log(` ${pc9.bold(pc9.cyan("Change details:"))}`);
|
|
1897
|
+
for (const [blockName, diffs] of Object.entries(result.diffs)) {
|
|
1898
|
+
console.log(`
|
|
1899
|
+
${pc9.bold(pc9.cyan(`\u25B8 ${blockName}`))}`);
|
|
1900
|
+
printDiff(diffs);
|
|
1901
|
+
}
|
|
1902
|
+
console.log("");
|
|
1903
|
+
}
|
|
1904
|
+
if (result.applied && result.applied.length > 0) {
|
|
1905
|
+
outro8(pc9.green(`\u2728 Updated ${result.applied.length} block(s) successfully.`));
|
|
1906
|
+
} else if (updates.length > 0) {
|
|
1907
|
+
outro8(`${pc9.yellow(`${updates.length} block(s) have updates.`)} Run with --apply to update.`);
|
|
1908
|
+
} else {
|
|
1909
|
+
outro8(pc9.green("All tracked blocks are up to date."));
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
function resolveBlockFiles(blockMeta, envKey) {
|
|
1913
|
+
const adapterContext = resolveAdapterContext3(blockMeta, envKey);
|
|
1914
|
+
if (!adapterContext) return null;
|
|
1915
|
+
const variantKeys = Object.keys(adapterContext.variants ?? {});
|
|
1916
|
+
if (variantKeys.length === 0) return null;
|
|
1917
|
+
const selectedVariant = variantKeys[0];
|
|
1918
|
+
const variantMeta = adapterContext.variants[selectedVariant];
|
|
1919
|
+
const expectedFiles = [];
|
|
1920
|
+
if (blockMeta.baseFiles?.length) expectedFiles.push(...blockMeta.baseFiles);
|
|
1921
|
+
if (adapterContext.core) {
|
|
1922
|
+
expectedFiles.push({ source: adapterContext.core, target: path6.basename(adapterContext.core) });
|
|
1923
|
+
}
|
|
1924
|
+
if (variantMeta && Array.isArray(variantMeta.files)) {
|
|
1925
|
+
for (const entry of variantMeta.files) {
|
|
1926
|
+
expectedFiles.push(
|
|
1927
|
+
typeof entry === "string" ? { source: entry, target: path6.basename(entry) } : entry
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return { files: expectedFiles, variant: selectedVariant };
|
|
1932
|
+
}
|
|
1933
|
+
async function updateCommand(options = {}) {
|
|
1934
|
+
const { json = false, diff: diff2 = true, apply = false } = options;
|
|
1935
|
+
if (!json) {
|
|
1936
|
+
console.log("");
|
|
1937
|
+
intro5(`${pc9.bgCyan(pc9.black(" blockend "))} ${pc9.dim("update")}`);
|
|
1938
|
+
}
|
|
1939
|
+
const cwd = process.cwd();
|
|
1940
|
+
const configPath = await findUp5("blockend.json", cwd);
|
|
1941
|
+
if (!configPath) {
|
|
1942
|
+
const error = "blockend.json not found. Run 'npx blockend init' first.";
|
|
1943
|
+
if (json) process.stdout.write(JSON.stringify({ blocks: [], error }) + "\n");
|
|
1944
|
+
else log4.error(error);
|
|
1945
|
+
return { blocks: [], error };
|
|
1946
|
+
}
|
|
1947
|
+
const rootDir = dirname5(configPath);
|
|
1948
|
+
let config;
|
|
1949
|
+
try {
|
|
1950
|
+
config = JSON.parse(await fs7.readFile(configPath, "utf-8"));
|
|
1951
|
+
} catch {
|
|
1952
|
+
const error = "Failed to parse blockend.json.";
|
|
1953
|
+
if (json) process.stdout.write(JSON.stringify({ blocks: [], error }) + "\n");
|
|
1954
|
+
else log4.error(error);
|
|
1955
|
+
return { blocks: [], error };
|
|
1956
|
+
}
|
|
1957
|
+
const physicalBlocksPath = config.paths.blocks;
|
|
1958
|
+
const blocksRootAbsolute = path6.resolve(rootDir, physicalBlocksPath);
|
|
1959
|
+
const envKey = config.environment;
|
|
1960
|
+
const aliasBase = config.aliases.blocks;
|
|
1961
|
+
const rewriteStrategy = config.importRewriteStrategy ?? "remove";
|
|
1962
|
+
const installedRecords = Array.isArray(config.installed) ? config.installed : [];
|
|
1963
|
+
const s = spinner7();
|
|
1964
|
+
if (!json) s.start("Fetching registry...");
|
|
1965
|
+
let registry;
|
|
1966
|
+
try {
|
|
1967
|
+
const res = await fetch(MANIFEST_URL4);
|
|
1968
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1969
|
+
registry = await res.json();
|
|
1970
|
+
if (!json) s.stop("Registry synced.");
|
|
1971
|
+
} catch (err) {
|
|
1972
|
+
const error = `Failed to fetch registry: ${String(err)}`;
|
|
1973
|
+
if (!json) {
|
|
1974
|
+
s.stop("Failed.");
|
|
1975
|
+
log4.error(error);
|
|
1976
|
+
}
|
|
1977
|
+
return { blocks: [], error };
|
|
1978
|
+
}
|
|
1979
|
+
const blockMap = registry.blocks && typeof registry.blocks === "object" && !Array.isArray(registry.blocks) ? registry.blocks : registry;
|
|
1980
|
+
if (!json) s.start("Comparing versions...");
|
|
1981
|
+
const blockResults = [];
|
|
1982
|
+
const diffs = {};
|
|
1983
|
+
for (const record of installedRecords) {
|
|
1984
|
+
const blockMeta = blockMap[record.name];
|
|
1985
|
+
const remoteVersion = blockMeta?.version ?? "0.0.0";
|
|
1986
|
+
if (!blockMeta || !blockSupportsEnv3(blockMeta, envKey)) {
|
|
1987
|
+
blockResults.push({
|
|
1988
|
+
name: record.name,
|
|
1989
|
+
localVersion: record.version,
|
|
1990
|
+
remoteVersion: "?",
|
|
1991
|
+
status: "not-tracked"
|
|
1992
|
+
});
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
const hasUpdate = record.version !== remoteVersion || record.contentHash === "0";
|
|
1996
|
+
blockResults.push({
|
|
1997
|
+
name: record.name,
|
|
1998
|
+
localVersion: record.version,
|
|
1999
|
+
remoteVersion,
|
|
2000
|
+
status: hasUpdate ? "update-available" : "up-to-date"
|
|
2001
|
+
});
|
|
2002
|
+
if (hasUpdate && diff2) {
|
|
2003
|
+
const resolved = resolveBlockFiles(blockMeta, envKey);
|
|
2004
|
+
if (!resolved) continue;
|
|
2005
|
+
const targetFolder = path6.resolve(blocksRootAbsolute, record.name);
|
|
2006
|
+
const fileDiffs = [];
|
|
2007
|
+
for (const fm of resolved.files) {
|
|
2008
|
+
const localPath = join12(targetFolder, fm.target);
|
|
2009
|
+
let localContent = "";
|
|
2010
|
+
let localExists = false;
|
|
2011
|
+
try {
|
|
2012
|
+
localContent = await fs7.readFile(localPath, "utf-8");
|
|
2013
|
+
localExists = true;
|
|
2014
|
+
} catch {
|
|
2015
|
+
localContent = "";
|
|
2016
|
+
}
|
|
2017
|
+
let remoteContent = "";
|
|
2018
|
+
try {
|
|
2019
|
+
const res = await fetch(`${RAW_CDN_BASE3}/${fm.source}`);
|
|
2020
|
+
if (res.ok) remoteContent = await res.text();
|
|
2021
|
+
} catch {
|
|
2022
|
+
remoteContent = "";
|
|
2023
|
+
}
|
|
2024
|
+
if (!localExists) {
|
|
2025
|
+
fileDiffs.push({
|
|
2026
|
+
name: fm.target,
|
|
2027
|
+
file: fm.target,
|
|
2028
|
+
status: "added",
|
|
2029
|
+
lines: remoteContent.split("\n").map((l, i) => ({
|
|
2030
|
+
type: "added",
|
|
2031
|
+
content: l,
|
|
2032
|
+
newLineNo: i + 1
|
|
2033
|
+
}))
|
|
2034
|
+
});
|
|
2035
|
+
} else if (localContent !== remoteContent) {
|
|
2036
|
+
fileDiffs.push({
|
|
2037
|
+
name: fm.target,
|
|
2038
|
+
file: fm.target,
|
|
2039
|
+
status: "modified",
|
|
2040
|
+
lines: computeLineDiff(localContent, remoteContent)
|
|
2041
|
+
});
|
|
2042
|
+
} else {
|
|
2043
|
+
fileDiffs.push({
|
|
2044
|
+
name: fm.target,
|
|
2045
|
+
file: fm.target,
|
|
2046
|
+
status: "unchanged",
|
|
2047
|
+
lines: []
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
diffs[record.name] = fileDiffs;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
if (!json) s.stop("Comparison complete.");
|
|
2055
|
+
const updateableBlocks = blockResults.filter((b) => b.status === "update-available");
|
|
2056
|
+
if (apply && updateableBlocks.length > 0) {
|
|
2057
|
+
let selectedNames;
|
|
2058
|
+
if (json) {
|
|
2059
|
+
selectedNames = updateableBlocks.map((b) => b.name);
|
|
2060
|
+
} else {
|
|
2061
|
+
const multiPrompt = await multiselect2({
|
|
2062
|
+
message: "Select blocks to update (space to select, enter to confirm):",
|
|
2063
|
+
options: updateableBlocks.map((b) => ({
|
|
2064
|
+
value: b.name,
|
|
2065
|
+
label: `${pc9.bold(pc9.cyan(b.name))} ${pc9.dim(`${b.localVersion} \u2192 ${b.remoteVersion}`)}`
|
|
2066
|
+
}))
|
|
2067
|
+
});
|
|
2068
|
+
handleCancel2(multiPrompt);
|
|
2069
|
+
selectedNames = multiPrompt;
|
|
2070
|
+
}
|
|
2071
|
+
if (selectedNames.length === 0) {
|
|
2072
|
+
if (!json) outro8(pc9.dim("No blocks selected."));
|
|
2073
|
+
return { blocks: blockResults, diffs };
|
|
2074
|
+
}
|
|
2075
|
+
if (!json) {
|
|
2076
|
+
const proceed = await confirm4({
|
|
2077
|
+
message: `Update ${selectedNames.length} block(s)? This will overwrite local files.`,
|
|
2078
|
+
initialValue: true
|
|
2079
|
+
});
|
|
2080
|
+
handleCancel2(proceed);
|
|
2081
|
+
if (!proceed) {
|
|
2082
|
+
outro8(pc9.dim("Update cancelled."));
|
|
2083
|
+
return { blocks: blockResults, diffs };
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
const applied = [];
|
|
2087
|
+
for (const blockName of selectedNames) {
|
|
2088
|
+
const blockMeta = blockMap[blockName];
|
|
2089
|
+
if (!blockMeta) continue;
|
|
2090
|
+
const resolved = resolveBlockFiles(blockMeta, envKey);
|
|
2091
|
+
if (!resolved) continue;
|
|
2092
|
+
const targetFolder = path6.resolve(blocksRootAbsolute, blockName);
|
|
2093
|
+
const downloadedTargets = /* @__PURE__ */ new Set();
|
|
2094
|
+
try {
|
|
2095
|
+
const fileBatches = [];
|
|
2096
|
+
for (const fm of resolved.files) {
|
|
2097
|
+
const res = await fetch(`${RAW_CDN_BASE3}/${fm.source}`);
|
|
2098
|
+
if (!res.ok) throw new Error(`Download failed: ${fm.source} (HTTP ${res.status})`);
|
|
2099
|
+
fileBatches.push({ fm, raw: await res.text() });
|
|
2100
|
+
downloadedTargets.add(fm.target);
|
|
2101
|
+
}
|
|
2102
|
+
let contentHash = 0;
|
|
2103
|
+
for (const { fm, raw } of fileBatches) {
|
|
2104
|
+
const writtenFilePath = path6.join(targetFolder, fm.target);
|
|
2105
|
+
const content = rewriteFileImports(
|
|
2106
|
+
raw,
|
|
2107
|
+
writtenFilePath,
|
|
2108
|
+
blocksRootAbsolute,
|
|
2109
|
+
aliasBase,
|
|
2110
|
+
rewriteStrategy,
|
|
2111
|
+
downloadedTargets
|
|
2112
|
+
);
|
|
2113
|
+
await fs7.mkdir(dirname5(writtenFilePath), { recursive: true });
|
|
2114
|
+
await fs7.writeFile(writtenFilePath, content, "utf-8");
|
|
2115
|
+
for (let i = 0; i < content.length; i++) {
|
|
2116
|
+
contentHash = (contentHash << 5) - contentHash + content.charCodeAt(i) | 0;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
const blockVersion = blockMeta.version ?? "0.0.0";
|
|
2120
|
+
const installedRecord = {
|
|
2121
|
+
name: blockName,
|
|
2122
|
+
version: blockVersion,
|
|
2123
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2124
|
+
files: resolved.files.map((f) => f.target),
|
|
2125
|
+
contentHash: contentHash.toString(16)
|
|
2126
|
+
};
|
|
2127
|
+
try {
|
|
2128
|
+
const cfgRaw = await fs7.readFile(configPath, "utf-8");
|
|
2129
|
+
const cfgData = JSON.parse(cfgRaw);
|
|
2130
|
+
if (!Array.isArray(cfgData.installed)) cfgData.installed = [];
|
|
2131
|
+
cfgData.installed = cfgData.installed.filter(
|
|
2132
|
+
(b) => b.name !== blockName
|
|
2133
|
+
);
|
|
2134
|
+
cfgData.installed.push(installedRecord);
|
|
2135
|
+
await fs7.writeFile(configPath, JSON.stringify(cfgData, null, 2), "utf-8");
|
|
2136
|
+
} catch {
|
|
2137
|
+
}
|
|
2138
|
+
applied.push(blockName);
|
|
2139
|
+
if (!json) log4.info(` ${pc9.green("\u2714")} ${blockName} updated to v${blockVersion}`);
|
|
2140
|
+
} catch (err) {
|
|
2141
|
+
if (!json) log4.error(` ${pc9.red("\u2716")} ${blockName}: ${String(err)}`);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
const result2 = { blocks: blockResults, applied, diffs };
|
|
2145
|
+
outputUpdateResult(json, result2);
|
|
2146
|
+
return result2;
|
|
2147
|
+
}
|
|
2148
|
+
const result = { blocks: blockResults, diffs };
|
|
2149
|
+
outputUpdateResult(json, result);
|
|
2150
|
+
return result;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
1063
2153
|
// src/index.ts
|
|
1064
2154
|
var mcpInit = defineCommand({
|
|
1065
2155
|
meta: {
|
|
@@ -1165,10 +2255,16 @@ var add = defineCommand({
|
|
|
1165
2255
|
type: "boolean",
|
|
1166
2256
|
default: false,
|
|
1167
2257
|
description: "Output streaming machine-readable JSON payloads for automation systems"
|
|
2258
|
+
},
|
|
2259
|
+
multi: {
|
|
2260
|
+
type: "boolean",
|
|
2261
|
+
alias: "m",
|
|
2262
|
+
default: false,
|
|
2263
|
+
description: "Select and add multiple blocks at once via interactive multiselect"
|
|
1168
2264
|
}
|
|
1169
2265
|
},
|
|
1170
2266
|
async run({ args }) {
|
|
1171
|
-
await addCommand(args.block, { yes: args.yes, json: args.json });
|
|
2267
|
+
await addCommand(args.block, { yes: args.yes, json: args.json, multi: args.multi });
|
|
1172
2268
|
}
|
|
1173
2269
|
});
|
|
1174
2270
|
var detect = defineCommand({
|
|
@@ -1187,12 +2283,75 @@ var detect = defineCommand({
|
|
|
1187
2283
|
await detectCommand({ json: args.json });
|
|
1188
2284
|
}
|
|
1189
2285
|
});
|
|
2286
|
+
var diff = defineCommand({
|
|
2287
|
+
meta: {
|
|
2288
|
+
name: "diff",
|
|
2289
|
+
description: "Preview generated files for a block without writing to disk"
|
|
2290
|
+
},
|
|
2291
|
+
args: {
|
|
2292
|
+
block: {
|
|
2293
|
+
type: "positional",
|
|
2294
|
+
required: true,
|
|
2295
|
+
description: "Block name to preview"
|
|
2296
|
+
},
|
|
2297
|
+
json: {
|
|
2298
|
+
type: "boolean",
|
|
2299
|
+
default: false,
|
|
2300
|
+
description: "Output diff results as JSON"
|
|
2301
|
+
}
|
|
2302
|
+
},
|
|
2303
|
+
async run({ args }) {
|
|
2304
|
+
await diffCommand(args.block, { json: args.json });
|
|
2305
|
+
}
|
|
2306
|
+
});
|
|
2307
|
+
var doctor = defineCommand({
|
|
2308
|
+
meta: {
|
|
2309
|
+
name: "doctor",
|
|
2310
|
+
description: "Detect configuration and project issues affecting Blockend"
|
|
2311
|
+
},
|
|
2312
|
+
args: {
|
|
2313
|
+
json: {
|
|
2314
|
+
type: "boolean",
|
|
2315
|
+
default: false,
|
|
2316
|
+
description: "Output health check results as JSON"
|
|
2317
|
+
}
|
|
2318
|
+
},
|
|
2319
|
+
async run({ args }) {
|
|
2320
|
+
await doctorCommand({ json: args.json });
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
var update = defineCommand({
|
|
2324
|
+
meta: {
|
|
2325
|
+
name: "update",
|
|
2326
|
+
description: "Compare installed blocks with newer available versions and show diffs"
|
|
2327
|
+
},
|
|
2328
|
+
args: {
|
|
2329
|
+
json: {
|
|
2330
|
+
type: "boolean",
|
|
2331
|
+
default: false,
|
|
2332
|
+
description: "Output update status as JSON"
|
|
2333
|
+
},
|
|
2334
|
+
diff: {
|
|
2335
|
+
type: "boolean",
|
|
2336
|
+
default: true,
|
|
2337
|
+
description: "Show git-diff style file comparison for blocks with updates"
|
|
2338
|
+
},
|
|
2339
|
+
apply: {
|
|
2340
|
+
type: "boolean",
|
|
2341
|
+
default: false,
|
|
2342
|
+
description: "Select blocks to update (multiselect) and apply updates with import rewriting"
|
|
2343
|
+
}
|
|
2344
|
+
},
|
|
2345
|
+
async run({ args }) {
|
|
2346
|
+
await updateCommand({ json: args.json, diff: args.diff, apply: args.apply });
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
1190
2349
|
var main = defineCommand({
|
|
1191
2350
|
meta: {
|
|
1192
2351
|
name: "blockend",
|
|
1193
2352
|
version: "0.1.0",
|
|
1194
2353
|
description: "Blockend CLI - Core architectural blocks straight into your repository layout primitives"
|
|
1195
2354
|
},
|
|
1196
|
-
subCommands: { init, add, detect, list, mcp }
|
|
2355
|
+
subCommands: { init, add, detect, list, diff, doctor, update, mcp }
|
|
1197
2356
|
});
|
|
1198
2357
|
runMain(main);
|