@enactprotocol/shared 1.2.13 → 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.
- package/README.md +44 -0
- package/package.json +16 -58
- package/src/config.ts +476 -0
- package/src/constants.ts +36 -0
- package/src/execution/command.ts +314 -0
- package/src/execution/index.ts +73 -0
- package/src/execution/runtime.ts +308 -0
- package/src/execution/types.ts +379 -0
- package/src/execution/validation.ts +508 -0
- package/src/index.ts +237 -30
- package/src/manifest/index.ts +36 -0
- package/src/manifest/loader.ts +187 -0
- package/src/manifest/parser.ts +173 -0
- package/src/manifest/validator.ts +309 -0
- package/src/paths.ts +108 -0
- package/src/registry.ts +219 -0
- package/src/resolver.ts +345 -0
- package/src/types/index.ts +30 -0
- package/src/types/manifest.ts +255 -0
- package/src/types.ts +5 -188
- package/src/utils/fs.ts +281 -0
- package/src/utils/logger.ts +270 -59
- package/src/utils/version.ts +304 -36
- package/tests/config.test.ts +515 -0
- package/tests/execution/command.test.ts +317 -0
- package/tests/execution/validation.test.ts +384 -0
- package/tests/fixtures/invalid-tool.yaml +4 -0
- package/tests/fixtures/valid-tool.md +62 -0
- package/tests/fixtures/valid-tool.yaml +40 -0
- package/tests/index.test.ts +8 -0
- package/tests/manifest/loader.test.ts +291 -0
- package/tests/manifest/parser.test.ts +345 -0
- package/tests/manifest/validator.test.ts +394 -0
- package/tests/manifest-types.test.ts +358 -0
- package/tests/paths.test.ts +153 -0
- package/tests/registry.test.ts +231 -0
- package/tests/resolver.test.ts +272 -0
- package/tests/utils/fs.test.ts +388 -0
- package/tests/utils/logger.test.ts +480 -0
- package/tests/utils/version.test.ts +390 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/LocalToolResolver.d.ts +0 -84
- package/dist/LocalToolResolver.js +0 -353
- package/dist/api/enact-api.d.ts +0 -130
- package/dist/api/enact-api.js +0 -428
- package/dist/api/index.d.ts +0 -2
- package/dist/api/index.js +0 -2
- package/dist/api/types.d.ts +0 -103
- package/dist/api/types.js +0 -1
- package/dist/constants.d.ts +0 -7
- package/dist/constants.js +0 -10
- package/dist/core/DaggerExecutionProvider.d.ts +0 -169
- package/dist/core/DaggerExecutionProvider.js +0 -1029
- package/dist/core/DirectExecutionProvider.d.ts +0 -23
- package/dist/core/DirectExecutionProvider.js +0 -406
- package/dist/core/EnactCore.d.ts +0 -162
- package/dist/core/EnactCore.js +0 -597
- package/dist/core/NativeExecutionProvider.d.ts +0 -9
- package/dist/core/NativeExecutionProvider.js +0 -16
- package/dist/core/index.d.ts +0 -3
- package/dist/core/index.js +0 -3
- package/dist/exec/index.d.ts +0 -3
- package/dist/exec/index.js +0 -3
- package/dist/exec/logger.d.ts +0 -11
- package/dist/exec/logger.js +0 -57
- package/dist/exec/validate.d.ts +0 -5
- package/dist/exec/validate.js +0 -167
- package/dist/index.d.ts +0 -21
- package/dist/index.js +0 -25
- package/dist/lib/enact-direct.d.ts +0 -150
- package/dist/lib/enact-direct.js +0 -159
- package/dist/lib/index.d.ts +0 -1
- package/dist/lib/index.js +0 -1
- package/dist/security/index.d.ts +0 -3
- package/dist/security/index.js +0 -3
- package/dist/security/security.d.ts +0 -23
- package/dist/security/security.js +0 -137
- package/dist/security/sign.d.ts +0 -103
- package/dist/security/sign.js +0 -666
- package/dist/security/verification-enforcer.d.ts +0 -53
- package/dist/security/verification-enforcer.js +0 -204
- package/dist/services/McpCoreService.d.ts +0 -98
- package/dist/services/McpCoreService.js +0 -124
- package/dist/services/index.d.ts +0 -1
- package/dist/services/index.js +0 -1
- package/dist/types.d.ts +0 -132
- package/dist/types.js +0 -3
- package/dist/utils/config.d.ts +0 -111
- package/dist/utils/config.js +0 -342
- package/dist/utils/env-loader.d.ts +0 -54
- package/dist/utils/env-loader.js +0 -270
- package/dist/utils/help.d.ts +0 -36
- package/dist/utils/help.js +0 -248
- package/dist/utils/index.d.ts +0 -7
- package/dist/utils/index.js +0 -7
- package/dist/utils/logger.d.ts +0 -35
- package/dist/utils/logger.js +0 -75
- package/dist/utils/silent-monitor.d.ts +0 -67
- package/dist/utils/silent-monitor.js +0 -242
- package/dist/utils/timeout.d.ts +0 -5
- package/dist/utils/timeout.js +0 -23
- package/dist/utils/version.d.ts +0 -4
- package/dist/utils/version.js +0 -35
- package/dist/web/env-manager-server.d.ts +0 -29
- package/dist/web/env-manager-server.js +0 -367
- package/dist/web/index.d.ts +0 -1
- package/dist/web/index.js +0 -1
- package/src/LocalToolResolver.ts +0 -424
- package/src/api/enact-api.ts +0 -604
- package/src/api/index.ts +0 -2
- package/src/api/types.ts +0 -114
- package/src/core/DaggerExecutionProvider.ts +0 -1357
- package/src/core/DirectExecutionProvider.ts +0 -484
- package/src/core/EnactCore.ts +0 -847
- package/src/core/index.ts +0 -3
- package/src/exec/index.ts +0 -3
- package/src/exec/logger.ts +0 -63
- package/src/exec/validate.ts +0 -238
- package/src/lib/enact-direct.ts +0 -254
- package/src/lib/index.ts +0 -1
- package/src/services/McpCoreService.ts +0 -201
- package/src/services/index.ts +0 -1
- package/src/utils/config.ts +0 -438
- package/src/utils/env-loader.ts +0 -370
- package/src/utils/help.ts +0 -257
- package/src/utils/index.ts +0 -7
- package/src/utils/silent-monitor.ts +0 -328
- package/src/utils/timeout.ts +0 -26
- package/src/web/env-manager-server.ts +0 -465
- package/src/web/index.ts +0 -1
- package/src/web/static/app.js +0 -663
- package/src/web/static/index.html +0 -117
- package/src/web/static/style.css +0 -291
package/src/utils/logger.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
*
|
|
70
|
+
* Logger class with level filtering and structured output
|
|
33
71
|
*/
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
*
|
|
261
|
+
* Default logger instance
|
|
42
262
|
*/
|
|
43
|
-
|
|
44
|
-
if (currentLogLevel <= LogLevel.SUCCESS) {
|
|
45
|
-
console.error(pc.green(`✓ ${message}`));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
263
|
+
let defaultLogger = new Logger();
|
|
48
264
|
|
|
49
265
|
/**
|
|
50
|
-
*
|
|
266
|
+
* Get the default logger instance
|
|
51
267
|
*/
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
-
console.error(pc.yellow(`⚠️ ${message}`));
|
|
55
|
-
}
|
|
268
|
+
export function getLogger(): Logger {
|
|
269
|
+
return defaultLogger;
|
|
56
270
|
}
|
|
57
271
|
|
|
58
272
|
/**
|
|
59
|
-
*
|
|
273
|
+
* Configure the default logger
|
|
60
274
|
*/
|
|
61
|
-
export function
|
|
62
|
-
|
|
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
|
-
*
|
|
280
|
+
* Create a new logger with the given options
|
|
74
281
|
*/
|
|
75
|
-
export function
|
|
76
|
-
|
|
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);
|
package/src/utils/version.ts
CHANGED
|
@@ -1,37 +1,305 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
}
|