@enactprotocol/shared 1.2.11 → 2.0.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 (134) hide show
  1. package/README.md +44 -0
  2. package/package.json +16 -58
  3. package/src/config.ts +476 -0
  4. package/src/constants.ts +36 -0
  5. package/src/execution/command.ts +314 -0
  6. package/src/execution/index.ts +73 -0
  7. package/src/execution/runtime.ts +308 -0
  8. package/src/execution/types.ts +379 -0
  9. package/src/execution/validation.ts +508 -0
  10. package/src/index.ts +237 -30
  11. package/src/manifest/index.ts +36 -0
  12. package/src/manifest/loader.ts +187 -0
  13. package/src/manifest/parser.ts +173 -0
  14. package/src/manifest/validator.ts +309 -0
  15. package/src/paths.ts +108 -0
  16. package/src/registry.ts +219 -0
  17. package/src/resolver.ts +345 -0
  18. package/src/types/index.ts +30 -0
  19. package/src/types/manifest.ts +255 -0
  20. package/src/types.ts +5 -188
  21. package/src/utils/fs.ts +281 -0
  22. package/src/utils/logger.ts +270 -59
  23. package/src/utils/version.ts +304 -36
  24. package/tests/config.test.ts +515 -0
  25. package/tests/execution/command.test.ts +317 -0
  26. package/tests/execution/validation.test.ts +384 -0
  27. package/tests/fixtures/invalid-tool.yaml +4 -0
  28. package/tests/fixtures/valid-tool.md +62 -0
  29. package/tests/fixtures/valid-tool.yaml +40 -0
  30. package/tests/index.test.ts +8 -0
  31. package/tests/manifest/loader.test.ts +291 -0
  32. package/tests/manifest/parser.test.ts +345 -0
  33. package/tests/manifest/validator.test.ts +394 -0
  34. package/tests/manifest-types.test.ts +358 -0
  35. package/tests/paths.test.ts +153 -0
  36. package/tests/registry.test.ts +231 -0
  37. package/tests/resolver.test.ts +272 -0
  38. package/tests/utils/fs.test.ts +388 -0
  39. package/tests/utils/logger.test.ts +480 -0
  40. package/tests/utils/version.test.ts +390 -0
  41. package/tsconfig.json +12 -0
  42. package/tsconfig.tsbuildinfo +1 -0
  43. package/dist/LocalToolResolver.d.ts +0 -84
  44. package/dist/LocalToolResolver.js +0 -353
  45. package/dist/api/enact-api.d.ts +0 -130
  46. package/dist/api/enact-api.js +0 -428
  47. package/dist/api/index.d.ts +0 -2
  48. package/dist/api/index.js +0 -2
  49. package/dist/api/types.d.ts +0 -103
  50. package/dist/api/types.js +0 -1
  51. package/dist/constants.d.ts +0 -7
  52. package/dist/constants.js +0 -10
  53. package/dist/core/DaggerExecutionProvider.d.ts +0 -169
  54. package/dist/core/DaggerExecutionProvider.js +0 -1029
  55. package/dist/core/DirectExecutionProvider.d.ts +0 -23
  56. package/dist/core/DirectExecutionProvider.js +0 -406
  57. package/dist/core/EnactCore.d.ts +0 -162
  58. package/dist/core/EnactCore.js +0 -597
  59. package/dist/core/NativeExecutionProvider.d.ts +0 -9
  60. package/dist/core/NativeExecutionProvider.js +0 -16
  61. package/dist/core/index.d.ts +0 -3
  62. package/dist/core/index.js +0 -3
  63. package/dist/exec/index.d.ts +0 -3
  64. package/dist/exec/index.js +0 -3
  65. package/dist/exec/logger.d.ts +0 -11
  66. package/dist/exec/logger.js +0 -57
  67. package/dist/exec/validate.d.ts +0 -5
  68. package/dist/exec/validate.js +0 -167
  69. package/dist/index.d.ts +0 -21
  70. package/dist/index.js +0 -25
  71. package/dist/lib/enact-direct.d.ts +0 -150
  72. package/dist/lib/enact-direct.js +0 -159
  73. package/dist/lib/index.d.ts +0 -1
  74. package/dist/lib/index.js +0 -1
  75. package/dist/security/index.d.ts +0 -3
  76. package/dist/security/index.js +0 -3
  77. package/dist/security/security.d.ts +0 -23
  78. package/dist/security/security.js +0 -137
  79. package/dist/security/sign.d.ts +0 -103
  80. package/dist/security/sign.js +0 -666
  81. package/dist/security/verification-enforcer.d.ts +0 -53
  82. package/dist/security/verification-enforcer.js +0 -204
  83. package/dist/services/McpCoreService.d.ts +0 -98
  84. package/dist/services/McpCoreService.js +0 -124
  85. package/dist/services/index.d.ts +0 -1
  86. package/dist/services/index.js +0 -1
  87. package/dist/types.d.ts +0 -132
  88. package/dist/types.js +0 -3
  89. package/dist/utils/config.d.ts +0 -111
  90. package/dist/utils/config.js +0 -342
  91. package/dist/utils/env-loader.d.ts +0 -54
  92. package/dist/utils/env-loader.js +0 -270
  93. package/dist/utils/help.d.ts +0 -36
  94. package/dist/utils/help.js +0 -248
  95. package/dist/utils/index.d.ts +0 -7
  96. package/dist/utils/index.js +0 -7
  97. package/dist/utils/logger.d.ts +0 -35
  98. package/dist/utils/logger.js +0 -75
  99. package/dist/utils/silent-monitor.d.ts +0 -67
  100. package/dist/utils/silent-monitor.js +0 -242
  101. package/dist/utils/timeout.d.ts +0 -5
  102. package/dist/utils/timeout.js +0 -23
  103. package/dist/utils/version.d.ts +0 -4
  104. package/dist/utils/version.js +0 -35
  105. package/dist/web/env-manager-server.d.ts +0 -29
  106. package/dist/web/env-manager-server.js +0 -367
  107. package/dist/web/index.d.ts +0 -1
  108. package/dist/web/index.js +0 -1
  109. package/src/LocalToolResolver.ts +0 -424
  110. package/src/api/enact-api.ts +0 -604
  111. package/src/api/index.ts +0 -2
  112. package/src/api/types.ts +0 -114
  113. package/src/core/DaggerExecutionProvider.ts +0 -1357
  114. package/src/core/DirectExecutionProvider.ts +0 -484
  115. package/src/core/EnactCore.ts +0 -847
  116. package/src/core/index.ts +0 -3
  117. package/src/exec/index.ts +0 -3
  118. package/src/exec/logger.ts +0 -63
  119. package/src/exec/validate.ts +0 -238
  120. package/src/lib/enact-direct.ts +0 -254
  121. package/src/lib/index.ts +0 -1
  122. package/src/services/McpCoreService.ts +0 -201
  123. package/src/services/index.ts +0 -1
  124. package/src/utils/config.ts +0 -438
  125. package/src/utils/env-loader.ts +0 -370
  126. package/src/utils/help.ts +0 -257
  127. package/src/utils/index.ts +0 -7
  128. package/src/utils/silent-monitor.ts +0 -328
  129. package/src/utils/timeout.ts +0 -26
  130. package/src/web/env-manager-server.ts +0 -465
  131. package/src/web/index.ts +0 -1
  132. package/src/web/static/app.js +0 -663
  133. package/src/web/static/index.html +0 -117
  134. package/src/web/static/style.css +0 -291
