@katmer/core 0.0.3

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 (71) hide show
  1. package/README.md +1 -0
  2. package/cli/katmer.js +28 -0
  3. package/cli/run.ts +16 -0
  4. package/index.ts +5 -0
  5. package/lib/config.ts +82 -0
  6. package/lib/interfaces/config.interface.ts +113 -0
  7. package/lib/interfaces/executor.interface.ts +13 -0
  8. package/lib/interfaces/module.interface.ts +170 -0
  9. package/lib/interfaces/provider.interface.ts +214 -0
  10. package/lib/interfaces/task.interface.ts +100 -0
  11. package/lib/katmer.ts +126 -0
  12. package/lib/lookup/env.lookup.ts +13 -0
  13. package/lib/lookup/file.lookup.ts +23 -0
  14. package/lib/lookup/index.ts +46 -0
  15. package/lib/lookup/url.lookup.ts +21 -0
  16. package/lib/lookup/var.lookup.ts +13 -0
  17. package/lib/module.ts +560 -0
  18. package/lib/module_registry.ts +64 -0
  19. package/lib/modules/apt-repository/apt-repository.module.ts +435 -0
  20. package/lib/modules/apt-repository/apt-sources-list.ts +363 -0
  21. package/lib/modules/apt.module.ts +546 -0
  22. package/lib/modules/archive.module.ts +280 -0
  23. package/lib/modules/become.module.ts +119 -0
  24. package/lib/modules/copy.module.ts +807 -0
  25. package/lib/modules/cron.module.ts +541 -0
  26. package/lib/modules/debug.module.ts +231 -0
  27. package/lib/modules/gather_facts.module.ts +605 -0
  28. package/lib/modules/git.module.ts +243 -0
  29. package/lib/modules/hostname.module.ts +213 -0
  30. package/lib/modules/http/http.curl.module.ts +342 -0
  31. package/lib/modules/http/http.local.module.ts +253 -0
  32. package/lib/modules/http/http.module.ts +298 -0
  33. package/lib/modules/index.ts +14 -0
  34. package/lib/modules/package.module.ts +283 -0
  35. package/lib/modules/script.module.ts +121 -0
  36. package/lib/modules/set_fact.module.ts +171 -0
  37. package/lib/modules/systemd_service.module.ts +373 -0
  38. package/lib/modules/template.module.ts +478 -0
  39. package/lib/providers/local.provider.ts +336 -0
  40. package/lib/providers/provider_response.ts +20 -0
  41. package/lib/providers/ssh/ssh.provider.ts +420 -0
  42. package/lib/providers/ssh/ssh.utils.ts +31 -0
  43. package/lib/schemas/katmer_config.schema.json +358 -0
  44. package/lib/target_resolver.ts +298 -0
  45. package/lib/task/controls/environment.control.ts +42 -0
  46. package/lib/task/controls/index.ts +13 -0
  47. package/lib/task/controls/loop.control.ts +89 -0
  48. package/lib/task/controls/register.control.ts +23 -0
  49. package/lib/task/controls/until.control.ts +64 -0
  50. package/lib/task/controls/when.control.ts +25 -0
  51. package/lib/task/task.ts +225 -0
  52. package/lib/utils/ajv.utils.ts +24 -0
  53. package/lib/utils/cls.ts +4 -0
  54. package/lib/utils/datetime.utils.ts +15 -0
  55. package/lib/utils/errors.ts +25 -0
  56. package/lib/utils/execute-shell.ts +116 -0
  57. package/lib/utils/file.utils.ts +68 -0
  58. package/lib/utils/http.utils.ts +10 -0
  59. package/lib/utils/json.utils.ts +15 -0
  60. package/lib/utils/number.utils.ts +9 -0
  61. package/lib/utils/object.utils.ts +11 -0
  62. package/lib/utils/os.utils.ts +31 -0
  63. package/lib/utils/path.utils.ts +9 -0
  64. package/lib/utils/renderer/render_functions.ts +3 -0
  65. package/lib/utils/renderer/renderer.ts +89 -0
  66. package/lib/utils/renderer/twig.ts +191 -0
  67. package/lib/utils/string.utils.ts +33 -0
  68. package/lib/utils/typed-event-emitter.ts +26 -0
  69. package/lib/utils/unix.utils.ts +91 -0
  70. package/lib/utils/windows.utils.ts +92 -0
  71. package/package.json +67 -0
