@expressivecss/mcp-server 0.2.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.
@@ -0,0 +1,736 @@
1
+ // Generated from scripts/lib/resolve-expressivecss-version.mjs. Do not edit.
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import { lstat, open, realpath } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const PACKAGE_NAME = '@expressivecss/expressive';
8
+ const MAX_MANIFEST_BYTES = 1 * 1024 * 1024;
9
+ const MAX_LOCKFILE_BYTES = 16 * 1024 * 1024;
10
+ const MAX_FRAMEWORK_MARKER_BYTES = 2 * 1024 * 1024;
11
+
12
+ function readLimit(filePath) {
13
+ const name = path.basename(filePath);
14
+ if (['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'].includes(name)) return MAX_LOCKFILE_BYTES;
15
+ if (['llm.md', 'semantics.json'].includes(name)) return MAX_FRAMEWORK_MARKER_BYTES;
16
+ return MAX_MANIFEST_BYTES;
17
+ }
18
+
19
+ function sameFile(left, right) {
20
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size
21
+ && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
22
+ }
23
+
24
+ async function readBounded(filePath) {
25
+ let handle;
26
+ try {
27
+ const pathBefore = await lstat(filePath, { bigint: true });
28
+ if (pathBefore.isSymbolicLink()) return { error: 'symbolic-link' };
29
+ if (!pathBefore.isFile()) return { error: 'not-regular-file' };
30
+ const noFollow = fsConstants.O_NOFOLLOW ?? 0;
31
+ handle = await open(filePath, fsConstants.O_RDONLY | noFollow);
32
+ const before = await handle.stat({ bigint: true });
33
+ if (!before.isFile() || !sameFile(before, pathBefore)) return { error: 'identity-changed' };
34
+ const limit = readLimit(filePath);
35
+ if (before.size > BigInt(limit)) return { error: 'oversized' };
36
+ const buffer = Buffer.alloc(limit + 1);
37
+ let offset = 0;
38
+ while (offset < buffer.length) {
39
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
40
+ if (bytesRead === 0) break;
41
+ offset += bytesRead;
42
+ }
43
+ if (offset > limit) return { error: 'oversized' };
44
+ const after = await handle.stat({ bigint: true });
45
+ const pathAfter = await lstat(filePath, { bigint: true });
46
+ const currentResolved = await realpath(filePath);
47
+ const openedResolved = await realpath(`/proc/self/fd/${handle.fd}`).catch(() => currentResolved);
48
+ if (!sameFile(before, after) || !sameFile(after, pathAfter) || currentResolved !== openedResolved) {
49
+ return { error: 'identity-changed' };
50
+ }
51
+ return { text: buffer.subarray(0, offset).toString('utf8') };
52
+ } catch (error) {
53
+ if (error?.code === 'ENOENT') return { missing: true };
54
+ if (error?.code === 'ELOOP' || error?.code === 'EMLINK') return { error: 'symbolic-link' };
55
+ return { error: 'unreadable' };
56
+ } finally {
57
+ await handle?.close().catch(() => {});
58
+ }
59
+ }
60
+
61
+ async function readText(filePath) {
62
+ const result = await readBounded(filePath);
63
+ return result.text ?? null;
64
+ }
65
+
66
+ async function readJson(filePath) {
67
+ const text = await readText(filePath);
68
+ if (text === null) return null;
69
+ try {
70
+ return JSON.parse(text);
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function cleanVersion(value) {
77
+ if (typeof value !== 'string') return null;
78
+ const cleaned = value.trim().replace(/^['"]|['"]$/gu, '').replace(/\(.+\)$/u, '');
79
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(cleaned)
80
+ ? cleaned
81
+ : null;
82
+ }
83
+
84
+ export function parseSemver(value) {
85
+ const cleaned = cleanVersion(value);
86
+ if (!cleaned) return null;
87
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(cleaned);
88
+ return {
89
+ major: Number(match[1]),
90
+ minor: Number(match[2]),
91
+ patch: Number(match[3]),
92
+ prerelease: match[4]?.split('.') ?? [],
93
+ };
94
+ }
95
+
96
+ function versionsHaveEqualPrecedence(left, right) {
97
+ const parsedLeft = parseSemver(left);
98
+ const parsedRight = parseSemver(right);
99
+ return Boolean(parsedLeft && parsedRight && compareSemver(parsedLeft, parsedRight) === 0);
100
+ }
101
+
102
+ export function compareSemver(left, right) {
103
+ for (const key of ['major', 'minor', 'patch']) {
104
+ if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1;
105
+ }
106
+ if (left.prerelease.length === 0 || right.prerelease.length === 0) {
107
+ return left.prerelease.length === right.prerelease.length ? 0 : left.prerelease.length ? -1 : 1;
108
+ }
109
+ const length = Math.max(left.prerelease.length, right.prerelease.length);
110
+ for (let index = 0; index < length; index += 1) {
111
+ const leftPart = left.prerelease[index];
112
+ const rightPart = right.prerelease[index];
113
+ if (leftPart === undefined || rightPart === undefined) return leftPart === undefined ? -1 : 1;
114
+ if (leftPart === rightPart) continue;
115
+ const leftNumber = /^\d+$/u.test(leftPart) ? BigInt(leftPart) : null;
116
+ const rightNumber = /^\d+$/u.test(rightPart) ? BigInt(rightPart) : null;
117
+ if (leftNumber !== null && rightNumber !== null) return leftNumber < rightNumber ? -1 : 1;
118
+ if (leftNumber !== null || rightNumber !== null) return leftNumber !== null ? -1 : 1;
119
+ return leftPart < rightPart ? -1 : 1;
120
+ }
121
+ return 0;
122
+ }
123
+
124
+ function satisfiesComparator(version, operator, boundary) {
125
+ const comparison = compareSemver(version, boundary);
126
+ if (operator === '>') return comparison > 0;
127
+ if (operator === '>=') return comparison >= 0;
128
+ if (operator === '<') return comparison < 0;
129
+ if (operator === '<=') return comparison <= 0;
130
+ return comparison === 0;
131
+ }
132
+
133
+ function satisfiesSimpleRange(version, range) {
134
+ if (/^(?:\*|x)$/iu.test(range)) return true;
135
+ const wildcard = /^(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?$/iu.exec(range);
136
+ if (wildcard) {
137
+ const [, major, minor, patch] = wildcard;
138
+ return version.major === Number(major)
139
+ && (minor === undefined || /^(?:x|\*)$/iu.test(minor) || version.minor === Number(minor))
140
+ && (patch === undefined || /^(?:x|\*)$/iu.test(patch) || version.patch === Number(patch))
141
+ && version.prerelease.length === 0;
142
+ }
143
+
144
+ const shorthand = /^(\^|~)(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/u.exec(range);
145
+ if (shorthand) {
146
+ const lower = parseSemver(shorthand[2]);
147
+ if (!lower) return null;
148
+ const upper = shorthand[1] === '~'
149
+ ? { major: lower.major, minor: lower.minor + 1, patch: 0, prerelease: [] }
150
+ : lower.major > 0
151
+ ? { major: lower.major + 1, minor: 0, patch: 0, prerelease: [] }
152
+ : lower.minor > 0
153
+ ? { major: 0, minor: lower.minor + 1, patch: 0, prerelease: [] }
154
+ : { major: 0, minor: 0, patch: lower.patch + 1, prerelease: [] };
155
+ return compareSemver(version, lower) >= 0 && compareSemver(version, upper) < 0;
156
+ }
157
+
158
+ const comparator = /^(>=|<=|>|<|=)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/u.exec(range);
159
+ if (!comparator) return null;
160
+ const boundary = parseSemver(comparator[2]);
161
+ return boundary ? satisfiesComparator(version, comparator[1] ?? '=', boundary) : null;
162
+ }
163
+
164
+ function versionSatisfiesRange(versionValue, rangeValue) {
165
+ const version = parseSemver(versionValue);
166
+ if (!version || typeof rangeValue !== 'string' || !rangeValue.trim()) return null;
167
+ const alternatives = rangeValue.trim().split(/\s*\|\|\s*/u);
168
+ let unsupported = false;
169
+ for (const alternative of alternatives) {
170
+ const comparators = alternative.trim().split(/\s+/u).filter(Boolean);
171
+ if (comparators.length === 0) {
172
+ unsupported = true;
173
+ continue;
174
+ }
175
+ const prereleaseCore = `${version.major}.${version.minor}.${version.patch}-`;
176
+ if (version.prerelease.length > 0 && !alternative.includes(prereleaseCore)) continue;
177
+ const results = comparators.map((comparator) => satisfiesSimpleRange(version, comparator));
178
+ if (results.every((result) => result === true)) return true;
179
+ if (results.some((result) => result === null)) unsupported = true;
180
+ }
181
+ return unsupported ? null : false;
182
+ }
183
+
184
+ function manifestRange(manifest) {
185
+ return manifest?.dependencies?.[PACKAGE_NAME]
186
+ ?? manifest?.devDependencies?.[PACKAGE_NAME]
187
+ ?? manifest?.peerDependencies?.[PACKAGE_NAME]
188
+ ?? manifest?.optionalDependencies?.[PACKAGE_NAME]
189
+ ?? null;
190
+ }
191
+
192
+ function verifiedLockVersion(version, declaredRange, source, file) {
193
+ const satisfies = versionSatisfiesRange(version, declaredRange);
194
+ if (satisfies === true) return { version, diagnostics: [] };
195
+ return {
196
+ version: null,
197
+ diagnostics: [{
198
+ code: satisfies === false ? 'lockfile-version-outside-manifest-range' : 'unsupported-manifest-range',
199
+ severity: 'blocked',
200
+ source,
201
+ path: file,
202
+ message: satisfies === false
203
+ ? `${file} resolves ${version}, outside manifest range ${JSON.stringify(declaredRange)}.`
204
+ : `Cannot verify ${file} version ${version} against unsupported manifest range ${JSON.stringify(declaredRange)}.`,
205
+ }],
206
+ };
207
+ }
208
+
209
+ function npmLockResolution(lock, declaredRange) {
210
+ if (lock?.lockfileVersion === 1) {
211
+ const entry = lock?.dependencies?.[PACKAGE_NAME];
212
+ const version = cleanVersion(entry?.version);
213
+ if (version) return verifiedLockVersion(version, declaredRange, 'npm', 'package-lock.json');
214
+ return entry
215
+ ? { version: null, diagnostics: [malformedLockVersion('npm', 'package-lock.json')] }
216
+ : { version: null, diagnostics: [directEntryMissing('npm', 'package-lock.json')] };
217
+ }
218
+
219
+ const lockedRange = manifestRange(lock?.packages?.['']);
220
+ if (!lockedRange) {
221
+ return { version: null, diagnostics: [directEntryMissing('npm', 'package-lock.json')] };
222
+ }
223
+ if (lockedRange !== declaredRange) {
224
+ return {
225
+ version: null,
226
+ diagnostics: [{
227
+ code: 'lockfile-declaration-mismatch',
228
+ severity: 'warning',
229
+ source: 'npm',
230
+ path: 'package-lock.json',
231
+ message: `package-lock.json declares ${JSON.stringify(lockedRange)}, not manifest range ${JSON.stringify(declaredRange)}.`,
232
+ }],
233
+ };
234
+ }
235
+ const version = cleanVersion(lock?.packages?.[`node_modules/${PACKAGE_NAME}`]?.version);
236
+ return version
237
+ ? verifiedLockVersion(version, declaredRange, 'npm', 'package-lock.json')
238
+ : { version: null, diagnostics: [malformedLockVersion('npm', 'package-lock.json')] };
239
+ }
240
+
241
+ function directEntryMissing(source, file) {
242
+ return {
243
+ code: 'direct-lock-entry-missing',
244
+ severity: 'warning',
245
+ source,
246
+ path: file,
247
+ message: `${file} has no matching direct ${PACKAGE_NAME} entry.`,
248
+ };
249
+ }
250
+
251
+ function malformedLockVersion(source, file) {
252
+ return {
253
+ code: 'malformed-lockfile',
254
+ severity: 'blocked',
255
+ source,
256
+ path: file,
257
+ message: `${file} has no valid resolved ExpressiveCSS version for its direct entry.`,
258
+ };
259
+ }
260
+
261
+ function pnpmLockResolution(text, declaredRange) {
262
+ const lines = text.split(/\r?\n/u);
263
+ if (lines.some((line) => /^\t|^ +\t/u.test(line))) {
264
+ return {
265
+ version: null,
266
+ diagnostics: [{
267
+ code: 'malformed-lockfile',
268
+ severity: 'blocked',
269
+ source: 'pnpm',
270
+ path: 'pnpm-lock.yaml',
271
+ message: 'pnpm-lock.yaml uses tab indentation.',
272
+ }],
273
+ };
274
+ }
275
+ const lockfileVersion = /^lockfileVersion:\s*['"]?([^'"\s]+)['"]?\s*$/u.exec(lines.find((line) => line.startsWith('lockfileVersion:')) ?? '')?.[1];
276
+ const major = Number.parseInt(lockfileVersion ?? '', 10);
277
+ if (!Number.isInteger(major) || major < 6 || major > 9) {
278
+ return {
279
+ version: null,
280
+ diagnostics: [{
281
+ code: lockfileVersion ? 'unsupported-lockfile-version' : 'malformed-lockfile',
282
+ severity: 'blocked',
283
+ source: 'pnpm',
284
+ path: 'pnpm-lock.yaml',
285
+ message: lockfileVersion
286
+ ? `Unsupported pnpm lockfile version ${JSON.stringify(lockfileVersion)}.`
287
+ : 'pnpm-lock.yaml has no valid lockfileVersion.',
288
+ }],
289
+ };
290
+ }
291
+
292
+ const importersIndex = lines.findIndex((line) => line === 'importers:');
293
+ const targetIndex = lines.findIndex((line, index) => index > importersIndex && /^ {2}(?:['"]\.['"]|\.):\s*$/u.test(line));
294
+ if (importersIndex < 0 || targetIndex < 0) {
295
+ return { version: null, diagnostics: [directEntryMissing('pnpm', 'pnpm-lock.yaml')] };
296
+ }
297
+ let targetEnd = lines.length;
298
+ for (let index = targetIndex + 1; index < lines.length; index += 1) {
299
+ if (lines[index].trim() && /^\s*/u.exec(lines[index])[0].length <= 2) {
300
+ targetEnd = index;
301
+ break;
302
+ }
303
+ }
304
+ const targetLines = lines.slice(targetIndex + 1, targetEnd);
305
+ const dependencyIndex = targetLines.findIndex((line) => /^ {6}['"]?@expressivecss\/expressive['"]?:\s*$/u.test(line));
306
+ if (dependencyIndex < 0) {
307
+ return { version: null, diagnostics: [directEntryMissing('pnpm', 'pnpm-lock.yaml')] };
308
+ }
309
+ const dependencyLines = [];
310
+ for (let index = dependencyIndex + 1; index < targetLines.length; index += 1) {
311
+ if (targetLines[index].trim() && /^\s*/u.exec(targetLines[index])[0].length <= 6) break;
312
+ dependencyLines.push(targetLines[index]);
313
+ }
314
+ const specifier = /^\s+specifier:\s*['"]?([^'"\s]+)['"]?\s*$/mu.exec(dependencyLines.join('\n'))?.[1] ?? null;
315
+ const rawVersion = /^\s+version:\s*['"]?([^'"\s]+)['"]?\s*$/mu.exec(dependencyLines.join('\n'))?.[1] ?? null;
316
+ if (specifier !== declaredRange) {
317
+ return {
318
+ version: null,
319
+ diagnostics: [{
320
+ code: 'lockfile-declaration-mismatch',
321
+ severity: 'warning',
322
+ source: 'pnpm',
323
+ path: 'pnpm-lock.yaml',
324
+ message: `pnpm-lock.yaml declares ${JSON.stringify(specifier)}, not manifest range ${JSON.stringify(declaredRange)}.`,
325
+ }],
326
+ };
327
+ }
328
+ const version = cleanVersion(rawVersion);
329
+ return version
330
+ ? verifiedLockVersion(version, declaredRange, 'pnpm', 'pnpm-lock.yaml')
331
+ : {
332
+ version: null,
333
+ diagnostics: [{
334
+ code: 'malformed-lockfile',
335
+ severity: 'blocked',
336
+ source: 'pnpm',
337
+ path: 'pnpm-lock.yaml',
338
+ message: 'The target pnpm importer has no valid resolved ExpressiveCSS version.',
339
+ }],
340
+ };
341
+ }
342
+
343
+ function yarnLockResolution(text, declaredRange) {
344
+ const classic = /^# yarn lockfile v1\s*$/mu.test(text);
345
+ const berry = /^__metadata:\s*$/mu.test(text);
346
+ if (!classic && !berry) {
347
+ return {
348
+ version: null,
349
+ diagnostics: [{
350
+ code: 'malformed-lockfile',
351
+ severity: 'blocked',
352
+ source: 'yarn',
353
+ path: 'yarn.lock',
354
+ message: 'yarn.lock has no supported Classic or Berry format marker.',
355
+ }],
356
+ };
357
+ }
358
+
359
+ const expected = new Set([
360
+ `${PACKAGE_NAME}@${declaredRange}`,
361
+ `${PACKAGE_NAME}@npm:${declaredRange}`,
362
+ ]);
363
+ const lines = text.split(/\r?\n/u);
364
+ for (let index = 0; index < lines.length; index += 1) {
365
+ const header = lines[index];
366
+ if (!header.trim() || /^\s|#/u.test(header) || !header.endsWith(':')) continue;
367
+ const descriptors = [...header.slice(0, -1).matchAll(/"([^"]+)"|'([^']+)'|([^,\s]+)/gu)]
368
+ .map((match) => match[1] ?? match[2] ?? match[3]);
369
+ if (!descriptors.some((descriptor) => expected.has(descriptor))) continue;
370
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
371
+ const line = lines[cursor];
372
+ if (line.trim() && !/^\s/u.test(line)) break;
373
+ const rawVersion = /^\s+version(?:\s+|:\s*)["']?([^\s"']+)["']?\s*$/u.exec(line)?.[1];
374
+ if (!rawVersion) continue;
375
+ const version = cleanVersion(rawVersion);
376
+ if (version) return verifiedLockVersion(version, declaredRange, 'yarn', 'yarn.lock');
377
+ break;
378
+ }
379
+ return {
380
+ version: null,
381
+ diagnostics: [{
382
+ code: 'malformed-lockfile',
383
+ severity: 'blocked',
384
+ source: 'yarn',
385
+ path: 'yarn.lock',
386
+ message: 'The matching Yarn descriptor has no valid resolved ExpressiveCSS version.',
387
+ }],
388
+ };
389
+ }
390
+ return { version: null, diagnostics: [directEntryMissing('yarn', 'yarn.lock')] };
391
+ }
392
+
393
+ async function resolveLockfile(projectRoot, packageManagerField = null, declaredRange = null) {
394
+ const declaredManager = typeof packageManagerField === 'string'
395
+ ? /^(npm|pnpm|yarn)@/u.exec(packageManagerField)?.[1] ?? null
396
+ : null;
397
+ const lockfiles = [
398
+ { manager: 'npm', file: 'package-lock.json' },
399
+ { manager: 'pnpm', file: 'pnpm-lock.yaml' },
400
+ { manager: 'yarn', file: 'yarn.lock' },
401
+ ];
402
+ const reads = {};
403
+ for (const entry of lockfiles) {
404
+ reads[entry.manager] = { ...entry, ...(await readBounded(path.join(projectRoot, entry.file))) };
405
+ }
406
+ const unreadable = lockfiles
407
+ .map((entry) => reads[entry.manager])
408
+ .filter((entry) => entry.error);
409
+ if (unreadable.length > 0) {
410
+ return {
411
+ packageManager: declaredManager ?? unreadable[0].manager,
412
+ version: null,
413
+ file: unreadable[0].file,
414
+ diagnostics: unreadable.map((entry) => ({
415
+ code: 'unreadable-lockfile',
416
+ severity: 'blocked',
417
+ source: entry.manager,
418
+ path: entry.file,
419
+ message: `${entry.file} could not be read as a regular file (${entry.error}).`,
420
+ })),
421
+ };
422
+ }
423
+ const npmText = reads.npm.text ?? null;
424
+ let npmLock = null;
425
+ if (npmText !== null) {
426
+ try {
427
+ npmLock = JSON.parse(npmText);
428
+ } catch {
429
+ npmLock = null;
430
+ }
431
+ }
432
+ const pnpmText = reads.pnpm.text ?? null;
433
+ const yarnText = reads.yarn.text ?? null;
434
+ const existing = [
435
+ npmText !== null ? 'npm' : null,
436
+ pnpmText !== null ? 'pnpm' : null,
437
+ yarnText !== null ? 'yarn' : null,
438
+ ].filter(Boolean);
439
+
440
+ if (existing.length > 1 && !declaredManager) {
441
+ return {
442
+ packageManager: 'unknown',
443
+ version: null,
444
+ file: null,
445
+ diagnostics: [{
446
+ code: 'ambiguous-lockfiles',
447
+ severity: 'blocked',
448
+ source: 'lockfile',
449
+ path: null,
450
+ message: 'Multiple lockfiles exist and package.json does not select one.',
451
+ }],
452
+ };
453
+ }
454
+
455
+ const selectedManager = declaredManager ?? existing[0] ?? null;
456
+ if (selectedManager === 'npm' && npmText !== null && !npmLock) {
457
+ return {
458
+ packageManager: 'npm',
459
+ version: null,
460
+ file: 'package-lock.json',
461
+ diagnostics: [{
462
+ code: 'malformed-lockfile',
463
+ severity: 'blocked',
464
+ source: 'npm',
465
+ path: 'package-lock.json',
466
+ message: 'package-lock.json is not valid JSON.',
467
+ }],
468
+ };
469
+ }
470
+ if (selectedManager === 'npm' && ![1, 2, 3].includes(npmLock?.lockfileVersion)) {
471
+ return {
472
+ packageManager: 'npm',
473
+ version: null,
474
+ file: 'package-lock.json',
475
+ diagnostics: [{
476
+ code: 'unsupported-lockfile-version',
477
+ severity: 'blocked',
478
+ source: 'npm',
479
+ path: 'package-lock.json',
480
+ message: `Unsupported npm lockfile version ${JSON.stringify(npmLock?.lockfileVersion)}.`,
481
+ }],
482
+ };
483
+ }
484
+ if (declaredManager && !existing.includes(declaredManager) && existing.length > 0) {
485
+ const selectedFile = {
486
+ npm: 'package-lock.json',
487
+ pnpm: 'pnpm-lock.yaml',
488
+ yarn: 'yarn.lock',
489
+ }[declaredManager];
490
+ return {
491
+ packageManager: declaredManager,
492
+ version: null,
493
+ file: selectedFile,
494
+ diagnostics: [{
495
+ code: 'selected-lockfile-missing',
496
+ severity: 'blocked',
497
+ source: declaredManager,
498
+ path: selectedFile,
499
+ message: `package.json selects ${declaredManager}, but ${selectedFile} is missing.`,
500
+ }],
501
+ };
502
+ }
503
+
504
+ if (declaredManager === 'pnpm' && pnpmText !== null) {
505
+ return { packageManager: 'pnpm', file: 'pnpm-lock.yaml', ...pnpmLockResolution(pnpmText, declaredRange) };
506
+ }
507
+ if (declaredManager === 'yarn' && yarnText !== null) {
508
+ return { packageManager: 'yarn', file: 'yarn.lock', ...yarnLockResolution(yarnText, declaredRange) };
509
+ }
510
+ if (declaredManager === 'npm' && npmLock) {
511
+ return { packageManager: 'npm', file: 'package-lock.json', ...npmLockResolution(npmLock, declaredRange) };
512
+ }
513
+
514
+ if (npmLock) {
515
+ return { packageManager: 'npm', file: 'package-lock.json', ...npmLockResolution(npmLock, declaredRange) };
516
+ }
517
+ if (pnpmText !== null) {
518
+ return { packageManager: 'pnpm', file: 'pnpm-lock.yaml', ...pnpmLockResolution(pnpmText, declaredRange) };
519
+ }
520
+ if (yarnText !== null) {
521
+ return { packageManager: 'yarn', file: 'yarn.lock', ...yarnLockResolution(yarnText, declaredRange) };
522
+ }
523
+
524
+ return { packageManager: declaredManager ?? 'unknown', version: null, file: null };
525
+ }
526
+
527
+ async function bundledContract(explicitPath = null) {
528
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
529
+ const candidates = [
530
+ explicitPath,
531
+ path.resolve(scriptDirectory, '../references/contract.json'),
532
+ path.resolve(scriptDirectory, '../contract.json'),
533
+ ].filter(Boolean);
534
+ for (const candidate of candidates) {
535
+ const contract = await readJson(candidate);
536
+ if (contract?.frameworkVersion) return contract;
537
+ }
538
+ return null;
539
+ }
540
+
541
+ export async function resolveExpressiveVersion({
542
+ projectRoot = process.cwd(),
543
+ contractVersion = null,
544
+ skillVersion = null,
545
+ contractManifestPath = null,
546
+ } = {}) {
547
+ const root = path.resolve(projectRoot);
548
+ const manifest = await readJson(path.join(root, 'package.json'));
549
+ const declaredRange = manifestRange(manifest);
550
+ const lock = await resolveLockfile(root, manifest?.packageManager, declaredRange);
551
+ const isFrameworkSource = manifest?.name === PACKAGE_NAME
552
+ && cleanVersion(manifest.version)
553
+ && (await Promise.all([
554
+ readText(path.join(root, 'llm.md')),
555
+ readText(path.join(root, 'semantics.json')),
556
+ readText(path.join(root, 'docs', 'src', 'data', 'nav.ts')),
557
+ ])).every((source) => source !== null);
558
+ const warnings = [];
559
+ const diagnostics = isFrameworkSource || !declaredRange ? [] : [...(lock.diagnostics ?? [])];
560
+ const candidates = [];
561
+ const conflicts = [];
562
+
563
+ let resolvedVersion = null;
564
+ let resolutionSource = 'none';
565
+
566
+ if (isFrameworkSource) {
567
+ resolvedVersion = cleanVersion(manifest.version);
568
+ resolutionSource = 'framework-source';
569
+ } else if (declaredRange) {
570
+ const installedRelativePath = 'node_modules/@expressivecss/expressive/package.json';
571
+ const installedText = await readText(path.join(root, installedRelativePath));
572
+ let installed = null;
573
+ if (installedText !== null) {
574
+ try {
575
+ installed = JSON.parse(installedText);
576
+ } catch {
577
+ installed = null;
578
+ }
579
+ }
580
+ const installedVersion = installed?.name === PACKAGE_NAME ? cleanVersion(installed.version) : null;
581
+ if (installedText !== null && !installedVersion) {
582
+ resolutionSource = 'manifest-only';
583
+ diagnostics.push({
584
+ code: 'malformed-installed-metadata',
585
+ severity: 'blocked',
586
+ source: 'installed-package',
587
+ path: installedRelativePath,
588
+ message: 'Installed ExpressiveCSS package metadata has no valid package name and version.',
589
+ });
590
+ } else if (installedVersion) {
591
+ resolvedVersion = installedVersion;
592
+ resolutionSource = 'installed-package';
593
+ const installedCandidate = {
594
+ source: 'installed-package',
595
+ version: installedVersion,
596
+ path: installedRelativePath,
597
+ };
598
+ candidates.push(installedCandidate);
599
+ const installedSatisfiesRange = versionSatisfiesRange(installedVersion, declaredRange);
600
+ if (installedSatisfiesRange !== true) {
601
+ diagnostics.push({
602
+ code: installedSatisfiesRange === false
603
+ ? 'installed-version-outside-manifest-range'
604
+ : 'unsupported-manifest-range',
605
+ severity: 'blocked',
606
+ source: 'installed-package',
607
+ path: installedRelativePath,
608
+ message: installedSatisfiesRange === false
609
+ ? `Installed ExpressiveCSS ${installedVersion} does not satisfy the direct manifest range ${declaredRange}.`
610
+ : `Cannot verify installed ExpressiveCSS ${installedVersion} against unsupported manifest range ${declaredRange}.`,
611
+ });
612
+ }
613
+ if (lock.version && lock.version !== installedVersion) {
614
+ const lockCandidate = { source: 'lockfile', version: lock.version, path: lock.file };
615
+ candidates.push(lockCandidate);
616
+ conflicts.push({
617
+ code: 'installed-lockfile-version-conflict',
618
+ selected: installedCandidate,
619
+ other: lockCandidate,
620
+ });
621
+ diagnostics.push({
622
+ code: 'installed-lockfile-version-conflict',
623
+ severity: 'blocked',
624
+ source: 'resolution',
625
+ path: lock.file,
626
+ message: `Installed ExpressiveCSS ${installedVersion} differs from ${lock.file} version ${lock.version}.`,
627
+ });
628
+ warnings.push(`Installed ExpressiveCSS ${installedVersion} differs from ${lock.file} version ${lock.version}.`);
629
+ }
630
+ } else if (lock.version) {
631
+ resolvedVersion = lock.version;
632
+ resolutionSource = 'lockfile';
633
+ candidates.push({ source: 'lockfile', version: lock.version, path: lock.file });
634
+ } else {
635
+ resolutionSource = 'manifest-only';
636
+ }
637
+ }
638
+
639
+ const contract = await bundledContract(contractManifestPath);
640
+ const bundledVersion = cleanVersion(contract?.frameworkVersion ?? '');
641
+ const normalizedContract = cleanVersion(contractVersion ?? contract?.frameworkVersion ?? '');
642
+ const normalizedSkillVersion = cleanVersion(skillVersion ?? contract?.skillVersion ?? '');
643
+ const hasBlockingDiagnostic = diagnostics.some((diagnostic) => diagnostic.severity === 'blocked');
644
+ const status = hasBlockingDiagnostic || !resolvedVersion || !normalizedContract
645
+ ? 'unresolved'
646
+ : versionsHaveEqualPrecedence(resolvedVersion, normalizedContract)
647
+ ? 'match'
648
+ : 'mismatch';
649
+ const matchingTag = resolvedVersion && Array.isArray(contract?.releaseTags)
650
+ && contract.releaseTags.includes(`v${resolvedVersion}`)
651
+ ? `v${resolvedVersion}`
652
+ : null;
653
+ const bundledContractSafe = status === 'match'
654
+ && versionsHaveEqualPrecedence(normalizedContract, bundledVersion);
655
+ const documentationMode = bundledContractSafe
656
+ ? 'bundled'
657
+ : matchingTag
658
+ ? 'matching-tag'
659
+ : resolutionSource === 'installed-package'
660
+ ? 'installed-package'
661
+ : 'unavailable';
662
+ const documentationSources = {
663
+ bundled: {
664
+ available: bundledContractSafe,
665
+ frameworkVersion: bundledVersion,
666
+ sourceHash: contract?.sourceHash ?? null,
667
+ },
668
+ current: {
669
+ // Local version agreement does not verify what the public site currently serves.
670
+ available: false,
671
+ url: 'https://www.expressivecss.com',
672
+ },
673
+ matchingTag: {
674
+ available: Boolean(matchingTag),
675
+ url: matchingTag ? `https://github.com/BaezFJ/ExpressiveCSS/tree/${matchingTag}` : null,
676
+ },
677
+ installedPackage: {
678
+ available: resolutionSource === 'installed-package',
679
+ path: resolutionSource === 'installed-package' ? 'node_modules/@expressivecss/expressive' : null,
680
+ componentDocumentationAvailable: false,
681
+ },
682
+ };
683
+ diagnostics.sort((left, right) => `${left.code}\0${left.path ?? ''}\0${left.message}`
684
+ .localeCompare(`${right.code}\0${right.path ?? ''}\0${right.message}`));
685
+ warnings.sort();
686
+
687
+ return {
688
+ projectRoot: root,
689
+ packageManager: lock.packageManager,
690
+ declaredRange,
691
+ declaredVersion: declaredRange,
692
+ resolvedVersion,
693
+ exactInstalledVersion: resolutionSource === 'installed-package' ? resolvedVersion : null,
694
+ resolutionSource,
695
+ skillVersion: normalizedSkillVersion,
696
+ skillContractVersion: normalizedContract,
697
+ contractVersion: normalizedContract,
698
+ contractSourceHash: contract?.sourceHash ?? null,
699
+ matchingTag,
700
+ matchingCommit: null,
701
+ status,
702
+ contractStatus: status,
703
+ documentationMode,
704
+ documentationSources,
705
+ bundledContractSafe,
706
+ currentDocsSafe: false,
707
+ warnings,
708
+ diagnostics,
709
+ candidates,
710
+ conflicts,
711
+ };
712
+ }
713
+
714
+ function cliArguments(argv) {
715
+ const values = {};
716
+ for (let index = 0; index < argv.length; index += 1) {
717
+ const argument = argv[index];
718
+ if (!argument.startsWith('--')) continue;
719
+ const [key, ...rest] = argument.slice(2).split('=');
720
+ if (rest.length) values[key] = rest.join('=');
721
+ else if (argv[index + 1] && !argv[index + 1].startsWith('--')) values[key] = argv[index += 1];
722
+ else values[key] = true;
723
+ }
724
+ return values;
725
+ }
726
+
727
+ const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
728
+ if (invokedPath === fileURLToPath(import.meta.url)) {
729
+ const args = cliArguments(process.argv.slice(2));
730
+ const result = await resolveExpressiveVersion({
731
+ projectRoot: typeof args['project-root'] === 'string' ? args['project-root'] : process.cwd(),
732
+ contractVersion: typeof args['contract-version'] === 'string' ? args['contract-version'] : null,
733
+ skillVersion: typeof args['skill-version'] === 'string' ? args['skill-version'] : null,
734
+ });
735
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
736
+ }