@zenithbuild/cli 0.6.2 → 0.6.4

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/index.js CHANGED
@@ -116,6 +116,15 @@ export async function cli(args, cwd) {
116
116
  const outDir = join(projectRoot, 'dist');
117
117
  const config = await loadConfig(projectRoot);
118
118
 
119
+ if (command === 'build' || command === 'dev') {
120
+ const { maybeWarnAboutZenithVersionMismatch } = await import('./version-check.js');
121
+ await maybeWarnAboutZenithVersionMismatch({
122
+ projectRoot,
123
+ logger,
124
+ command
125
+ });
126
+ }
127
+
119
128
  if (command === 'build') {
120
129
  const { build } = await import('./build.js');
121
130
  logger.build('Building…');
@@ -0,0 +1,110 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const CLI_ROOT = resolve(__dirname, '..');
9
+ const localRequire = createRequire(import.meta.url);
10
+
11
+ function safeCreateRequire(projectRoot) {
12
+ if (!projectRoot) {
13
+ return localRequire;
14
+ }
15
+ try {
16
+ return createRequire(resolve(projectRoot, 'package.json'));
17
+ } catch {
18
+ return localRequire;
19
+ }
20
+ }
21
+
22
+ function safeResolve(requireFn, specifier) {
23
+ try {
24
+ return requireFn.resolve(specifier);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ export function resolveBinary(candidates) {
31
+ for (const candidate of candidates) {
32
+ if (existsSync(candidate)) {
33
+ return candidate;
34
+ }
35
+ }
36
+ return candidates[0] || '';
37
+ }
38
+
39
+ export function resolvePackageRoot(packageName, projectRoot = null) {
40
+ const projectRequire = safeCreateRequire(projectRoot);
41
+ const projectPath = safeResolve(projectRequire, `${packageName}/package.json`);
42
+ if (projectPath) {
43
+ return dirname(projectPath);
44
+ }
45
+
46
+ const localPath = safeResolve(localRequire, `${packageName}/package.json`);
47
+ return localPath ? dirname(localPath) : null;
48
+ }
49
+
50
+ export function readInstalledPackageVersion(packageName, projectRoot = null) {
51
+ const packageRoot = resolvePackageRoot(packageName, projectRoot);
52
+ if (!packageRoot) {
53
+ return null;
54
+ }
55
+ try {
56
+ const pkg = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8'));
57
+ return typeof pkg.version === 'string' ? pkg.version : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ export function readCliPackageVersion() {
64
+ try {
65
+ const pkg = JSON.parse(readFileSync(resolve(CLI_ROOT, 'package.json'), 'utf8'));
66
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
67
+ } catch {
68
+ return '0.0.0';
69
+ }
70
+ }
71
+
72
+ export function compilerBinCandidates(projectRoot = null) {
73
+ const candidates = [
74
+ resolve(CLI_ROOT, '../compiler/target/release/zenith-compiler'),
75
+ resolve(CLI_ROOT, '../zenith-compiler/target/release/zenith-compiler')
76
+ ];
77
+ const installedRoot = resolvePackageRoot('@zenithbuild/compiler', projectRoot);
78
+ if (installedRoot) {
79
+ candidates.unshift(resolve(installedRoot, 'target/release/zenith-compiler'));
80
+ }
81
+ return candidates;
82
+ }
83
+
84
+ export function resolveCompilerBin(projectRoot = null) {
85
+ return resolveBinary(compilerBinCandidates(projectRoot));
86
+ }
87
+
88
+ export function bundlerBinCandidates(projectRoot = null, env = process.env) {
89
+ const candidates = [];
90
+ const envBin = env?.ZENITH_BUNDLER_BIN;
91
+ if (typeof envBin === 'string' && envBin.length > 0) {
92
+ candidates.push(envBin);
93
+ }
94
+
95
+ const installedRoot = resolvePackageRoot('@zenithbuild/bundler', projectRoot);
96
+ if (installedRoot) {
97
+ candidates.push(resolve(installedRoot, 'target/release/zenith-bundler'));
98
+ }
99
+
100
+ candidates.push(
101
+ resolve(CLI_ROOT, '../bundler/target/release/zenith-bundler'),
102
+ resolve(CLI_ROOT, '../zenith-bundler/target/release/zenith-bundler')
103
+ );
104
+
105
+ return candidates;
106
+ }
107
+
108
+ export function resolveBundlerBin(projectRoot = null, env = process.env) {
109
+ return resolveBinary(bundlerBinCandidates(projectRoot, env));
110
+ }
@@ -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.2",
3
+ "version": "0.6.4",
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.4",
28
28
  "picocolors": "^1.1.1"
29
29
  },
30
30
  "devDependencies": {