@nimbus-sh/core 0.2.0 → 0.4.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/dist/_shared/tarball-stream.d.ts +93 -0
  4. package/dist/_shared/tarball-stream.d.ts.map +1 -0
  5. package/dist/_shared/tarball-stream.js +235 -0
  6. package/dist/_shared/tarball.d.ts +17 -0
  7. package/dist/_shared/tarball.d.ts.map +1 -0
  8. package/dist/_shared/tarball.js +39 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/runtime/clang-runner.d.ts +38 -0
  13. package/dist/runtime/clang-runner.d.ts.map +1 -0
  14. package/dist/runtime/clang-runner.js +866 -0
  15. package/dist/runtime/facet-host.d.ts +12 -0
  16. package/dist/runtime/facet-host.d.ts.map +1 -1
  17. package/dist/runtime/local-facet-host.d.ts.map +1 -1
  18. package/dist/runtime/local-facet-host.js +29 -9
  19. package/dist/runtime/ruby-gems.d.ts +30 -0
  20. package/dist/runtime/ruby-gems.d.ts.map +1 -0
  21. package/dist/runtime/ruby-gems.js +636 -0
  22. package/dist/runtime/ruby-runner.d.ts +127 -0
  23. package/dist/runtime/ruby-runner.d.ts.map +1 -0
  24. package/dist/runtime/ruby-runner.js +1357 -0
  25. package/dist/runtime/runtime-package.d.ts +63 -0
  26. package/dist/runtime/runtime-package.d.ts.map +1 -0
  27. package/dist/runtime/runtime-package.js +66 -0
  28. package/dist/runtime/runtime-registry.d.ts +3 -2
  29. package/dist/runtime/runtime-registry.d.ts.map +1 -1
  30. package/dist/runtime/runtime-registry.js +1 -1
  31. package/dist/runtime/session-process-supervisor.d.ts +15 -0
  32. package/dist/runtime/session-process-supervisor.d.ts.map +1 -1
  33. package/dist/runtime/session-process-supervisor.js +30 -0
  34. package/dist/workspace/nimbus-workspace.d.ts +102 -22
  35. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  36. package/dist/workspace/nimbus-workspace.js +197 -52
  37. package/package.json +4 -2
  38. package/src/_shared/tarball-stream.ts +263 -0
  39. package/src/_shared/tarball.ts +46 -0
  40. package/src/index.ts +7 -0
  41. package/src/runtime/clang-runner.ts +924 -0
  42. package/src/runtime/facet-host.ts +12 -0
  43. package/src/runtime/local-facet-host.ts +28 -8
  44. package/src/runtime/ruby-gems.ts +682 -0
  45. package/src/runtime/ruby-runner.ts +1484 -0
  46. package/src/runtime/runtime-package.ts +114 -0
  47. package/src/runtime/runtime-registry.ts +4 -3
  48. package/src/runtime/session-process-supervisor.ts +27 -0
  49. package/src/workspace/nimbus-workspace.ts +268 -63
