@zenithbuild/cli 0.6.3 → 0.6.5

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.
@@ -0,0 +1,378 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import {
5
+ readCliPackageVersion,
6
+ readInstalledPackageVersion,
7
+ resolveBundlerBin
8
+ } from './toolchain-paths.js';
9
+
10
+ const PACKAGE_KEYS = [
11
+ ['core', '@zenithbuild/core'],
12
+ ['compiler', '@zenithbuild/compiler'],
13
+ ['runtime', '@zenithbuild/runtime'],
14
+ ['router', '@zenithbuild/router'],
15
+ ['bundlerPackage', '@zenithbuild/bundler']
16
+ ];
17
+
18
+ function parseVersion(version) {
19
+ const raw = String(version || '').trim();
20
+ const match = raw.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
21
+ if (!match) {
22
+ return null;
23
+ }
24
+ return {
25
+ raw,
26
+ major: Number.parseInt(match[1], 10),
27
+ minor: Number.parseInt(match[2], 10),
28
+ patch: Number.parseInt(match[3], 10),
29
+ prerelease: match[4] || '',
30
+ prereleaseParts: match[4] ? match[4].split('.') : []
31
+ };
32
+ }
33
+
34
+ function compareIdentifiers(left, right) {
35
+ const leftNumeric = /^\d+$/.test(left);
36
+ const rightNumeric = /^\d+$/.test(right);
37
+ if (leftNumeric && rightNumeric) {
38
+ return Number(left) - Number(right);
39
+ }
40
+ if (leftNumeric) {
41
+ return -1;
42
+ }
43
+ if (rightNumeric) {
44
+ return 1;
45
+ }
46
+ return left.localeCompare(right);
47
+ }
48
+
49
+ export function compareVersions(leftVersion, rightVersion) {
50
+ const left = parseVersion(leftVersion);
51
+ const right = parseVersion(rightVersion);
52
+ if (!left && !right) return 0;
53
+ if (!left) return -1;
54
+ if (!right) return 1;
55
+
56
+ const numberDelta = (
57
+ (left.major - right.major)
58
+ || (left.minor - right.minor)
59
+ || (left.patch - right.patch)
60
+ );
61
+ if (numberDelta !== 0) {
62
+ return numberDelta;
63
+ }
64
+
65
+ if (!left.prerelease && !right.prerelease) {
66
+ return 0;
67
+ }
68
+ if (!left.prerelease) {
69
+ return 1;
70
+ }
71
+ if (!right.prerelease) {
72
+ return -1;
73
+ }
74
+
75
+ const len = Math.max(left.prereleaseParts.length, right.prereleaseParts.length);
76
+ for (let index = 0; index < len; index += 1) {
77
+ const leftPart = left.prereleaseParts[index];
78
+ const rightPart = right.prereleaseParts[index];
79
+ if (leftPart === undefined) {
80
+ return -1;
81
+ }
82
+ if (rightPart === undefined) {
83
+ return 1;
84
+ }
85
+ const delta = compareIdentifiers(leftPart, rightPart);
86
+ if (delta !== 0) {
87
+ return delta;
88
+ }
89
+ }
90
+
91
+ return 0;
92
+ }
93
+
94
+ function prereleaseChannel(parsed) {
95
+ if (!parsed || !parsed.prerelease) {
96
+ return 'stable';
97
+ }
98
+ const [label = 'prerelease', train] = parsed.prereleaseParts;
99
+ if (train && /^\d+$/.test(train)) {
100
+ return `${label}.${train}`;
101
+ }
102
+ return label;
103
+ }
104
+
105
+ function classifyDifference(expectedVersion, actualVersion) {
106
+ if (!expectedVersion || !actualVersion) {
107
+ return 'unknown';
108
+ }
109
+ if (expectedVersion === actualVersion) {
110
+ return 'exact';
111
+ }
112
+ const expected = parseVersion(expectedVersion);
113
+ const actual = parseVersion(actualVersion);
114
+ if (!expected || !actual) {
115
+ return 'unknown';
116
+ }
117
+ if (expected.major !== actual.major || expected.minor !== actual.minor) {
118
+ return 'hard';
119
+ }
120
+ if (prereleaseChannel(expected) !== prereleaseChannel(actual)) {
121
+ return 'hard';
122
+ }
123
+ return 'soft';
124
+ }
125
+
126
+ function readProjectPackage(projectRoot) {
127
+ if (!projectRoot) {
128
+ return null;
129
+ }
130
+ try {
131
+ const manifestPath = resolve(projectRoot, 'package.json');
132
+ if (!existsSync(manifestPath)) {
133
+ return null;
134
+ }
135
+ return JSON.parse(readFileSync(manifestPath, 'utf8'));
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ function buildFixCommand(projectRoot, targetVersion) {
142
+ const manifest = readProjectPackage(projectRoot);
143
+ const dependencyNames = [
144
+ '@zenithbuild/core',
145
+ '@zenithbuild/cli',
146
+ '@zenithbuild/compiler',
147
+ '@zenithbuild/runtime',
148
+ '@zenithbuild/router',
149
+ '@zenithbuild/bundler'
150
+ ];
151
+
152
+ const deps = [];
153
+ const devDeps = [];
154
+ for (const name of dependencyNames) {
155
+ if (manifest?.dependencies && Object.prototype.hasOwnProperty.call(manifest.dependencies, name)) {
156
+ deps.push(`${name}@${targetVersion}`);
157
+ continue;
158
+ }
159
+ devDeps.push(`${name}@${targetVersion}`);
160
+ }
161
+
162
+ const commands = [];
163
+ if (deps.length > 0) {
164
+ commands.push(`npm i ${deps.join(' ')}`);
165
+ }
166
+ if (devDeps.length > 0) {
167
+ commands.push(`npm i -D ${devDeps.join(' ')}`);
168
+ }
169
+ if (commands.length === 0) {
170
+ commands.push(`npm i -D ${dependencyNames.map((name) => `${name}@${targetVersion}`).join(' ')}`);
171
+ }
172
+ return commands.join(' && ');
173
+ }
174
+
175
+ function describeVersions(versions) {
176
+ const entries = [
177
+ ['cli', versions.cli],
178
+ ['project cli', versions.projectCli],
179
+ ['core', versions.core],
180
+ ['compiler', versions.compiler],
181
+ ['runtime', versions.runtime],
182
+ ['router', versions.router],
183
+ ['bundler pkg', versions.bundlerPackage],
184
+ ['bundler bin', versions.bundlerBinary]
185
+ ];
186
+ return entries
187
+ .filter(([, version]) => typeof version === 'string' && version.length > 0)
188
+ .map(([label, version]) => `${label}=${version}`)
189
+ .join(' ');
190
+ }
191
+
192
+ function summarizeIssues(issues) {
193
+ const preview = issues.slice(0, 3).map((issue) => issue.summary);
194
+ const suffix = issues.length > 3 ? ` +${issues.length - 3} more` : '';
195
+ return `${preview.join('; ')}${suffix}`;
196
+ }
197
+
198
+ function determineTargetVersion(versions) {
199
+ const candidates = [
200
+ versions.projectCli,
201
+ versions.core,
202
+ versions.compiler,
203
+ versions.runtime,
204
+ versions.router,
205
+ versions.bundlerPackage,
206
+ versions.cli
207
+ ].filter((value) => typeof value === 'string' && value.length > 0);
208
+
209
+ if (candidates.length === 0) {
210
+ return '0.0.0';
211
+ }
212
+
213
+ let highest = candidates[0];
214
+ for (const candidate of candidates.slice(1)) {
215
+ if (compareVersions(candidate, highest) > 0) {
216
+ highest = candidate;
217
+ }
218
+ }
219
+ return highest;
220
+ }
221
+
222
+ export function getBundlerVersion(bundlerBinPath) {
223
+ const path = String(bundlerBinPath || '').trim();
224
+ if (!path) {
225
+ return { version: null, path: '', rawOutput: '', ok: false };
226
+ }
227
+ const result = spawnSync(path, ['--version'], { encoding: 'utf8' });
228
+ if (result.error) {
229
+ return {
230
+ version: null,
231
+ path,
232
+ rawOutput: result.error.message,
233
+ ok: false
234
+ };
235
+ }
236
+
237
+ const rawOutput = `${result.stdout || ''}\n${result.stderr || ''}`.trim();
238
+ const versionMatch = rawOutput.match(/(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/);
239
+ return {
240
+ version: versionMatch ? versionMatch[1] : null,
241
+ path,
242
+ rawOutput,
243
+ ok: result.status === 0 && Boolean(versionMatch)
244
+ };
245
+ }
246
+
247
+ export function getLocalZenithVersions({ projectRoot, bundlerBinPath } = {}) {
248
+ const resolvedBundlerBin = bundlerBinPath || resolveBundlerBin(projectRoot);
249
+ const bundlerVersion = getBundlerVersion(resolvedBundlerBin);
250
+ const versions = {
251
+ cli: readCliPackageVersion(),
252
+ projectCli: readInstalledPackageVersion('@zenithbuild/cli', projectRoot),
253
+ bundlerBinary: bundlerVersion.version,
254
+ bundlerBinPath: bundlerVersion.path,
255
+ bundlerBinRawOutput: bundlerVersion.rawOutput,
256
+ targetVersion: null
257
+ };
258
+
259
+ for (const [key, packageName] of PACKAGE_KEYS) {
260
+ versions[key] = readInstalledPackageVersion(packageName, projectRoot);
261
+ }
262
+
263
+ versions.targetVersion = determineTargetVersion(versions);
264
+ return versions;
265
+ }
266
+
267
+ export function checkCompatibility(versions) {
268
+ const targetVersion = versions?.targetVersion || determineTargetVersion(versions || {});
269
+ const issues = [];
270
+ const fixCommand = buildFixCommand(versions?.projectRoot, targetVersion);
271
+
272
+ const addIssue = (code, summary, message) => {
273
+ issues.push({
274
+ code,
275
+ summary,
276
+ message,
277
+ hint: `${fixCommand} (suppress with ZENITH_SKIP_VERSION_CHECK=1)`,
278
+ fixCommand
279
+ });
280
+ };
281
+
282
+ if (versions.projectCli && versions.cli && versions.projectCli !== versions.cli) {
283
+ const severity = classifyDifference(versions.projectCli, versions.cli);
284
+ addIssue(
285
+ severity === 'hard' ? 'CLI_TRAIN_MISMATCH' : 'CLI_OUTDATED',
286
+ `cli ${versions.cli} != project ${versions.projectCli}`,
287
+ `Version mismatch detected (may break HMR/refs): executing CLI ${versions.cli} does not match project CLI ${versions.projectCli}.`
288
+ );
289
+ }
290
+
291
+ for (const [key, label] of [
292
+ ['core', 'core'],
293
+ ['compiler', 'compiler'],
294
+ ['runtime', 'runtime'],
295
+ ['router', 'router'],
296
+ ['bundlerPackage', 'bundler package']
297
+ ]) {
298
+ const actual = versions[key];
299
+ const difference = classifyDifference(targetVersion, actual);
300
+ if (difference === 'hard') {
301
+ addIssue(
302
+ 'VERSION_TRAIN_MISMATCH',
303
+ `${label} ${actual} != ${targetVersion}`,
304
+ `Version mismatch detected (may break HMR/refs): ${label} ${actual} is on a different Zenith train than ${targetVersion}.`
305
+ );
306
+ } else if (difference === 'soft') {
307
+ addIssue(
308
+ 'VERSION_OUTDATED',
309
+ `${label} ${actual} != ${targetVersion}`,
310
+ `Version mismatch detected (may break HMR/refs): ${label} ${actual} is not aligned with ${targetVersion}.`
311
+ );
312
+ }
313
+ }
314
+
315
+ const bundlerExpected = versions.bundlerPackage || targetVersion;
316
+ const bundlerDifference = classifyDifference(bundlerExpected, versions.bundlerBinary);
317
+ if (bundlerDifference === 'hard') {
318
+ addIssue(
319
+ 'BUNDLER_BINARY_MISMATCH',
320
+ `bundler bin ${versions.bundlerBinary || 'missing'} != ${bundlerExpected}`,
321
+ `Version mismatch detected (may break build/IR contracts): bundler binary ${versions.bundlerBinary || 'missing'} does not match ${bundlerExpected}.`
322
+ );
323
+ } else if (bundlerDifference === 'soft') {
324
+ addIssue(
325
+ 'BUNDLER_BINARY_OUTDATED',
326
+ `bundler bin ${versions.bundlerBinary} != ${bundlerExpected}`,
327
+ `Version mismatch detected (may break build/IR contracts): bundler binary ${versions.bundlerBinary} is not aligned with ${bundlerExpected}.`
328
+ );
329
+ }
330
+
331
+ return {
332
+ status: issues.length === 0 ? 'ok' : 'warn',
333
+ issues,
334
+ details: {
335
+ targetVersion,
336
+ versions: {
337
+ ...versions
338
+ },
339
+ summary: describeVersions(versions)
340
+ }
341
+ };
342
+ }
343
+
344
+ export async function maybeWarnAboutZenithVersionMismatch({
345
+ projectRoot,
346
+ logger,
347
+ command = 'build',
348
+ bundlerBinPath = null
349
+ } = {}) {
350
+ if (!logger || process.env.ZENITH_SKIP_VERSION_CHECK === '1') {
351
+ return { status: 'ok', issues: [], details: {} };
352
+ }
353
+
354
+ const versions = getLocalZenithVersions({ projectRoot, bundlerBinPath });
355
+ versions.projectRoot = projectRoot;
356
+ const result = checkCompatibility(versions);
357
+ const onceKey = `zenith-version-check:${describeVersions(versions)}:${result.status}`;
358
+ const verboseTag = command === 'dev' ? 'DEV' : 'BUILD';
359
+
360
+ if (result.status === 'ok') {
361
+ logger.verbose(verboseTag, `toolchain versions ok ${result.details.summary}`);
362
+ return result;
363
+ }
364
+
365
+ const primary = result.issues[0];
366
+ logger.warn(
367
+ `${primary.message} ${summarizeIssues(result.issues)}`,
368
+ {
369
+ onceKey,
370
+ hint: primary.hint
371
+ }
372
+ );
373
+ logger.verbose(verboseTag, `toolchain versions ${result.details.summary}`);
374
+ if (versions.bundlerBinPath) {
375
+ logger.verbose(verboseTag, `bundler bin path=${versions.bundlerBinPath}`);
376
+ }
377
+ return result;
378
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/cli",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "Deterministic project orchestrator for Zenith framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  "prepublishOnly": "npm run build"
25
25
  },
26
26
  "dependencies": {
27
- "@zenithbuild/compiler": "0.6.2",
27
+ "@zenithbuild/compiler": "^0.6.5",
28
28
  "picocolors": "^1.1.1"
29
29
  },
30
30
  "devDependencies": {