@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.
- package/README.md +1 -0
- package/cli/katmer.js +28 -0
- package/cli/run.ts +16 -0
- package/index.ts +5 -0
- package/lib/config.ts +82 -0
- package/lib/interfaces/config.interface.ts +113 -0
- package/lib/interfaces/executor.interface.ts +13 -0
- package/lib/interfaces/module.interface.ts +170 -0
- package/lib/interfaces/provider.interface.ts +214 -0
- package/lib/interfaces/task.interface.ts +100 -0
- package/lib/katmer.ts +126 -0
- package/lib/lookup/env.lookup.ts +13 -0
- package/lib/lookup/file.lookup.ts +23 -0
- package/lib/lookup/index.ts +46 -0
- package/lib/lookup/url.lookup.ts +21 -0
- package/lib/lookup/var.lookup.ts +13 -0
- package/lib/module.ts +560 -0
- package/lib/module_registry.ts +64 -0
- package/lib/modules/apt-repository/apt-repository.module.ts +435 -0
- package/lib/modules/apt-repository/apt-sources-list.ts +363 -0
- package/lib/modules/apt.module.ts +546 -0
- package/lib/modules/archive.module.ts +280 -0
- package/lib/modules/become.module.ts +119 -0
- package/lib/modules/copy.module.ts +807 -0
- package/lib/modules/cron.module.ts +541 -0
- package/lib/modules/debug.module.ts +231 -0
- package/lib/modules/gather_facts.module.ts +605 -0
- package/lib/modules/git.module.ts +243 -0
- package/lib/modules/hostname.module.ts +213 -0
- package/lib/modules/http/http.curl.module.ts +342 -0
- package/lib/modules/http/http.local.module.ts +253 -0
- package/lib/modules/http/http.module.ts +298 -0
- package/lib/modules/index.ts +14 -0
- package/lib/modules/package.module.ts +283 -0
- package/lib/modules/script.module.ts +121 -0
- package/lib/modules/set_fact.module.ts +171 -0
- package/lib/modules/systemd_service.module.ts +373 -0
- package/lib/modules/template.module.ts +478 -0
- package/lib/providers/local.provider.ts +336 -0
- package/lib/providers/provider_response.ts +20 -0
- package/lib/providers/ssh/ssh.provider.ts +420 -0
- package/lib/providers/ssh/ssh.utils.ts +31 -0
- package/lib/schemas/katmer_config.schema.json +358 -0
- package/lib/target_resolver.ts +298 -0
- package/lib/task/controls/environment.control.ts +42 -0
- package/lib/task/controls/index.ts +13 -0
- package/lib/task/controls/loop.control.ts +89 -0
- package/lib/task/controls/register.control.ts +23 -0
- package/lib/task/controls/until.control.ts +64 -0
- package/lib/task/controls/when.control.ts +25 -0
- package/lib/task/task.ts +225 -0
- package/lib/utils/ajv.utils.ts +24 -0
- package/lib/utils/cls.ts +4 -0
- package/lib/utils/datetime.utils.ts +15 -0
- package/lib/utils/errors.ts +25 -0
- package/lib/utils/execute-shell.ts +116 -0
- package/lib/utils/file.utils.ts +68 -0
- package/lib/utils/http.utils.ts +10 -0
- package/lib/utils/json.utils.ts +15 -0
- package/lib/utils/number.utils.ts +9 -0
- package/lib/utils/object.utils.ts +11 -0
- package/lib/utils/os.utils.ts +31 -0
- package/lib/utils/path.utils.ts +9 -0
- package/lib/utils/renderer/render_functions.ts +3 -0
- package/lib/utils/renderer/renderer.ts +89 -0
- package/lib/utils/renderer/twig.ts +191 -0
- package/lib/utils/string.utils.ts +33 -0
- package/lib/utils/typed-event-emitter.ts +26 -0
- package/lib/utils/unix.utils.ts +91 -0
- package/lib/utils/windows.utils.ts +92 -0
- 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
|
+
}
|