@@ -0,0 +1,636 @@
1
+ import { extractTarball } from '../_shared/tarball.js';
2
+ import { normalizeVfsPath, parentVfsPath, resolveVfsPath } from '../vfs/path.js';
3
+ const RUBYGEMS_API = 'https://rubygems.org';
4
+ const DEFAULT_GEM_HOME = 'home/user/.gem';
5
+ export function defaultGemHome() {
6
+ return DEFAULT_GEM_HOME;
7
+ }
8
+ export function installedGemLibRoots(vfs, gemHome = DEFAULT_GEM_HOME) {
9
+ const gemsRoot = `${gemHome}/gems`;
10
+ if (!vfs.exists(gemsRoot))
11
+ return [];
12
+ const out = [];
13
+ for (const entry of vfs.readdir(gemsRoot)) {
14
+ if (entry.type !== 'directory')
15
+ continue;
16
+ const lib = `${gemsRoot}/${entry.name}/lib`;
17
+ if (vfs.exists(lib) && vfs.isDirectory(lib))
18
+ out.push('/' + lib);
19
+ }
20
+ return out.sort();
21
+ }
22
+ export function installedGemBins(vfs, gemHome = DEFAULT_GEM_HOME) {
23
+ const binRoot = `${normalizeVfsPath(gemHome)}/bin`;
24
+ if (!vfs.exists(binRoot) || !vfs.isDirectory(binRoot))
25
+ return [];
26
+ return vfs.readdir(binRoot)
27
+ .filter((entry) => entry.type === 'file' && isValidGemExecutableName(entry.name))
28
+ .map((entry) => ({ name: entry.name, path: `${binRoot}/${entry.name}` }))
29
+ .sort((a, b) => a.name.localeCompare(b.name));
30
+ }
31
+ export async function installRubyGems(vfs, requests, opts = {}) {
32
+ const gemHome = normalizeVfsPath(opts.gemHome || DEFAULT_GEM_HOME);
33
+ const includeDependencies = opts.includeDependencies !== false;
34
+ const report = { installed: [], alreadyInstalled: [] };
35
+ const visiting = new Set();
36
+ ensureDir(vfs, `${gemHome}/gems`);
37
+ ensureDir(vfs, `${gemHome}/specifications`);
38
+ ensureDir(vfs, `${gemHome}/cache`);
39
+ ensureDir(vfs, `${gemHome}/bin`);
40
+ for (const req of requests) {
41
+ await installOneGem(vfs, req, {
42
+ gemHome,
43
+ includeDependencies,
44
+ report,
45
+ visiting,
46
+ });
47
+ }
48
+ return report;
49
+ }
50
+ export async function installRubyBundle(vfs, cwd, opts = {}) {
51
+ const gemfilePath = resolveVfsPath('Gemfile', cwd);
52
+ if (!vfs.exists(gemfilePath)) {
53
+ throw new Error('Gemfile not found');
54
+ }
55
+ const text = new TextDecoder('utf-8').decode(vfs.readFile(gemfilePath));
56
+ const requests = parseGemfile(text);
57
+ if (requests.length === 0) {
58
+ throw new Error('Gemfile has no supported gem declarations');
59
+ }
60
+ const report = await installRubyGems(vfs, requests, {
61
+ gemHome: opts.gemHome,
62
+ includeDependencies: true,
63
+ });
64
+ const lockfilePath = resolveVfsPath('Gemfile.lock', cwd);
65
+ const all = readInstalledGemRecords(vfs, normalizeVfsPath(opts.gemHome || DEFAULT_GEM_HOME));
66
+ const specs = all
67
+ .sort((a, b) => a.name.localeCompare(b.name) || compareVersions(a.version, b.version))
68
+ .map((g) => ` ${g.name} (${g.version})`)
69
+ .join('\n');
70
+ vfs.writeFile(lockfilePath, [
71
+ 'GEM',
72
+ ' remote: https://rubygems.org/',
73
+ ' specs:',
74
+ specs || ' (none)',
75
+ '',
76
+ 'DEPENDENCIES',
77
+ ...requests.map((r) => ` ${r.name}${r.requirements.length ? ' (' + r.requirements.join(', ') + ')' : ''}`),
78
+ '',
79
+ 'BUNDLED WITH',
80
+ ' Nimbus RubyGems',
81
+ '',
82
+ ].join('\n'));
83
+ return { requests, report, lockfilePath };
84
+ }
85
+ export function parseGemfile(text) {
86
+ const out = [];
87
+ for (const raw of text.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n')) {
88
+ const line = trimRubyComment(raw).trim();
89
+ if (!line)
90
+ continue;
91
+ const parsed = parseGemDslLine(line);
92
+ if (parsed)
93
+ out.push(parsed);
94
+ }
95
+ return out;
96
+ }
97
+ function parseGemDslLine(line) {
98
+ const argsText = gemCallArgsText(line);
99
+ if (argsText == null)
100
+ return null;
101
+ const args = splitTopLevelRubyArgs(argsText);
102
+ if (args.length === 0)
103
+ return null;
104
+ const first = parseRubyStringLiteral(args[0]);
105
+ if (!first || first.includes('/'))
106
+ return null;
107
+ const requirements = [];
108
+ for (let i = 1; i < args.length; i++) {
109
+ const arg = args[i].trim();
110
+ if (!arg)
111
+ continue;
112
+ if (isRubyKeywordArg(arg)) {
113
+ const key = rubyKeywordName(arg);
114
+ if (key === 'git' || key === 'github' || key === 'path') {
115
+ throw new Error(`Gemfile entry for ${first} uses unsupported '${key}' source; Nimbus bundle install supports RubyGems.org gems only`);
116
+ }
117
+ continue;
118
+ }
119
+ const req = parseRubyStringLiteral(arg);
120
+ if (!req) {
121
+ throw new Error(`Gemfile entry for ${first} has an unsupported argument: ${arg}`);
122
+ }
123
+ requirements.push(req);
124
+ }
125
+ return { name: first, requirements };
126
+ }
127
+ function gemCallArgsText(line) {
128
+ const trimmed = line.trimStart();
129
+ if (!trimmed.startsWith('gem'))
130
+ return null;
131
+ const next = trimmed[3] || '';
132
+ if (next !== ' ' && next !== '\t' && next !== '(')
133
+ return null;
134
+ if (next === '(')
135
+ return stripSingleOuterCallParens(trimmed.slice(3));
136
+ return trimmed.slice(3).trim();
137
+ }
138
+ function stripSingleOuterCallParens(input) {
139
+ const text = input.trim();
140
+ if (!text.startsWith('('))
141
+ return text;
142
+ let quote = '';
143
+ let depth = 0;
144
+ for (let i = 0; i < text.length; i++) {
145
+ const ch = text[i];
146
+ if (quote) {
147
+ if (ch === quote)
148
+ quote = '';
149
+ if (ch === '\\')
150
+ i++;
151
+ continue;
152
+ }
153
+ if (ch === '"' || ch === "'") {
154
+ quote = ch;
155
+ continue;
156
+ }
157
+ if (ch === '(')
158
+ depth++;
159
+ else if (ch === ')') {
160
+ depth--;
161
+ if (depth === 0)
162
+ return text.slice(1, i).trim();
163
+ }
164
+ }
165
+ return text.slice(1).trim();
166
+ }
167
+ function splitTopLevelRubyArgs(input) {
168
+ const args = [];
169
+ let cur = '';
170
+ let quote = '';
171
+ let paren = 0;
172
+ let bracket = 0;
173
+ let brace = 0;
174
+ for (let i = 0; i < input.length; i++) {
175
+ const ch = input[i];
176
+ if (quote) {
177
+ if (ch === quote) {
178
+ quote = '';
179
+ cur += ch;
180
+ continue;
181
+ }
182
+ if (ch === '\\' && i + 1 < input.length) {
183
+ cur += ch + input[++i];
184
+ }
185
+ else {
186
+ cur += ch;
187
+ }
188
+ continue;
189
+ }
190
+ if (ch === '"' || ch === "'") {
191
+ quote = ch;
192
+ cur += ch;
193
+ continue;
194
+ }
195
+ if (ch === '(')
196
+ paren++;
197
+ else if (ch === ')' && paren > 0)
198
+ paren--;
199
+ else if (ch === '[')
200
+ bracket++;
201
+ else if (ch === ']' && bracket > 0)
202
+ bracket--;
203
+ else if (ch === '{')
204
+ brace++;
205
+ else if (ch === '}' && brace > 0)
206
+ brace--;
207
+ if (ch === ',' && paren === 0 && bracket === 0 && brace === 0) {
208
+ if (cur.trim())
209
+ args.push(cur.trim());
210
+ cur = '';
211
+ continue;
212
+ }
213
+ cur += ch;
214
+ }
215
+ if (cur.trim())
216
+ args.push(cur.trim());
217
+ return args;
218
+ }
219
+ function parseRubyStringLiteral(input) {
220
+ const parsed = readRubyStringLiteral(input);
221
+ if (!parsed)
222
+ return null;
223
+ return input.slice(parsed.end).trim() === '' ? parsed.value : null;
224
+ }
225
+ function readRubyStringLiteral(input) {
226
+ const leading = input.length - input.trimStart().length;
227
+ const text = input.trimStart();
228
+ const quote = text[0];
229
+ if ((quote !== '"' && quote !== "'") || text.length < 2)
230
+ return null;
231
+ let out = '';
232
+ for (let i = 1; i < text.length; i++) {
233
+ const ch = text[i];
234
+ if (ch === quote) {
235
+ return { value: out, end: leading + i + 1 };
236
+ }
237
+ if (ch === '\\' && i + 1 < text.length) {
238
+ out += text[++i];
239
+ }
240
+ else {
241
+ out += ch;
242
+ }
243
+ }
244
+ return null;
245
+ }
246
+ function isRubyKeywordArg(input) {
247
+ return rubyKeywordName(input) !== null;
248
+ }
249
+ function rubyKeywordName(input) {
250
+ const text = input.trim();
251
+ if (!text)
252
+ return null;
253
+ const symbolRocket = parseRubySymbolHashRocketKey(text);
254
+ if (symbolRocket)
255
+ return symbolRocket;
256
+ const stringRocket = parseRubyStringHashRocketKey(text);
257
+ if (stringRocket)
258
+ return stringRocket;
259
+ const idx = text.indexOf(':');
260
+ if (idx <= 0)
261
+ return null;
262
+ for (let i = 0; i < idx; i++) {
263
+ const ch = text[i];
264
+ const ok = ch === '_' ||
265
+ (ch >= 'a' && ch <= 'z') ||
266
+ (ch >= 'A' && ch <= 'Z') ||
267
+ (i > 0 && ch >= '0' && ch <= '9');
268
+ if (!ok)
269
+ return null;
270
+ }
271
+ return text.slice(0, idx);
272
+ }
273
+ function parseRubySymbolHashRocketKey(input) {
274
+ if (!input.startsWith(':'))
275
+ return null;
276
+ let key = '';
277
+ for (let i = 1; i < input.length; i++) {
278
+ const ch = input[i];
279
+ const ok = ch === '_' ||
280
+ (ch >= 'a' && ch <= 'z') ||
281
+ (ch >= 'A' && ch <= 'Z') ||
282
+ (key.length > 0 && ch >= '0' && ch <= '9');
283
+ if (!ok) {
284
+ const rest = input.slice(i).trimStart();
285
+ return key && rest.startsWith('=>') ? key : null;
286
+ }
287
+ key += ch;
288
+ }
289
+ return null;
290
+ }
291
+ function parseRubyStringHashRocketKey(input) {
292
+ const parsed = readRubyStringLiteral(input);
293
+ if (!parsed)
294
+ return null;
295
+ return input.slice(parsed.end).trimStart().startsWith('=>') ? parsed.value : null;
296
+ }
297
+ function trimRubyComment(line) {
298
+ let quote = '';
299
+ for (let i = 0; i < line.length; i++) {
300
+ const ch = line[i];
301
+ if (quote) {
302
+ if (ch === quote)
303
+ quote = '';
304
+ if (ch === '\\')
305
+ i++;
306
+ continue;
307
+ }
308
+ if (ch === '"' || ch === "'") {
309
+ quote = ch;
310
+ continue;
311
+ }
312
+ if (ch === '#')
313
+ return line.slice(0, i);
314
+ }
315
+ return line;
316
+ }
317
+ async function installOneGem(vfs, req, ctx) {
318
+ const normalizedName = normalizeGemName(req.name);
319
+ const visitKey = `${normalizedName}:${req.requirements.join(',')}`;
320
+ if (ctx.visiting.has(visitKey))
321
+ return;
322
+ ctx.visiting.add(visitKey);
323
+ const metadata = await resolveGemMetadata(normalizedName, req.requirements);
324
+ const installedKey = `${metadata.name}-${metadata.version}`;
325
+ const gemRoot = `${ctx.gemHome}/gems/${installedKey}`;
326
+ if (vfs.exists(`${gemRoot}/lib`) || vfs.exists(`${ctx.gemHome}/specifications/${installedKey}.gemspec`)) {
327
+ ctx.report.alreadyInstalled.push(installedKey);
328
+ ctx.visiting.delete(visitKey);
329
+ return;
330
+ }
331
+ if (ctx.includeDependencies) {
332
+ for (const dep of metadata.dependencies?.runtime || []) {
333
+ await installOneGem(vfs, {
334
+ name: dep.name,
335
+ requirements: parseRubyGemRequirements(dep.requirements),
336
+ }, ctx);
337
+ }
338
+ }
339
+ if (!metadata.gem_uri) {
340
+ throw new Error(`RubyGems metadata for ${metadata.name}-${metadata.version} did not include gem_uri`);
341
+ }
342
+ const gemBytes = await fetchGemBytes(metadata.gem_uri);
343
+ const dataFiles = await extractGemData(gemBytes);
344
+ const nativePath = findNativeExtensionPath(Array.from(dataFiles.keys()));
345
+ if (nativePath) {
346
+ throw new Error(`${metadata.name}-${metadata.version} contains native extension '${nativePath}', ` +
347
+ 'which is not compatible with ruby.wasm in Nimbus');
348
+ }
349
+ ensureDir(vfs, gemRoot);
350
+ const executables = gemExecutableNames(dataFiles);
351
+ for (const [rel, bytes] of dataFiles) {
352
+ const cleanRel = normalizeVfsPath(rel);
353
+ if (!cleanRel || cleanRel.startsWith('..'))
354
+ continue;
355
+ const target = `${gemRoot}/${cleanRel}`;
356
+ ensureDir(vfs, parentVfsPath(target));
357
+ vfs.writeFile(target, bytes);
358
+ }
359
+ const specPath = `${ctx.gemHome}/specifications/${installedKey}.gemspec`;
360
+ vfs.writeFile(specPath, buildSyntheticGemspec(metadata, executables));
361
+ vfs.writeFile(`${ctx.gemHome}/cache/${installedKey}.gem`, gemBytes);
362
+ for (const executable of executables) {
363
+ writeGemExecutableWrapper(vfs, ctx.gemHome, installedKey, executable);
364
+ }
365
+ writeInstalledGemRecord(vfs, ctx.gemHome, {
366
+ name: metadata.name,
367
+ version: metadata.version,
368
+ platform: metadata.platform || 'ruby',
369
+ installedAt: Date.now(),
370
+ dependencies: metadata.dependencies?.runtime || [],
371
+ executables,
372
+ });
373
+ ctx.report.installed.push(installedKey);
374
+ ctx.visiting.delete(visitKey);
375
+ }
376
+ async function resolveGemMetadata(name, requirements) {
377
+ const exact = exactVersionRequirement(requirements);
378
+ if (exact)
379
+ return fetchGemMetadata(name, exact);
380
+ const versions = await fetchGemVersions(name);
381
+ const selected = versions
382
+ .filter((v) => !v.prerelease)
383
+ .filter((v) => !v.platform || v.platform === 'ruby')
384
+ .filter((v) => typeof v.number === 'string' && satisfiesRequirements(v.number, requirements))
385
+ .sort((a, b) => -compareVersions(a.number || '0', b.number || '0'))[0];
386
+ if (!selected?.number) {
387
+ throw new Error(`no ruby platform version of ${name} satisfies ${requirements.join(', ') || '>= 0'}`);
388
+ }
389
+ return fetchGemMetadata(name, selected.number);
390
+ }
391
+ async function fetchGemMetadata(name, version) {
392
+ const url = version
393
+ ? `${RUBYGEMS_API}/api/v2/rubygems/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}.json`
394
+ : `${RUBYGEMS_API}/api/v1/gems/${encodeURIComponent(name)}.json`;
395
+ const resp = await fetch(url);
396
+ if (!resp.ok)
397
+ throw new Error(`RubyGems metadata fetch failed for ${name}${version ? '-' + version : ''}: HTTP ${resp.status}`);
398
+ return await resp.json();
399
+ }
400
+ async function fetchGemVersions(name) {
401
+ const resp = await fetch(`${RUBYGEMS_API}/api/v1/versions/${encodeURIComponent(name)}.json`);
402
+ if (!resp.ok)
403
+ throw new Error(`RubyGems versions fetch failed for ${name}: HTTP ${resp.status}`);
404
+ return await resp.json();
405
+ }
406
+ async function fetchGemBytes(url) {
407
+ const resp = await fetch(url);
408
+ if (!resp.ok)
409
+ throw new Error(`RubyGems download failed: HTTP ${resp.status}`);
410
+ return new Uint8Array(await resp.arrayBuffer());
411
+ }
412
+ async function extractGemData(gemBytes) {
413
+ const outer = await extractTarball(toArrayBuffer(gemBytes));
414
+ const data = outer.get('data.tar.gz');
415
+ if (!data)
416
+ throw new Error('RubyGems archive missing data.tar.gz');
417
+ return await extractTarball(toArrayBuffer(data));
418
+ }
419
+ function toArrayBuffer(bytes) {
420
+ const out = new ArrayBuffer(bytes.byteLength);
421
+ new Uint8Array(out).set(bytes);
422
+ return out;
423
+ }
424
+ function findNativeExtensionPath(paths) {
425
+ for (const path of paths) {
426
+ const clean = normalizeVfsPath(path);
427
+ const parts = clean.split('/').filter(Boolean);
428
+ if (parts[0] === 'ext')
429
+ return clean;
430
+ const leaf = parts[parts.length - 1] || '';
431
+ if (leaf.endsWith('.so') || leaf.endsWith('.bundle') || leaf.endsWith('.dll') || leaf.endsWith('.dylib')) {
432
+ return clean;
433
+ }
434
+ }
435
+ return null;
436
+ }
437
+ function buildSyntheticGemspec(metadata, executables) {
438
+ return [
439
+ '# Synthetic gemspec written by Nimbus RubyGems.',
440
+ `Gem::Specification.new do |s|`,
441
+ ` s.name = ${JSON.stringify(metadata.name)}`,
442
+ ` s.version = ${JSON.stringify(metadata.version)}`,
443
+ ` s.platform = ${JSON.stringify(metadata.platform || 'ruby')}`,
444
+ ` s.summary = ${JSON.stringify('Installed by Nimbus')}`,
445
+ ` s.files = []`,
446
+ ` s.executables = ${JSON.stringify(executables)}`,
447
+ `end`,
448
+ '',
449
+ ].join('\n');
450
+ }
451
+ function gemExecutableNames(files) {
452
+ const names = new Set();
453
+ for (const path of files.keys()) {
454
+ const clean = normalizeVfsPath(path);
455
+ const parts = clean.split('/').filter(Boolean);
456
+ if (parts.length !== 2 || parts[0] !== 'bin')
457
+ continue;
458
+ const name = parts[1];
459
+ if (isValidGemExecutableName(name))
460
+ names.add(name);
461
+ }
462
+ return Array.from(names).sort();
463
+ }
464
+ function writeGemExecutableWrapper(vfs, gemHome, installedKey, executable) {
465
+ const binRoot = `${gemHome}/bin`;
466
+ ensureDir(vfs, binRoot);
467
+ const scriptPath = `/${gemHome}/gems/${installedKey}/bin/${executable}`;
468
+ vfs.writeFile(`${binRoot}/${executable}`, [
469
+ '#!/usr/bin/env ruby',
470
+ `load ${JSON.stringify(scriptPath)}`,
471
+ '',
472
+ ].join('\n'));
473
+ }
474
+ function writeInstalledGemRecord(vfs, gemHome, record) {
475
+ const path = `${gemHome}/.nimbus-gems.json`;
476
+ const records = readInstalledGemRecords(vfs, gemHome)
477
+ .filter((r) => !(r.name === record.name && r.version === record.version));
478
+ records.push(record);
479
+ vfs.writeFile(path, JSON.stringify({ gems: records }, null, 2) + '\n');
480
+ }
481
+ function readInstalledGemRecords(vfs, gemHome) {
482
+ const path = `${gemHome}/.nimbus-gems.json`;
483
+ if (!vfs.exists(path))
484
+ return [];
485
+ try {
486
+ const parsed = JSON.parse(new TextDecoder('utf-8').decode(vfs.readFile(path)));
487
+ return Array.isArray(parsed?.gems) ? parsed.gems : [];
488
+ }
489
+ catch {
490
+ return [];
491
+ }
492
+ }
493
+ export function parseRubyGemRequirements(input) {
494
+ if (!input || input.trim() === '' || input.trim() === '>= 0')
495
+ return [];
496
+ return input
497
+ .split(',')
498
+ .map((part) => part.trim())
499
+ .filter(Boolean);
500
+ }
501
+ function exactVersionRequirement(requirements) {
502
+ for (const req of requirements) {
503
+ const parsed = parseRequirement(req);
504
+ if (parsed && parsed.op === '=')
505
+ return parsed.version;
506
+ }
507
+ return null;
508
+ }
509
+ function satisfiesRequirements(version, requirements) {
510
+ if (requirements.length === 0)
511
+ return true;
512
+ return requirements.every((req) => {
513
+ const parsed = parseRequirement(req);
514
+ if (!parsed)
515
+ return true;
516
+ const cmp = compareVersions(version, parsed.version);
517
+ if (parsed.op === '=')
518
+ return cmp === 0;
519
+ if (parsed.op === '!=')
520
+ return cmp !== 0;
521
+ if (parsed.op === '>')
522
+ return cmp > 0;
523
+ if (parsed.op === '>=')
524
+ return cmp >= 0;
525
+ if (parsed.op === '<')
526
+ return cmp < 0;
527
+ if (parsed.op === '<=')
528
+ return cmp <= 0;
529
+ if (parsed.op === '~>')
530
+ return cmp >= 0 && compareVersions(version, pessimisticUpperBound(parsed.version)) < 0;
531
+ return true;
532
+ });
533
+ }
534
+ function parseRequirement(req) {
535
+ const trimmed = req.trim();
536
+ if (!trimmed)
537
+ return null;
538
+ const ops = ['>=', '<=', '!=', '~>', '=', '>', '<'];
539
+ for (const op of ops) {
540
+ if (trimmed.startsWith(op)) {
541
+ const version = trimmed.slice(op.length).trim();
542
+ return version ? { op, version } : null;
543
+ }
544
+ }
545
+ return { op: '=', version: trimmed };
546
+ }
547
+ function pessimisticUpperBound(version) {
548
+ const parts = version.split('.').map((p) => Number.parseInt(p, 10)).filter((n) => Number.isFinite(n));
549
+ if (parts.length <= 1)
550
+ return String((parts[0] || 0) + 1);
551
+ const bumpIndex = parts.length === 2 ? 0 : parts.length - 2;
552
+ const out = parts.slice(0, bumpIndex + 1);
553
+ out[bumpIndex] = (out[bumpIndex] || 0) + 1;
554
+ return out.join('.');
555
+ }
556
+ function compareVersions(a, b) {
557
+ const aa = splitVersion(a);
558
+ const bb = splitVersion(b);
559
+ const len = Math.max(aa.length, bb.length);
560
+ for (let i = 0; i < len; i++) {
561
+ const av = aa[i] ?? 0;
562
+ const bv = bb[i] ?? 0;
563
+ if (typeof av === 'number' && typeof bv === 'number') {
564
+ if (av !== bv)
565
+ return av - bv;
566
+ continue;
567
+ }
568
+ const as = String(av);
569
+ const bs = String(bv);
570
+ if (as !== bs)
571
+ return as < bs ? -1 : 1;
572
+ }
573
+ return 0;
574
+ }
575
+ function splitVersion(version) {
576
+ const out = [];
577
+ let cur = '';
578
+ let numeric = false;
579
+ const push = () => {
580
+ if (!cur)
581
+ return;
582
+ out.push(numeric ? Number.parseInt(cur, 10) : cur);
583
+ cur = '';
584
+ };
585
+ for (const ch of version) {
586
+ const isDigit = ch >= '0' && ch <= '9';
587
+ if (ch === '.' || ch === '-' || ch === '_') {
588
+ push();
589
+ numeric = false;
590
+ continue;
591
+ }
592
+ if (!cur)
593
+ numeric = isDigit;
594
+ if (cur && numeric !== isDigit) {
595
+ push();
596
+ numeric = isDigit;
597
+ }
598
+ cur += ch;
599
+ }
600
+ push();
601
+ return out;
602
+ }
603
+ function normalizeGemName(name) {
604
+ const clean = name.trim();
605
+ if (!clean)
606
+ throw new Error('empty gem name');
607
+ for (const ch of clean) {
608
+ const ok = ch === '-' || ch === '_' || ch === '.' ||
609
+ ch >= '0' && ch <= '9' ||
610
+ ch >= 'a' && ch <= 'z' ||
611
+ ch >= 'A' && ch <= 'Z';
612
+ if (!ok)
613
+ throw new Error(`unsupported gem name '${name}'`);
614
+ }
615
+ return clean;
616
+ }
617
+ function isValidGemExecutableName(name) {
618
+ if (!name || name === '.' || name === '..' || name.includes('/'))
619
+ return false;
620
+ for (const ch of name) {
621
+ const ok = ch === '-' || ch === '_' || ch === '.' ||
622
+ ch >= '0' && ch <= '9' ||
623
+ ch >= 'a' && ch <= 'z' ||
624
+ ch >= 'A' && ch <= 'Z';
625
+ if (!ok)
626
+ return false;
627
+ }
628
+ return true;
629
+ }
630
+ function ensureDir(vfs, path) {
631
+ const clean = normalizeVfsPath(path);
632
+ if (!clean)
633
+ return;
634
+ if (!vfs.exists(clean))
635
+ vfs.mkdir(clean, { recursive: true });
636
+ }