@push.rocks/smartregistry 1.5.0 → 1.6.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,398 @@
1
+ /**
2
+ * Helper functions for RubyGems registry
3
+ * Compact Index generation, dependency formatting, etc.
4
+ */
5
+
6
+ import type {
7
+ IRubyGemsVersion,
8
+ IRubyGemsDependency,
9
+ IRubyGemsRequirement,
10
+ ICompactIndexVersionsEntry,
11
+ ICompactIndexInfoEntry,
12
+ IRubyGemsMetadata,
13
+ } from './interfaces.rubygems.js';
14
+
15
+ /**
16
+ * Generate Compact Index versions file
17
+ * Format: GEMNAME [-]VERSION_PLATFORM[,VERSION_PLATFORM,...] MD5
18
+ * @param entries - Version entries for all gems
19
+ * @returns Compact Index versions file content
20
+ */
21
+ export function generateCompactIndexVersions(entries: ICompactIndexVersionsEntry[]): string {
22
+ const lines: string[] = [];
23
+
24
+ // Add metadata header
25
+ lines.push(`created_at: ${new Date().toISOString()}`);
26
+ lines.push('---');
27
+
28
+ // Add gem entries
29
+ for (const entry of entries) {
30
+ const versions = entry.versions
31
+ .map(v => {
32
+ const yanked = v.yanked ? '-' : '';
33
+ const platform = v.platform && v.platform !== 'ruby' ? `_${v.platform}` : '';
34
+ return `${yanked}${v.version}${platform}`;
35
+ })
36
+ .join(',');
37
+
38
+ lines.push(`${entry.name} ${versions} ${entry.infoChecksum}`);
39
+ }
40
+
41
+ return lines.join('\n');
42
+ }
43
+
44
+ /**
45
+ * Generate Compact Index info file for a gem
46
+ * Format: VERSION[-PLATFORM] [DEP[,DEP,...]]|REQ[,REQ,...]
47
+ * @param entries - Info entries for gem versions
48
+ * @returns Compact Index info file content
49
+ */
50
+ export function generateCompactIndexInfo(entries: ICompactIndexInfoEntry[]): string {
51
+ const lines: string[] = ['---']; // Info files start with ---
52
+
53
+ for (const entry of entries) {
54
+ // Build version string with optional platform
55
+ const versionStr = entry.platform && entry.platform !== 'ruby'
56
+ ? `${entry.version}-${entry.platform}`
57
+ : entry.version;
58
+
59
+ // Build dependencies string
60
+ const depsStr = entry.dependencies.length > 0
61
+ ? entry.dependencies.map(formatDependency).join(',')
62
+ : '';
63
+
64
+ // Build requirements string (checksum is always required)
65
+ const reqParts: string[] = [`checksum:${entry.checksum}`];
66
+
67
+ for (const req of entry.requirements) {
68
+ reqParts.push(`${req.type}:${req.requirement}`);
69
+ }
70
+
71
+ const reqStr = reqParts.join(',');
72
+
73
+ // Combine: VERSION[-PLATFORM] [DEPS]|REQS
74
+ const depPart = depsStr ? ` ${depsStr}` : '';
75
+ lines.push(`${versionStr}${depPart}|${reqStr}`);
76
+ }
77
+
78
+ return lines.join('\n');
79
+ }
80
+
81
+ /**
82
+ * Format a dependency for Compact Index
83
+ * Format: GEM:CONSTRAINT[&CONSTRAINT]
84
+ * @param dep - Dependency object
85
+ * @returns Formatted dependency string
86
+ */
87
+ export function formatDependency(dep: IRubyGemsDependency): string {
88
+ return `${dep.name}:${dep.requirement}`;
89
+ }
90
+
91
+ /**
92
+ * Parse dependency string from Compact Index
93
+ * @param depStr - Dependency string
94
+ * @returns Dependency object
95
+ */
96
+ export function parseDependency(depStr: string): IRubyGemsDependency {
97
+ const [name, ...reqParts] = depStr.split(':');
98
+ const requirement = reqParts.join(':'); // Handle :: in gem names
99
+
100
+ return { name, requirement };
101
+ }
102
+
103
+ /**
104
+ * Generate names file (newline-separated gem names)
105
+ * @param names - List of gem names
106
+ * @returns Names file content
107
+ */
108
+ export function generateNamesFile(names: string[]): string {
109
+ return `---\n${names.sort().join('\n')}`;
110
+ }
111
+
112
+ /**
113
+ * Calculate MD5 hash for Compact Index checksum
114
+ * @param content - Content to hash
115
+ * @returns MD5 hash (hex)
116
+ */
117
+ export async function calculateMD5(content: string): Promise<string> {
118
+ const crypto = await import('crypto');
119
+ return crypto.createHash('md5').update(content).digest('hex');
120
+ }
121
+
122
+ /**
123
+ * Calculate SHA256 hash for gem files
124
+ * @param data - Data to hash
125
+ * @returns SHA256 hash (hex)
126
+ */
127
+ export async function calculateSHA256(data: Buffer): Promise<string> {
128
+ const crypto = await import('crypto');
129
+ return crypto.createHash('sha256').update(data).digest('hex');
130
+ }
131
+
132
+ /**
133
+ * Parse gem filename to extract name, version, and platform
134
+ * @param filename - Gem filename (e.g., "rails-7.0.0-x86_64-linux.gem")
135
+ * @returns Parsed info or null
136
+ */
137
+ export function parseGemFilename(filename: string): {
138
+ name: string;
139
+ version: string;
140
+ platform?: string;
141
+ } | null {
142
+ if (!filename.endsWith('.gem')) return null;
143
+
144
+ const withoutExt = filename.slice(0, -4); // Remove .gem
145
+
146
+ // Try to match: name-version-platform
147
+ // Platform can contain hyphens (e.g., x86_64-linux)
148
+ const parts = withoutExt.split('-');
149
+ if (parts.length < 2) return null;
150
+
151
+ // Find version (first part that starts with a digit)
152
+ let versionIndex = -1;
153
+ for (let i = 1; i < parts.length; i++) {
154
+ if (/^\d/.test(parts[i])) {
155
+ versionIndex = i;
156
+ break;
157
+ }
158
+ }
159
+
160
+ if (versionIndex === -1) return null;
161
+
162
+ const name = parts.slice(0, versionIndex).join('-');
163
+ const version = parts[versionIndex];
164
+ const platform = versionIndex + 1 < parts.length
165
+ ? parts.slice(versionIndex + 1).join('-')
166
+ : undefined;
167
+
168
+ return {
169
+ name,
170
+ version,
171
+ platform: platform && platform !== 'ruby' ? platform : undefined,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Validate gem name
177
+ * Must contain only ASCII letters, numbers, _, and -
178
+ * @param name - Gem name
179
+ * @returns true if valid
180
+ */
181
+ export function isValidGemName(name: string): boolean {
182
+ return /^[a-zA-Z0-9_-]+$/.test(name);
183
+ }
184
+
185
+ /**
186
+ * Validate version string
187
+ * Basic semantic versioning check
188
+ * @param version - Version string
189
+ * @returns true if valid
190
+ */
191
+ export function isValidVersion(version: string): boolean {
192
+ // Allow semver and other common Ruby version formats
193
+ return /^[\d.a-zA-Z_-]+$/.test(version);
194
+ }
195
+
196
+ /**
197
+ * Build version list entry for Compact Index
198
+ * @param versions - Version info
199
+ * @returns Version list string
200
+ */
201
+ export function buildVersionList(versions: Array<{
202
+ version: string;
203
+ platform?: string;
204
+ yanked: boolean;
205
+ }>): string {
206
+ return versions
207
+ .map(v => {
208
+ const yanked = v.yanked ? '-' : '';
209
+ const platform = v.platform && v.platform !== 'ruby' ? `_${v.platform}` : '';
210
+ return `${yanked}${v.version}${platform}`;
211
+ })
212
+ .join(',');
213
+ }
214
+
215
+ /**
216
+ * Parse version list from Compact Index
217
+ * @param versionStr - Version list string
218
+ * @returns Parsed versions
219
+ */
220
+ export function parseVersionList(versionStr: string): Array<{
221
+ version: string;
222
+ platform?: string;
223
+ yanked: boolean;
224
+ }> {
225
+ return versionStr.split(',').map(v => {
226
+ const yanked = v.startsWith('-');
227
+ const withoutYank = yanked ? v.substring(1) : v;
228
+
229
+ // Split on _ to separate version from platform
230
+ const [version, ...platformParts] = withoutYank.split('_');
231
+ const platform = platformParts.length > 0 ? platformParts.join('_') : undefined;
232
+
233
+ return {
234
+ version,
235
+ platform: platform && platform !== 'ruby' ? platform : undefined,
236
+ yanked,
237
+ };
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Generate JSON response for /api/v1/versions/{gem}.json
243
+ * @param gemName - Gem name
244
+ * @param versions - Version list
245
+ * @returns JSON response object
246
+ */
247
+ export function generateVersionsJson(
248
+ gemName: string,
249
+ versions: Array<{
250
+ version: string;
251
+ platform?: string;
252
+ uploadTime?: string;
253
+ }>
254
+ ): any {
255
+ return {
256
+ name: gemName,
257
+ versions: versions.map(v => ({
258
+ number: v.version,
259
+ platform: v.platform || 'ruby',
260
+ built_at: v.uploadTime,
261
+ })),
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Generate JSON response for /api/v1/dependencies
267
+ * @param gems - Map of gem names to version dependencies
268
+ * @returns JSON response array
269
+ */
270
+ export function generateDependenciesJson(gems: Map<string, Array<{
271
+ version: string;
272
+ platform?: string;
273
+ dependencies: IRubyGemsDependency[];
274
+ }>>): any {
275
+ const result: any[] = [];
276
+
277
+ for (const [name, versions] of gems) {
278
+ for (const v of versions) {
279
+ result.push({
280
+ name,
281
+ number: v.version,
282
+ platform: v.platform || 'ruby',
283
+ dependencies: v.dependencies.map(d => ({
284
+ name: d.name,
285
+ requirements: d.requirement,
286
+ })),
287
+ });
288
+ }
289
+ }
290
+
291
+ return result;
292
+ }
293
+
294
+ /**
295
+ * Update Compact Index versions file with new gem version
296
+ * Handles append-only semantics for the current month
297
+ * @param existingContent - Current versions file content
298
+ * @param gemName - Gem name
299
+ * @param newVersion - New version info
300
+ * @param infoChecksum - MD5 of info file
301
+ * @returns Updated versions file content
302
+ */
303
+ export function updateCompactIndexVersions(
304
+ existingContent: string,
305
+ gemName: string,
306
+ newVersion: { version: string; platform?: string; yanked: boolean },
307
+ infoChecksum: string
308
+ ): string {
309
+ const lines = existingContent.split('\n');
310
+ const headerEndIndex = lines.findIndex(l => l === '---');
311
+
312
+ if (headerEndIndex === -1) {
313
+ throw new Error('Invalid Compact Index versions file');
314
+ }
315
+
316
+ const header = lines.slice(0, headerEndIndex + 1);
317
+ const entries = lines.slice(headerEndIndex + 1).filter(l => l.trim());
318
+
319
+ // Find existing entry for gem
320
+ const gemLineIndex = entries.findIndex(l => l.startsWith(`${gemName} `));
321
+
322
+ const versionStr = buildVersionList([newVersion]);
323
+
324
+ if (gemLineIndex >= 0) {
325
+ // Append to existing entry
326
+ const parts = entries[gemLineIndex].split(' ');
327
+ const existingVersions = parts[1];
328
+ const updatedVersions = `${existingVersions},${versionStr}`;
329
+ entries[gemLineIndex] = `${gemName} ${updatedVersions} ${infoChecksum}`;
330
+ } else {
331
+ // Add new entry
332
+ entries.push(`${gemName} ${versionStr} ${infoChecksum}`);
333
+ entries.sort(); // Keep alphabetical
334
+ }
335
+
336
+ return [...header, ...entries].join('\n');
337
+ }
338
+
339
+ /**
340
+ * Update Compact Index info file with new version
341
+ * @param existingContent - Current info file content
342
+ * @param newEntry - New version entry
343
+ * @returns Updated info file content
344
+ */
345
+ export function updateCompactIndexInfo(
346
+ existingContent: string,
347
+ newEntry: ICompactIndexInfoEntry
348
+ ): string {
349
+ const lines = existingContent ? existingContent.split('\n').filter(l => l !== '---') : [];
350
+
351
+ // Build version string
352
+ const versionStr = newEntry.platform && newEntry.platform !== 'ruby'
353
+ ? `${newEntry.version}-${newEntry.platform}`
354
+ : newEntry.version;
355
+
356
+ // Build dependencies string
357
+ const depsStr = newEntry.dependencies.length > 0
358
+ ? newEntry.dependencies.map(formatDependency).join(',')
359
+ : '';
360
+
361
+ // Build requirements string
362
+ const reqParts: string[] = [`checksum:${newEntry.checksum}`];
363
+ for (const req of newEntry.requirements) {
364
+ reqParts.push(`${req.type}:${req.requirement}`);
365
+ }
366
+ const reqStr = reqParts.join(',');
367
+
368
+ // Combine
369
+ const depPart = depsStr ? ` ${depsStr}` : '';
370
+ const newLine = `${versionStr}${depPart}|${reqStr}`;
371
+
372
+ lines.push(newLine);
373
+
374
+ return `---\n${lines.join('\n')}`;
375
+ }
376
+
377
+ /**
378
+ * Extract gem specification from .gem file
379
+ * Note: This is a simplified version. Full implementation would use tar + gzip + Marshal
380
+ * @param gemData - Gem file data
381
+ * @returns Extracted spec or null
382
+ */
383
+ export async function extractGemSpec(gemData: Buffer): Promise<any | null> {
384
+ try {
385
+ // .gem files are gzipped tar archives
386
+ // They contain metadata.gz which has Marshal-encoded spec
387
+ // This is a placeholder - full implementation would need:
388
+ // 1. Unzip outer gzip
389
+ // 2. Untar to find metadata.gz
390
+ // 3. Unzip metadata.gz
391
+ // 4. Parse Ruby Marshal format
392
+
393
+ // For now, return null and expect metadata to be provided
394
+ return null;
395
+ } catch (error) {
396
+ return null;
397
+ }
398
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * RubyGems Registry Module
3
+ * RubyGems/Bundler Compact Index implementation
4
+ */
5
+
6
+ export * from './interfaces.rubygems.js';
7
+ export * from './classes.rubygemsregistry.js';
8
+ export * as rubygemsHelpers from './helpers.rubygems.js';
@@ -0,0 +1,251 @@
1
+ /**
2
+ * RubyGems Registry Type Definitions
3
+ * Compliant with Compact Index API and RubyGems protocol
4
+ */
5
+
6
+ /**
7
+ * Gem version entry in compact index
8
+ */
9
+ export interface IRubyGemsVersion {
10
+ /** Version number */
11
+ version: string;
12
+ /** Platform (e.g., ruby, x86_64-linux) */
13
+ platform?: string;
14
+ /** Dependencies */
15
+ dependencies?: IRubyGemsDependency[];
16
+ /** Requirements */
17
+ requirements?: IRubyGemsRequirement[];
18
+ /** Whether this version is yanked */
19
+ yanked?: boolean;
20
+ /** SHA256 checksum of .gem file */
21
+ checksum?: string;
22
+ }
23
+
24
+ /**
25
+ * Gem dependency specification
26
+ */
27
+ export interface IRubyGemsDependency {
28
+ /** Gem name */
29
+ name: string;
30
+ /** Version requirement (e.g., ">= 1.0", "~> 2.0") */
31
+ requirement: string;
32
+ }
33
+
34
+ /**
35
+ * Gem requirements (ruby version, rubygems version, etc.)
36
+ */
37
+ export interface IRubyGemsRequirement {
38
+ /** Requirement type (ruby, rubygems) */
39
+ type: 'ruby' | 'rubygems';
40
+ /** Version requirement */
41
+ requirement: string;
42
+ }
43
+
44
+ /**
45
+ * Complete gem metadata
46
+ */
47
+ export interface IRubyGemsMetadata {
48
+ /** Gem name */
49
+ name: string;
50
+ /** All versions */
51
+ versions: Record<string, IRubyGemsVersionMetadata>;
52
+ /** Last modified timestamp */
53
+ 'last-modified'?: string;
54
+ }
55
+
56
+ /**
57
+ * Version-specific metadata
58
+ */
59
+ export interface IRubyGemsVersionMetadata {
60
+ /** Version number */
61
+ version: string;
62
+ /** Platform */
63
+ platform?: string;
64
+ /** Authors */
65
+ authors?: string[];
66
+ /** Description */
67
+ description?: string;
68
+ /** Summary */
69
+ summary?: string;
70
+ /** Homepage */
71
+ homepage?: string;
72
+ /** License */
73
+ license?: string;
74
+ /** Dependencies */
75
+ dependencies?: IRubyGemsDependency[];
76
+ /** Requirements */
77
+ requirements?: IRubyGemsRequirement[];
78
+ /** SHA256 checksum */
79
+ checksum: string;
80
+ /** File size */
81
+ size: number;
82
+ /** Upload timestamp */
83
+ 'upload-time': string;
84
+ /** Uploader */
85
+ 'uploaded-by': string;
86
+ /** Yanked status */
87
+ yanked?: boolean;
88
+ /** Yank reason */
89
+ 'yank-reason'?: string;
90
+ }
91
+
92
+ /**
93
+ * Compact index versions file entry
94
+ * Format: GEMNAME [-]VERSION_PLATFORM[,VERSION_PLATFORM,...] MD5
95
+ */
96
+ export interface ICompactIndexVersionsEntry {
97
+ /** Gem name */
98
+ name: string;
99
+ /** Versions (with optional platform and yank flag) */
100
+ versions: Array<{
101
+ version: string;
102
+ platform?: string;
103
+ yanked: boolean;
104
+ }>;
105
+ /** MD5 checksum of info file */
106
+ infoChecksum: string;
107
+ }
108
+
109
+ /**
110
+ * Compact index info file entry
111
+ * Format: VERSION[-PLATFORM] [DEP[,DEP,...]]|REQ[,REQ,...]
112
+ */
113
+ export interface ICompactIndexInfoEntry {
114
+ /** Version number */
115
+ version: string;
116
+ /** Platform (optional) */
117
+ platform?: string;
118
+ /** Dependencies */
119
+ dependencies: IRubyGemsDependency[];
120
+ /** Requirements */
121
+ requirements: IRubyGemsRequirement[];
122
+ /** SHA256 checksum */
123
+ checksum: string;
124
+ }
125
+
126
+ /**
127
+ * Gem upload request
128
+ */
129
+ export interface IRubyGemsUploadRequest {
130
+ /** Gem file data */
131
+ gemData: Buffer;
132
+ /** Gem filename */
133
+ filename: string;
134
+ }
135
+
136
+ /**
137
+ * Gem upload response
138
+ */
139
+ export interface IRubyGemsUploadResponse {
140
+ /** Success message */
141
+ message?: string;
142
+ /** Gem name */
143
+ name?: string;
144
+ /** Version */
145
+ version?: string;
146
+ }
147
+
148
+ /**
149
+ * Yank request
150
+ */
151
+ export interface IRubyGemsYankRequest {
152
+ /** Gem name */
153
+ gem_name: string;
154
+ /** Version to yank */
155
+ version: string;
156
+ /** Platform (optional) */
157
+ platform?: string;
158
+ }
159
+
160
+ /**
161
+ * Yank response
162
+ */
163
+ export interface IRubyGemsYankResponse {
164
+ /** Success indicator */
165
+ success: boolean;
166
+ /** Message */
167
+ message?: string;
168
+ }
169
+
170
+ /**
171
+ * Version info response (JSON)
172
+ */
173
+ export interface IRubyGemsVersionInfo {
174
+ /** Gem name */
175
+ name: string;
176
+ /** Versions list */
177
+ versions: Array<{
178
+ /** Version number */
179
+ number: string;
180
+ /** Platform */
181
+ platform?: string;
182
+ /** Build date */
183
+ built_at?: string;
184
+ /** Download count */
185
+ downloads_count?: number;
186
+ }>;
187
+ }
188
+
189
+ /**
190
+ * Dependencies query response
191
+ */
192
+ export interface IRubyGemsDependenciesResponse {
193
+ /** Dependencies for requested gems */
194
+ dependencies: Array<{
195
+ /** Gem name */
196
+ name: string;
197
+ /** Version */
198
+ number: string;
199
+ /** Platform */
200
+ platform?: string;
201
+ /** Dependencies */
202
+ dependencies: Array<{
203
+ name: string;
204
+ requirements: string;
205
+ }>;
206
+ }>;
207
+ }
208
+
209
+ /**
210
+ * Error response structure
211
+ */
212
+ export interface IRubyGemsError {
213
+ /** Error message */
214
+ message: string;
215
+ /** HTTP status code */
216
+ status?: number;
217
+ }
218
+
219
+ /**
220
+ * Gem specification (extracted from .gem file)
221
+ */
222
+ export interface IRubyGemsSpec {
223
+ /** Gem name */
224
+ name: string;
225
+ /** Version */
226
+ version: string;
227
+ /** Platform */
228
+ platform?: string;
229
+ /** Authors */
230
+ authors?: string[];
231
+ /** Email */
232
+ email?: string;
233
+ /** Homepage */
234
+ homepage?: string;
235
+ /** Summary */
236
+ summary?: string;
237
+ /** Description */
238
+ description?: string;
239
+ /** License */
240
+ license?: string;
241
+ /** Dependencies */
242
+ dependencies?: IRubyGemsDependency[];
243
+ /** Required Ruby version */
244
+ required_ruby_version?: string;
245
+ /** Required RubyGems version */
246
+ required_rubygems_version?: string;
247
+ /** Files */
248
+ files?: string[];
249
+ /** Requirements */
250
+ requirements?: string[];
251
+ }