@@ -1,83 +1,294 @@
1
- // src/utils/logger.ts
2
- import pc from "picocolors";
3
-
4
- export enum LogLevel {
5
- DEBUG = 0,
6
- INFO = 1,
7
- SUCCESS = 2,
8
- WARN = 3,
9
- ERROR = 4,
10
- }
11
-
12
- // Default log level
13
- let currentLogLevel = LogLevel.INFO;
14
-
15
1
  /**
16
- * Sets the current log level
2
+ * @enactprotocol/shared - Logger utility
3
+ *
4
+ * Provides structured logging with level filtering and
5
+ * configurable output formats (console with colors or JSON).
17
6
  */
18
- export function setLogLevel(level: LogLevel): void {
19
- currentLogLevel = level;
7
+
8
+ export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
9
+
10
+ export interface LogEntry {
11
+ timestamp: string;
12
+ level: LogLevel;
13
+ message: string;
14
+ context?: Record<string, unknown> | undefined;
20
15
  }
21
16
 
22
- /**
23
- * Debug log - only shown when log level is DEBUG
24
- */
25
- export function debug(message: string): void {
26
- if (currentLogLevel <= LogLevel.DEBUG) {
27
- console.error(pc.dim(`🔍 ${message}`));
28
- }
17
+ export interface LoggerOptions {
18
+ /** Minimum level to output */
19
+ level?: LogLevel;
20
+ /** Output format: 'console' for colored output, 'json' for structured */
21
+ format?: "console" | "json";
22
+ /** Enable colors in console output */
23
+ colors?: boolean;
24
+ /** Custom output function (defaults to console) */
25
+ output?: (text: string) => void;
26
+ /** Custom error output function (defaults to console.error) */
27
+ errorOutput?: (text: string) => void;
28
+ /** Prefix for log messages */
29
+ prefix?: string;
29
30
  }
30
31
 
32
+ /** Level priorities (higher = more severe) */
33
+ const LEVEL_PRIORITY: Record<LogLevel, number> = {
34
+ debug: 0,
35
+ info: 1,
36
+ warn: 2,
37
+ error: 3,
38
+ silent: 4,
39
+ };
40
+
41
+ /** ANSI color codes */
42
+ const COLORS = {
43
+ reset: "\x1b[0m",
44
+ dim: "\x1b[2m",
45
+ bold: "\x1b[1m",
46
+ red: "\x1b[31m",
47
+ yellow: "\x1b[33m",
48
+ blue: "\x1b[34m",
49
+ cyan: "\x1b[36m",
50
+ gray: "\x1b[90m",
51
+ };
52
+
53
+ /** Level display colors */
54
+ const LEVEL_COLORS: Record<Exclude<LogLevel, "silent">, string> = {
55
+ debug: COLORS.gray,
56
+ info: COLORS.blue,
57
+ warn: COLORS.yellow,
58
+ error: COLORS.red,
59
+ };
60
+
61
+ /** Level labels for output */
62
+ const LEVEL_LABELS: Record<Exclude<LogLevel, "silent">, string> = {
63
+ debug: "DEBUG",
64
+ info: "INFO",
65
+ warn: "WARN",
66
+ error: "ERROR",
67
+ };
68
+
31
69
  /**
32
- * Info log - general information
70
+ * Logger class with level filtering and structured output
33
71
  */
34
- export function info(message: string): void {
35
- if (currentLogLevel <= LogLevel.INFO) {
36
- console.error(pc.blue(`ℹ️ ${message}`));
37
- }
72
+ export class Logger {
73
+ private level: LogLevel;
74
+ private format: "console" | "json";
75
+ private colors: boolean;
76
+ private output: (text: string) => void;
77
+ private errorOutput: (text: string) => void;
78
+ private prefix: string;
79
+
80
+ constructor(options: LoggerOptions = {}) {
81
+ this.level = options.level ?? "info";
82
+ this.format = options.format ?? "console";
83
+ this.colors = options.colors ?? true;
84
+ this.output = options.output ?? console.log;
85
+ this.errorOutput = options.errorOutput ?? console.error;
86
+ this.prefix = options.prefix ?? "";
87
+ }
88
+
89
+ /**
90
+ * Check if a level should be logged
91
+ */
92
+ shouldLog(level: LogLevel): boolean {
93
+ return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.level];
94
+ }
95
+
96
+ /**
97
+ * Set the minimum log level
98
+ */
99
+ setLevel(level: LogLevel): void {
100
+ this.level = level;
101
+ }
102
+
103
+ /**
104
+ * Get the current log level
105
+ */
106
+ getLevel(): LogLevel {
107
+ return this.level;
108
+ }
109
+
110
+ /**
111
+ * Set the output format
112
+ */
113
+ setFormat(format: "console" | "json"): void {
114
+ this.format = format;
115
+ }
116
+
117
+ /**
118
+ * Enable or disable colors
119
+ */
120
+ setColors(enabled: boolean): void {
121
+ this.colors = enabled;
122
+ }
123
+
124
+ /**
125
+ * Set the prefix for log messages
126
+ */
127
+ setPrefix(prefix: string): void {
128
+ this.prefix = prefix;
129
+ }
130
+
131
+ /**
132
+ * Create a child logger with a prefix
133
+ */
134
+ child(prefix: string): Logger {
135
+ const childPrefix = this.prefix ? `${this.prefix}:${prefix}` : prefix;
136
+ return new Logger({
137
+ level: this.level,
138
+ format: this.format,
139
+ colors: this.colors,
140
+ output: this.output,
141
+ errorOutput: this.errorOutput,
142
+ prefix: childPrefix,
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Log a debug message
148
+ */
149
+ debug(message: string, context?: Record<string, unknown>): void {
150
+ this.log("debug", message, context);
151
+ }
152
+
153
+ /**
154
+ * Log an info message
155
+ */
156
+ info(message: string, context?: Record<string, unknown>): void {
157
+ this.log("info", message, context);
158
+ }
159
+
160
+ /**
161
+ * Log a warning message
162
+ */
163
+ warn(message: string, context?: Record<string, unknown>): void {
164
+ this.log("warn", message, context);
165
+ }
166
+
167
+ /**
168
+ * Log an error message
169
+ */
170
+ error(message: string, context?: Record<string, unknown>): void {
171
+ this.log("error", message, context);
172
+ }
173
+
174
+ /**
175
+ * Core logging method
176
+ */
177
+ private log(
178
+ level: Exclude<LogLevel, "silent">,
179
+ message: string,
180
+ context?: Record<string, unknown>
181
+ ): void {
182
+ if (!this.shouldLog(level)) {
183
+ return;
184
+ }
185
+
186
+ const entry: LogEntry = {
187
+ timestamp: new Date().toISOString(),
188
+ level,
189
+ message: this.prefix ? `[${this.prefix}] ${message}` : message,
190
+ context,
191
+ };
192
+
193
+ const formatted = this.format === "json" ? this.formatJson(entry) : this.formatConsole(entry);
194
+
195
+ // Use error output for warn/error levels
196
+ if (level === "warn" || level === "error") {
197
+ this.errorOutput(formatted);
198
+ } else {
199
+ this.output(formatted);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Format entry as JSON
205
+ */
206
+ private formatJson(entry: LogEntry): string {
207
+ const obj: Record<string, unknown> = {
208
+ timestamp: entry.timestamp,
209
+ level: entry.level,
210
+ message: entry.message,
211
+ };
212
+
213
+ if (entry.context && Object.keys(entry.context).length > 0) {
214
+ obj.context = entry.context;
215
+ }
216
+
217
+ return JSON.stringify(obj);
218
+ }
219
+
220
+ /**
221
+ * Format entry for console with colors
222
+ */
223
+ private formatConsole(entry: LogEntry): string {
224
+ const parts: string[] = [];
225
+
226
+ // Timestamp (dim)
227
+ if (this.colors) {
228
+ parts.push(`${COLORS.dim}${entry.timestamp}${COLORS.reset}`);
229
+ } else {
230
+ parts.push(entry.timestamp);
231
+ }
232
+
233
+ // Level label with color (only active levels, not silent)
234
+ const level = entry.level as Exclude<LogLevel, "silent">;
235
+ const label = LEVEL_LABELS[level];
236
+ if (this.colors) {
237
+ const color = LEVEL_COLORS[level];
238
+ parts.push(`${color}${label.padEnd(5)}${COLORS.reset}`);
239
+ } else {
240
+ parts.push(label.padEnd(5));
241
+ }
242
+
243
+ // Message
244
+ parts.push(entry.message);
245
+
246
+ // Context (if any)
247
+ if (entry.context && Object.keys(entry.context).length > 0) {
248
+ const contextStr = JSON.stringify(entry.context);
249
+ if (this.colors) {
250
+ parts.push(`${COLORS.dim}${contextStr}${COLORS.reset}`);
251
+ } else {
252
+ parts.push(contextStr);
253
+ }
254
+ }
255
+
256
+ return parts.join(" ");
257
+ }
38
258
  }
39
259
 
40
260
  /**
41
- * Success log - operation completed successfully
261
+ * Default logger instance
42
262
  */
43
- export function success(message: string): void {
44
- if (currentLogLevel <= LogLevel.SUCCESS) {
45
- console.error(pc.green(`✓ ${message}`));
46
- }
47
- }
263
+ let defaultLogger = new Logger();
48
264
 
49
265
  /**
50
- * Warning log - non-critical issues
266
+ * Get the default logger instance
51
267
  */
52
- export function warn(message: string): void {
53
- if (currentLogLevel <= LogLevel.WARN) {
54
- console.error(pc.yellow(`⚠️ ${message}`));
55
- }
268
+ export function getLogger(): Logger {
269
+ return defaultLogger;
56
270
  }
57
271
 
58
272
  /**
59
- * Error log - critical issues
273
+ * Configure the default logger
60
274
  */
61
- export function error(message: string, details?: any): void {
62
- if (currentLogLevel <= LogLevel.ERROR) {
63
- console.error(pc.red(`✗ Error: ${message}`));
64
-
65
- if (details && currentLogLevel === LogLevel.DEBUG) {
66
- console.error(pc.dim("Details:"));
67
- console.error(details);
68
- }
69
- }
275
+ export function configureLogger(options: LoggerOptions): void {
276
+ defaultLogger = new Logger(options);
70
277
  }
71
278
 
72
279
  /**
73
- * Table log - display tabular data
280
+ * Create a new logger with the given options
74
281
  */
75
- export function table(data: any[], columns?: string[]): void {
76
- if (currentLogLevel <= LogLevel.INFO) {
77
- if (columns) {
78
- console.table(data, columns);
79
- } else {
80
- console.table(data);
81
- }
82
- }
282
+ export function createLogger(options?: LoggerOptions): Logger {
283
+ return new Logger(options);
83
284
  }
285
+
286
+ // Convenience exports for quick logging
287
+ export const debug = (message: string, context?: Record<string, unknown>) =>
288
+ defaultLogger.debug(message, context);
289
+ export const info = (message: string, context?: Record<string, unknown>) =>
290
+ defaultLogger.info(message, context);
291
+ export const warn = (message: string, context?: Record<string, unknown>) =>
292
+ defaultLogger.warn(message, context);
293
+ export const error = (message: string, context?: Record<string, unknown>) =>
294
+ defaultLogger.error(message, context);
@@ -1,37 +1,305 @@
1
- // src/utils/version.ts
2
- import pc from "picocolors";
3
- import { readFileSync } from "fs";
4
- import { join, dirname } from "path";
5
- import { fileURLToPath } from "url";
6
-
7
- /**
8
- * Displays the CLI version with nice formatting
9
- */
10
- export function showVersion(): void {
11
- let version = "0.0.1-dev";
12
-
13
- try {
14
- // Try to get version from environment first (for npm scripts)
15
- if (process.env.npm_package_version) {
16
- version = process.env.npm_package_version;
17
- } else {
18
- // When running as installed binary, read from package.json
19
- // Go up from shared/dist/utils/version.js to find package.json
20
- const currentFile = typeof __filename !== 'undefined' ? __filename : fileURLToPath(import.meta.url);
21
- const sharedDir = dirname(dirname(dirname(currentFile)));
22
- const packageJsonPath = join(sharedDir, 'package.json');
23
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
24
- version = packageJson.version;
25
- }
26
- } catch (error) {
27
- // Fall back to default version if anything fails
28
- version = "1.0.14";
29
- }
30
-
31
- const versionText = `v${version}`;
32
-
33
- console.error(`
34
- ${pc.bold("Enact CLI")} ${pc.cyan(versionText)}
35
- ${pc.dim("A tool to create and publish enact documents.")}
36
- `);
1
+ /**
2
+ * @enactprotocol/shared - Version utilities
3
+ *
4
+ * Provides semver parsing, comparison, and range checking.
5
+ */
6
+
7
+ export interface ParsedVersion {
8
+ major: number;
9
+ minor: number;
10
+ patch: number;
11
+ prerelease?: string | undefined;
12
+ build?: string | undefined;
13
+ raw: string;
14
+ }
15
+
16
+ export interface VersionRange {
17
+ operator: "=" | ">" | ">=" | "<" | "<=" | "^" | "~";
18
+ version: ParsedVersion;
19
+ raw: string;
20
+ }
21
+
22
+ /**
23
+ * Parse a semver version string
24
+ * Supports: 1.0.0, 1.0, 1, 1.0.0-alpha, 1.0.0+build, 1.0.0-alpha+build
25
+ */
26
+ export function parseVersion(version: string): ParsedVersion | null {
27
+ const trimmed = version.trim();
28
+
29
+ // Remove leading 'v' if present (common in tags)
30
+ const normalized = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
31
+
32
+ // Full semver regex with prerelease and build metadata
33
+ const semverRegex =
34
+ /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([a-zA-Z0-9.-]+))?(?:\+([a-zA-Z0-9.-]+))?$/;
35
+ const match = normalized.match(semverRegex);
36
+
37
+ if (!match) {
38
+ return null;
39
+ }
40
+
41
+ const major = Number.parseInt(match[1] ?? "0", 10);
42
+ const minor = Number.parseInt(match[2] ?? "0", 10);
43
+ const patch = Number.parseInt(match[3] ?? "0", 10);
44
+
45
+ return {
46
+ major,
47
+ minor,
48
+ patch,
49
+ prerelease: match[4],
50
+ build: match[5],
51
+ raw: trimmed,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Check if a string is a valid semver version
57
+ */
58
+ export function isValidVersion(version: string): boolean {
59
+ return parseVersion(version) !== null;
60
+ }
61
+
62
+ /**
63
+ * Compare two version strings
64
+ * @returns -1 if a < b, 0 if a === b, 1 if a > b
65
+ */
66
+ export function compareVersions(a: string, b: string): -1 | 0 | 1 {
67
+ const parsedA = parseVersion(a);
68
+ const parsedB = parseVersion(b);
69
+
70
+ // Invalid versions sort last
71
+ if (!parsedA && !parsedB) return 0;
72
+ if (!parsedA) return 1;
73
+ if (!parsedB) return -1;
74
+
75
+ // Compare major.minor.patch
76
+ if (parsedA.major !== parsedB.major) {
77
+ return parsedA.major < parsedB.major ? -1 : 1;
78
+ }
79
+ if (parsedA.minor !== parsedB.minor) {
80
+ return parsedA.minor < parsedB.minor ? -1 : 1;
81
+ }
82
+ if (parsedA.patch !== parsedB.patch) {
83
+ return parsedA.patch < parsedB.patch ? -1 : 1;
84
+ }
85
+
86
+ // Prerelease comparison
87
+ // A version without prerelease is greater than one with
88
+ if (!parsedA.prerelease && parsedB.prerelease) return 1;
89
+ if (parsedA.prerelease && !parsedB.prerelease) return -1;
90
+
91
+ // Both have prereleases - compare lexically
92
+ if (parsedA.prerelease && parsedB.prerelease) {
93
+ const prereleaseCompare = comparePrerelease(parsedA.prerelease, parsedB.prerelease);
94
+ if (prereleaseCompare !== 0) return prereleaseCompare;
95
+ }
96
+
97
+ return 0;
98
+ }
99
+
100
+ /**
101
+ * Compare prerelease strings according to semver rules
102
+ */
103
+ function comparePrerelease(a: string, b: string): -1 | 0 | 1 {
104
+ const partsA = a.split(".");
105
+ const partsB = b.split(".");
106
+
107
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
108
+ const partA = partsA[i];
109
+ const partB = partsB[i];
110
+
111
+ // Shorter prerelease is less than longer
112
+ if (partA === undefined) return -1;
113
+ if (partB === undefined) return 1;
114
+
115
+ const numA = Number.parseInt(partA, 10);
116
+ const numB = Number.parseInt(partB, 10);
117
+
118
+ const isNumA = !Number.isNaN(numA) && String(numA) === partA;
119
+ const isNumB = !Number.isNaN(numB) && String(numB) === partB;
120
+
121
+ // Numeric identifiers are less than alphanumeric
122
+ if (isNumA && !isNumB) return -1;
123
+ if (!isNumA && isNumB) return 1;
124
+
125
+ // Both numeric - compare as numbers
126
+ if (isNumA && isNumB) {
127
+ if (numA < numB) return -1;
128
+ if (numA > numB) return 1;
129
+ continue;
130
+ }
131
+
132
+ // Both alphanumeric - compare lexically
133
+ if (partA < partB) return -1;
134
+ if (partA > partB) return 1;
135
+ }
136
+
137
+ return 0;
138
+ }
139
+
140
+ /**
141
+ * Parse a version range string
142
+ * Supports: =1.0.0, >1.0.0, >=1.0.0, <1.0.0, <=1.0.0, ^1.0.0, ~1.0.0
143
+ */
144
+ export function parseRange(range: string): VersionRange | null {
145
+ const trimmed = range.trim();
146
+
147
+ // Match operator at start
148
+ const rangeRegex = /^([=><^~]+)?\s*(.+)$/;
149
+ const match = trimmed.match(rangeRegex);
150
+
151
+ if (!match || !match[2]) {
152
+ return null;
153
+ }
154
+
155
+ const operator = (match[1] ?? "=") as VersionRange["operator"];
156
+ const versionStr = match[2];
157
+
158
+ // Validate operator
159
+ if (!["=", ">", ">=", "<", "<=", "^", "~"].includes(operator)) {
160
+ return null;
161
+ }
162
+
163
+ const version = parseVersion(versionStr);
164
+ if (!version) {
165
+ return null;
166
+ }
167
+
168
+ return {
169
+ operator,
170
+ version,
171
+ raw: trimmed,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Check if a version satisfies a range
177
+ */
178
+ export function satisfiesRange(version: string, range: string): boolean {
179
+ const parsedVersion = parseVersion(version);
180
+ const parsedRange = parseRange(range);
181
+
182
+ if (!parsedVersion || !parsedRange) {
183
+ return false;
184
+ }
185
+
186
+ const comparison = compareVersions(version, parsedRange.version.raw);
187
+
188
+ switch (parsedRange.operator) {
189
+ case "=":
190
+ return comparison === 0;
191
+ case ">":
192
+ return comparison === 1;
193
+ case ">=":
194
+ return comparison >= 0;
195
+ case "<":
196
+ return comparison === -1;
197
+ case "<=":
198
+ return comparison <= 0;
199
+ case "^":
200
+ // Caret range: compatible changes (same major, >= version)
201
+ // ^1.2.3 := >=1.2.3 <2.0.0
202
+ // ^0.2.3 := >=0.2.3 <0.3.0
203
+ // ^0.0.3 := >=0.0.3 <0.0.4
204
+ if (comparison === -1) return false;
205
+ if (parsedRange.version.major === 0) {
206
+ if (parsedRange.version.minor === 0) {
207
+ // ^0.0.x - must be exact patch
208
+ return parsedVersion.patch === parsedRange.version.patch;
209
+ }
210
+ // ^0.x - must be same minor
211
+ return parsedVersion.minor === parsedRange.version.minor;
212
+ }
213
+ // ^x - must be same major
214
+ return parsedVersion.major === parsedRange.version.major;
215
+ case "~":
216
+ // Tilde range: patch-level changes (same major.minor, >= version)
217
+ // ~1.2.3 := >=1.2.3 <1.3.0
218
+ if (comparison === -1) return false;
219
+ return (
220
+ parsedVersion.major === parsedRange.version.major &&
221
+ parsedVersion.minor === parsedRange.version.minor
222
+ );
223
+ default:
224
+ return false;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Sort versions in ascending order
230
+ */
231
+ export function sortVersions(versions: string[]): string[] {
232
+ return [...versions].sort(compareVersions);
233
+ }
234
+
235
+ /**
236
+ * Get the highest version from a list
237
+ */
238
+ export function getHighestVersion(versions: string[]): string | null {
239
+ const valid = versions.filter(isValidVersion);
240
+ if (valid.length === 0) return null;
241
+
242
+ const sorted = sortVersions(valid);
243
+ const last = sorted[sorted.length - 1];
244
+ return last ?? null;
245
+ }
246
+
247
+ /**
248
+ * Increment a version by bump type
249
+ */
250
+ export function incrementVersion(
251
+ version: string,
252
+ bump: "major" | "minor" | "patch"
253
+ ): string | null {
254
+ const parsed = parseVersion(version);
255
+ if (!parsed) return null;
256
+
257
+ switch (bump) {
258
+ case "major":
259
+ return `${parsed.major + 1}.0.0`;
260
+ case "minor":
261
+ return `${parsed.major}.${parsed.minor + 1}.0`;
262
+ case "patch":
263
+ return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Coerce a string to a valid semver, filling in missing parts
269
+ */
270
+ export function coerceVersion(version: string): string | null {
271
+ const trimmed = version.trim();
272
+ const normalized = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
273
+
274
+ // Try parsing as-is first
275
+ const parsed = parseVersion(normalized);
276
+ if (parsed) {
277
+ return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
278
+ }
279
+
280
+ // Try extracting numbers
281
+ const numbers = normalized.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
282
+ if (!numbers) {
283
+ return null;
284
+ }
285
+
286
+ const major = Number.parseInt(numbers[1] ?? "0", 10);
287
+ const minor = Number.parseInt(numbers[2] ?? "0", 10);
288
+ const patch = Number.parseInt(numbers[3] ?? "0", 10);
289
+
290
+ return `${major}.${minor}.${patch}`;
291
+ }
292
+
293
+ /**
294
+ * Format a parsed version back to string
295
+ */
296
+ export function formatVersion(version: ParsedVersion): string {
297
+ let result = `${version.major}.${version.minor}.${version.patch}`;
298
+ if (version.prerelease) {
299
+ result += `-${version.prerelease}`;
300
+ }
301
+ if (version.build) {
302
+ result += `+${version.build}`;
303
+ }
304
+ return result;
37
305
  }