package/lib/module.ts ADDED
@@ -0,0 +1,560 @@
1
+ import type {
2
+ KatmerProvider,
3
+ OsArch,
4
+ OsFamily,
5
+ OsInfo
6
+ } from "./interfaces/provider.interface"
7
+ import type { Katmer } from "./interfaces/task.interface"
8
+ import type {
9
+ ModuleCommonReturn,
10
+ ModuleConstraints,
11
+ ModuleOptions,
12
+ ModulePlatformConstraint,
13
+ PackageConstraint,
14
+ BinaryConstraint,
15
+ PackageManager
16
+ } from "./interfaces/module.interface"
17
+ import { wrapInArray } from "./utils/json.utils"
18
+ import semver from "semver"
19
+
20
+ export abstract class KatmerModule<
21
+ TOptions extends ModuleOptions = ModuleOptions,
22
+ TReturn extends { [key: string]: any } = {},
23
+ TProvider extends KatmerProvider = KatmerProvider
24
+ > {
25
+ static readonly name: string
26
+ abstract constraints: ModuleConstraints
27
+
28
+ constructor(
29
+ public params: TOptions,
30
+ public provider: TProvider
31
+ ) {}
32
+
33
+ private async checkConstraints(ctx: Katmer.TaskContext<TProvider>) {
34
+ const osInfo = ctx.provider.os
35
+ const family = (osInfo?.family || "unknown") as OsFamily
36
+ const arch = (osInfo?.arch || "unknown") as OsArch
37
+ const distro = normalizeDistroId(osInfo) // e.g. "ubuntu", "rhel", "alpine", ...
38
+
39
+ const platformMap = this.constraints.platform || {}
40
+
41
+ // Resolve base: specific family OR "any" OR "local
42
+ const base =
43
+ (ctx.provider.type === "local" &&
44
+ normalizeConstraint(platformMap.local)) ??
45
+ normalizeConstraint(platformMap[family]) ??
46
+ normalizeConstraint(platformMap.any)
47
+
48
+ if (!base) {
49
+ throw new Error(
50
+ `Module '${this.constructor.name}' does not support platform '${family}'`
51
+ )
52
+ }
53
+
54
+ // Merge distro overrides (any + specific)
55
+ const merged = mergeConstraints(
56
+ base,
57
+ normalizeConstraint(base.distro?.any),
58
+ normalizeConstraint(base.distro?.[distro])
59
+ )
60
+
61
+ // ARCH check
62
+ const archList = merged.arch?.length ? merged.arch : ["any"]
63
+ if (!archList.includes("any") && !archList.includes(arch)) {
64
+ throw new Error(
65
+ `Module '${this.constructor.name}' does not support architecture '${arch}' on '${family}'`
66
+ )
67
+ }
68
+
69
+ // root/admin requirement
70
+ if (merged.requireRoot) {
71
+ const isRoot = await checkRoot(ctx, family)
72
+ if (!isRoot) {
73
+ throw new Error(
74
+ `Module '${this.constructor.name}' requires elevated privileges (root/Administrator) on '${family}'.`
75
+ )
76
+ }
77
+ }
78
+
79
+ // kernel/OS version gates
80
+ if (merged.minKernel && family !== "windows") {
81
+ const kv = await getKernelVersion(ctx, family)
82
+ if (kv && !(await satisfiesVersion(kv, merged.minKernel))) {
83
+ throw new Error(
84
+ `Kernel version ${kv} does not satisfy required '${merged.minKernel}' for '${this.constructor.name}'.`
85
+ )
86
+ }
87
+ }
88
+ if (merged.minOsVersion) {
89
+ const ov = osInfo?.versionId
90
+ if (ov && !(await satisfiesVersion(ov, merged.minOsVersion))) {
91
+ throw new Error(
92
+ `OS version ${ov} does not satisfy required '${merged.minOsVersion}' for '${this.constructor.name}'.`
93
+ )
94
+ }
95
+ }
96
+
97
+ // binaries check
98
+ if (merged.binaries?.length) {
99
+ for (const b of merged.binaries) {
100
+ const ok = await checkBinary(ctx, family, b)
101
+ if (!ok) {
102
+ throw new Error(
103
+ `Required binary '${b.cmd}'${b.range ? ` (version ${b.range})` : ""} not satisfied for '${this.constructor.name}'.`
104
+ )
105
+ }
106
+ }
107
+ }
108
+
109
+ // packages check (presence + optional version)
110
+ const pkgList = normalizePackageList(merged.packages)
111
+ if (pkgList.length) {
112
+ const pm = await detectPackageManager(ctx, family)
113
+ for (const p of pkgList) {
114
+ const ok = await checkPackage(ctx, family, pm, p)
115
+ if (!ok) {
116
+ const name =
117
+ p.name ||
118
+ p.alternatives?.map((a) => a.name).join(" | ") ||
119
+ "<unknown>"
120
+ throw new Error(
121
+ `Required package '${name}'${
122
+ p.range ? ` (${p.range})`
123
+ : p.version ? ` (= ${p.version})`
124
+ : ""
125
+ } not satisfied on '${family}'${pm ? ` (manager: ${pm})` : ""}.`
126
+ )
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ async doCheck(ctx: Katmer.TaskContext<TProvider>) {
133
+ await this.checkConstraints(ctx)
134
+ await this.check(ctx)
135
+ }
136
+
137
+ async doInitialize(ctx: Katmer.TaskContext<TProvider>) {
138
+ await this.initialize(ctx)
139
+ }
140
+
141
+ async doExecute(
142
+ ctx: Katmer.TaskContext<TProvider>
143
+ ): Promise<ModuleCommonReturn & TReturn> {
144
+ return await this.execute(ctx)
145
+ }
146
+
147
+ async doCleanup(ctx: Katmer.TaskContext<TProvider>) {
148
+ await this.cleanup(ctx)
149
+ }
150
+
151
+ protected abstract check(ctx: Katmer.TaskContext<TProvider>): Promise<void>
152
+ protected abstract initialize(
153
+ ctx: Katmer.TaskContext<TProvider>
154
+ ): Promise<void>
155
+ protected abstract execute(
156
+ ctx: Katmer.TaskContext<TProvider>
157
+ ): Promise<ModuleCommonReturn & TReturn>
158
+ protected abstract cleanup(ctx: Katmer.TaskContext<TProvider>): Promise<void>
159
+ }
160
+
161
+ /* ------------------------ helpers ------------------------ */
162
+
163
+ // Combine family/distro layers (later overrides earlier). Null/false disables.
164
+ function mergeConstraints(
165
+ ...layers: (ModulePlatformConstraint | null | undefined)[]
166
+ ): ModulePlatformConstraint {
167
+ const out: ModulePlatformConstraint = {}
168
+ for (const l of layers) {
169
+ if (!l) continue
170
+ // simple shallow merge for defined keys
171
+ if (l.arch) out.arch = Array.isArray(l.arch) ? l.arch.slice() : [l.arch]
172
+ if (l.packages) out.packages = l.packages.slice()
173
+ if (l.binaries) out.binaries = l.binaries.slice()
174
+ if (typeof l.requireRoot === "boolean") out.requireRoot = l.requireRoot
175
+ if (l.minKernel) out.minKernel = l.minKernel
176
+ if (l.minOsVersion) out.minOsVersion = l.minOsVersion
177
+ if (l.distro) out.distro = { ...(out.distro || {}), ...l.distro }
178
+ }
179
+ return out
180
+ }
181
+
182
+ // Normalize a platform entry: true → {}, false/undefined → null
183
+ function normalizeConstraint(
184
+ c: true | false | ModulePlatformConstraint | undefined
185
+ ): ModulePlatformConstraint | null {
186
+ if (c === true) return {}
187
+ if (!c) return null
188
+ return c
189
+ }
190
+
191
+ function normalizeDistroId(osInfo?: OsInfo | any): string {
192
+ const raw = (
193
+ osInfo?.distroId ||
194
+ osInfo?.distro ||
195
+ osInfo?.id ||
196
+ osInfo?.name ||
197
+ ""
198
+ )
199
+ .toString()
200
+ .toLowerCase()
201
+ if (!raw) return "any"
202
+ // very light normalization
203
+ if (/ubuntu/.test(raw)) return "ubuntu"
204
+ if (/debian/.test(raw)) return "debian"
205
+ if (/rhel|red hat|redhat/.test(raw)) return "rhel"
206
+ if (/centos/.test(raw)) return "centos"
207
+ if (/rocky/.test(raw)) return "rocky"
208
+ if (/fedora/.test(raw)) return "fedora"
209
+ if (/alpine/.test(raw)) return "alpine"
210
+ if (/arch/.test(raw)) return "arch"
211
+ if (/sles|suse|opensuse/.test(raw)) return "opensuse"
212
+ if (/amzn|amazon linux/.test(raw)) return "amazon"
213
+ return raw
214
+ }
215
+
216
+ async function checkRoot(
217
+ ctx: Katmer.TaskContext<any>,
218
+ family: OsFamily | "unknown"
219
+ ) {
220
+ if (family === "windows") {
221
+ const ps = `powershell -NoProfile -NonInteractive -Command "[Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) | Write-Output"`
222
+ const r = await ctx.exec(ps)
223
+ return r.code === 0 && /True/i.test(r.stdout || "")
224
+ } else {
225
+ const r = await ctx.exec(`id -u`)
226
+ return r.code === 0 && String(r.stdout || "").trim() === "0"
227
+ }
228
+ }
229
+
230
+ async function getKernelVersion(
231
+ ctx: Katmer.TaskContext<any>,
232
+ family: OsFamily | "unknown"
233
+ ) {
234
+ if (family === "windows") return null
235
+ const r = await ctx.exec(`uname -r`)
236
+ if (r.code !== 0) return null
237
+ return String(r.stdout || "").trim()
238
+ }
239
+
240
+ async function checkBinary(
241
+ ctx: Katmer.TaskContext<any>,
242
+ family: OsFamily | "unknown",
243
+ b: BinaryConstraint
244
+ ): Promise<boolean> {
245
+ // OR group support
246
+ if (b.or && b.or.length) {
247
+ for (const alt of b.or) {
248
+ if (await checkBinary(ctx, family, alt)) return true
249
+ }
250
+ return false
251
+ }
252
+
253
+ // locate
254
+ let found = false
255
+ if (family === "windows") {
256
+ const r = await ctx.exec(
257
+ `powershell -NoProfile -NonInteractive -Command "Get-Command ${b.cmd} -ErrorAction SilentlyContinue | Select-Object -First 1"`
258
+ )
259
+ found = r.code === 0 && /\S/.test(r.stdout || "")
260
+ } else {
261
+ const r = await ctx.exec(
262
+ `sh -lc 'command -v ${shq(b.cmd)} >/dev/null 2>&1'`
263
+ )
264
+ found = r.code === 0
265
+ }
266
+ if (!found) return false
267
+
268
+ // version constraint (optional)
269
+ if (b.range || b.versionRegex) {
270
+ const args = b.args?.length ? b.args.join(" ") : "--version"
271
+ const r = await ctx.exec(`${b.cmd} ${args}`)
272
+ const out = (r.stdout || r.stderr || "").trim()
273
+ const ver =
274
+ b.versionRegex ?
275
+ (out.match(new RegExp(b.versionRegex))?.[1] || "").trim()
276
+ : coerceVersion(out)
277
+ if (!ver) return !b.range // if we couldn't parse, accept only when no range is requested
278
+ if (b.range && !(await satisfiesVersion(ver, b.range))) return false
279
+ }
280
+ return true
281
+ }
282
+
283
+ async function checkPackage(
284
+ ctx: Katmer.TaskContext<any>,
285
+ family: OsFamily | "unknown",
286
+ pm: PackageManager,
287
+ p: PackageConstraint
288
+ ): Promise<boolean> {
289
+ // Alternatives (any-of)
290
+ if (p.alternatives?.length) {
291
+ for (const alt of p.alternatives) {
292
+ if (await checkPackage(ctx, family, pm, alt)) return true
293
+ }
294
+ return false
295
+ }
296
+
297
+ // Custom test command shortcut
298
+ if (p.testCmd) {
299
+ const r = await ctx.exec(p.testCmd)
300
+ if (r.code !== 0) return false
301
+ const raw = (r.stdout || r.stderr || "").trim()
302
+ const ver =
303
+ p.versionRegex ?
304
+ (raw.match(new RegExp(p.versionRegex))?.[1] || "").trim()
305
+ : coerceVersion(raw)
306
+ if (p.version && ver) return verEquals(ver, p.version)
307
+ if (p.range && ver) return await satisfiesVersion(ver, p.range)
308
+ return true
309
+ }
310
+
311
+ // Package manager detection override
312
+ const managers: PackageManager[] =
313
+ Array.isArray(p.manager) ? p.manager
314
+ : p.manager ? [p.manager]
315
+ : [pm]
316
+ for (const m of managers) {
317
+ const ver = await queryPackageVersion(ctx, family, m, p.name)
318
+ if (!ver) continue
319
+ if (p.version && !verEquals(ver, p.version)) continue
320
+ if (p.range && !(await satisfiesVersion(ver, p.range))) continue
321
+ return true
322
+ }
323
+ return false
324
+ }
325
+
326
+ async function detectPackageManager(
327
+ ctx: Katmer.TaskContext<any>,
328
+ family: OsFamily | "unknown"
329
+ ): Promise<PackageManager> {
330
+ if (family === "windows") {
331
+ // prefer winget, then choco
332
+ if (
333
+ (
334
+ await ctx.exec(
335
+ `powershell -Command "Get-Command winget -ErrorAction SilentlyContinue"`
336
+ )
337
+ ).code === 0
338
+ )
339
+ return "winget"
340
+ if (
341
+ (
342
+ await ctx.exec(
343
+ `powershell -Command "Get-Command choco -ErrorAction SilentlyContinue"`
344
+ )
345
+ ).code === 0
346
+ )
347
+ return "choco"
348
+ return "unknown"
349
+ }
350
+ // POSIX probes
351
+ if ((await ctx.exec(`sh -lc 'command -v apt >/dev/null 2>&1'`)).code === 0)
352
+ return "apt"
353
+ if ((await ctx.exec(`sh -lc 'command -v dnf >/dev/null 2>&1'`)).code === 0)
354
+ return "dnf"
355
+ if ((await ctx.exec(`sh -lc 'command -v yum >/dev/null 2>&1'`)).code === 0)
356
+ return "yum"
357
+ if ((await ctx.exec(`sh -lc 'command -v zypper >/dev/null 2>&1'`)).code === 0)
358
+ return "zypper"
359
+ if ((await ctx.exec(`sh -lc 'command -v apk >/dev/null 2>&1'`)).code === 0)
360
+ return "apk"
361
+ if ((await ctx.exec(`sh -lc 'command -v pacman >/dev/null 2>&1'`)).code === 0)
362
+ return "pacman"
363
+ if ((await ctx.exec(`sh -lc 'command -v brew >/dev/null 2>&1'`)).code === 0)
364
+ return "brew"
365
+ if ((await ctx.exec(`sh -lc 'command -v port >/dev/null 2>&1'`)).code === 0)
366
+ return "port"
367
+ return "unknown"
368
+ }
369
+
370
+ async function queryPackageVersion(
371
+ ctx: Katmer.TaskContext<any>,
372
+ family: OsFamily | "unknown",
373
+ pm: PackageManager,
374
+ name: string
375
+ ): Promise<string | null> {
376
+ switch (pm) {
377
+ case "apt": {
378
+ // dpkg-query returns 1 when not installed
379
+ const r = await ctx.exec(
380
+ `dpkg-query -W -f='${"${Version}"}' ${shq(name)}`
381
+ )
382
+ return r.code === 0 ? (r.stdout || "").trim() : null
383
+ }
384
+ case "dnf": {
385
+ const r = await ctx.exec(
386
+ `rpm -q --qf '%{EPOCH}:%{VERSION}-%{RELEASE}' ${shq(name)}`
387
+ )
388
+ return r.code === 0 ? (r.stdout || "").trim() : null
389
+ }
390
+ case "yum": {
391
+ const r = await ctx.exec(
392
+ `rpm -q --qf '%{EPOCH}:%{VERSION}-%{RELEASE}' ${shq(name)}`
393
+ )
394
+ return r.code === 0 ? (r.stdout || "").trim() : null
395
+ }
396
+ case "zypper": {
397
+ const r = await ctx.exec(
398
+ `rpm -q --qf '%{EPOCH}:%{VERSION}-%{RELEASE}' ${shq(name)}`
399
+ )
400
+ return r.code === 0 ? (r.stdout || "").trim() : null
401
+ }
402
+ case "apk": {
403
+ const r = await ctx.exec(
404
+ `apk info -e ${shq(name)} >/dev/null 2>&1 && apk info -v ${shq(name)}`
405
+ )
406
+ if (r.code !== 0) return null
407
+ // output like: "cron-4.2-r2"
408
+ const line = (r.stdout || "").split(/\r?\n/).find(Boolean) || ""
409
+ return line.replace(/^.*?-/, "") || null
410
+ }
411
+ case "pacman": {
412
+ const r = await ctx.exec(
413
+ `pacman -Qi ${shq(name)} 2>/dev/null | sed -n 's/^Version\\s*:\\s*//p'`
414
+ )
415
+ return r.code === 0 ? (r.stdout || "").trim() : null
416
+ }
417
+ case "brew": {
418
+ const r = await ctx.exec(
419
+ `brew list --versions ${shq(name)} 2>/dev/null | awk '{print $2}'`
420
+ )
421
+ return r.code === 0 ? (r.stdout || "").trim() : null
422
+ }
423
+ case "port": {
424
+ const r = await ctx.exec(
425
+ `port -q installed ${shq(name)} | awk '{print $2}'`
426
+ )
427
+ return r.code === 0 ? (r.stdout || "").trim() : null
428
+ }
429
+ case "winget": {
430
+ const r = await ctx.exec(
431
+ `powershell -NoProfile -NonInteractive -Command "winget list --id ${name} | Out-String"`
432
+ )
433
+ if (r.code !== 0) return null
434
+ const m = (r.stdout || "").match(/\b(\d+(?:\.\d+){1,3}(?:[-\w\.]+)?)\b/)
435
+ return m?.[1] || null
436
+ }
437
+ case "choco": {
438
+ const r = await ctx.exec(`choco list --local-only --limit-output ${name}`)
439
+ if (r.code !== 0) return null
440
+ const m = (r.stdout || "").trim().match(/^[^|]+\|(.+)$/)
441
+ return m?.[1] || null
442
+ }
443
+ default:
444
+ return null
445
+ }
446
+ }
447
+
448
+ function verEquals(a: string, b: string) {
449
+ // try exact first; if not, compare coerced semver
450
+ if (a === b) return true
451
+ const ca = coerceSemver(a),
452
+ cb = coerceSemver(b)
453
+ return !!(ca && cb && ca === cb)
454
+ }
455
+
456
+ // Very light coercion of distro versions to semver-like
457
+ function coerceSemver(v: string): string | null {
458
+ // strip epoch and release: "2:1.17.3-1ubuntu1~22.04.1" -> "1.17.3"
459
+ const m = v.match(/(\d+\.\d+\.\d+|\d+\.\d+|\d+)/)
460
+ return m ? normalizeSemverDigits(m[1]) : null
461
+ }
462
+ function coerceVersion(v: string): string | null {
463
+ const m = v.match(/(\d+(?:\.\d+){0,3}(?:[-\w\.]+)?)/)
464
+ return m?.[1] || null
465
+ }
466
+ function normalizeSemverDigits(v: string) {
467
+ const parts = v.split(".").map((x) => x.trim())
468
+ while (parts.length < 3) parts.push("0")
469
+ return parts.slice(0, 3).join(".")
470
+ }
471
+
472
+ async function satisfiesVersion(
473
+ version: string,
474
+ range: string
475
+ ): Promise<boolean> {
476
+ // Try semver if available
477
+ try {
478
+ const sv = coerceSemver(version)
479
+ const rr = range
480
+ if (sv && semver.valid(sv) && semver.validRange(rr)) {
481
+ return semver.satisfies(sv, rr)
482
+ }
483
+ } catch (_) {
484
+ // ignore; fall back
485
+ }
486
+ // Fallback: support simple comparators like ">=1.2.3", "<2.0.0", "==1.5.0", multiple separated by space
487
+ const sv = coerceSemver(version) || version
488
+ return simpleRangeSatisfies(sv, range)
489
+ }
490
+
491
+ function simpleRangeSatisfies(v: string, range: string): boolean {
492
+ const clauses = range.split(/\s+/).filter(Boolean)
493
+ for (const c of clauses) {
494
+ const m = c.match(/^(<=|>=|<|>|=|==)?\s*([^\s]+)$/)
495
+ if (!m) continue
496
+ const op = m[1] || "=="
497
+ const target = m[2]
498
+ if (!cmp(v, target, op)) return false
499
+ }
500
+ return true
501
+ }
502
+
503
+ // naive comparator using dotted-number compare on first 3 segments
504
+ function cmp(a: string, b: string, op: string): boolean {
505
+ const na = (coerceSemver(a) || a).split(".").map((n) => parseInt(n, 10) || 0)
506
+ const nb = (coerceSemver(b) || b).split(".").map((n) => parseInt(n, 10) || 0)
507
+ while (na.length < 3) na.push(0)
508
+ while (nb.length < 3) nb.push(0)
509
+ const c = na[0] - nb[0] || na[1] - nb[1] || na[2] - nb[2]
510
+ switch (op) {
511
+ case ">":
512
+ return c > 0
513
+ case ">=":
514
+ return c >= 0
515
+ case "<":
516
+ return c < 0
517
+ case "<=":
518
+ return c <= 0
519
+ case "=":
520
+ case "==":
521
+ return c === 0
522
+ default:
523
+ return false
524
+ }
525
+ }
526
+
527
+ function shq(s: string) {
528
+ return s.replace(/'/g, "'\"'\"'")
529
+ }
530
+
531
+ function normalizePackageList(
532
+ pkgs?: Array<PackageConstraint | string>
533
+ ): PackageConstraint[] {
534
+ if (!pkgs?.length) return []
535
+ return pkgs.map(normalizePackageEntry)
536
+ }
537
+ function normalizePackageEntry(
538
+ p: PackageConstraint | string
539
+ ): PackageConstraint {
540
+ if (typeof p !== "string") return p
541
+
542
+ const s = p.trim()
543
+ // Preferred: "name@<range>"
544
+ const at = s.indexOf("@")
545
+ if (at > 0) {
546
+ const name = s.slice(0, at).trim()
547
+ const range = s.slice(at + 1).trim()
548
+ return range ? { name, range } : { name }
549
+ }
550
+
551
+ // Lenient: "name <range...>" e.g., "cron >=3.0"
552
+ const m = s.match(/^([^\s]+)\s+(.+)$/)
553
+ if (m) {
554
+ const [, name, range] = m
555
+ return { name, range: range.trim() }
556
+ }
557
+
558
+ // Just a name
559
+ return { name: s }
560
+ }
@@ -0,0 +1,64 @@
1
+ import type { KatmerCore } from "./katmer"
2
+
3
+ import * as modules from "./modules/index"
4
+
5
+ import type { KatmerProvider } from "./interfaces/provider.interface"
6
+
7
+ import { isClass } from "./utils/object.utils"
8
+ import type { KatmerModule } from "./module"
9
+
10
+ export class KatmerModuleRegistry {
11
+ #moduleMap = new Map<string, (...args: any[]) => KatmerModule>()
12
+ constructor(private core: KatmerCore) {
13
+ this.registerDefaultModules()
14
+ }
15
+
16
+ register(module: any) {
17
+ let moduleWrapper
18
+ let name = module?.name
19
+ if (!name) {
20
+ throw new Error(`modules must have a 'name' property`)
21
+ }
22
+
23
+ if (isClass(module)) {
24
+ moduleWrapper = (params: any, provider: KatmerProvider) =>
25
+ new module(params, provider)
26
+ } else if (typeof module === "function") {
27
+ name = name.replace(/Module$/, "").toLowerCase()
28
+ moduleWrapper = (params: any, provider: KatmerProvider) =>
29
+ module(params, provider)
30
+ } else if (typeof module === "object") {
31
+ moduleWrapper = (params: any) => module
32
+ } else {
33
+ throw new Error(`Module ${name} is not a valid module`)
34
+ }
35
+ this.#moduleMap.set(name, moduleWrapper)
36
+ }
37
+
38
+ has(name: string): boolean {
39
+ return this.#moduleMap.has(name)
40
+ }
41
+
42
+ get(name: string, params: any = {}) {
43
+ const module = this.#moduleMap.get(name)
44
+ if (!module) {
45
+ throw new Error(`Module ${name} not found`)
46
+ }
47
+ const moduleInstance = module(params)
48
+ Object.defineProperty(moduleInstance, "logger", {
49
+ get: () => {
50
+ return this.core.logger.child({ module: name })
51
+ }
52
+ })
53
+
54
+ return moduleInstance
55
+ }
56
+
57
+ registerDefaultModules() {
58
+ for (const [name, module] of Object.entries(modules)) {
59
+ if (name.endsWith("Module")) {
60
+ this.register(module)
61
+ }
62
+ }
63
+ }
64
+ }