@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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function parseHeaderString(headerStr: string = "") {
|
|
2
|
+
const lines = headerStr.trim().split(/\r?\n/)
|
|
3
|
+
const pairs = lines
|
|
4
|
+
.filter((l) => l.includes(":"))
|
|
5
|
+
.map((l) => {
|
|
6
|
+
const [key, ...rest] = l.split(":")
|
|
7
|
+
return [key.trim(), rest.join(":").trim()]
|
|
8
|
+
})
|
|
9
|
+
return Object.fromEntries(new Headers(pairs))
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function safeJsonParse(input: any) {
|
|
2
|
+
try {
|
|
3
|
+
return JSON.parse(input)
|
|
4
|
+
} catch {
|
|
5
|
+
return input?.toString()
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function wrapInArray<T>(input?: T | T[] | null): T[] {
|
|
10
|
+
return (
|
|
11
|
+
Array.isArray(input) ? input
|
|
12
|
+
: input != null ? [input]
|
|
13
|
+
: []
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function toOctal<T extends number | string | null | undefined>(
|
|
2
|
+
mode: T
|
|
3
|
+
): T extends null | undefined ? undefined : string {
|
|
4
|
+
if (mode === null || mode === undefined) return undefined as any
|
|
5
|
+
if (typeof mode === "number") {
|
|
6
|
+
return ("0" + mode.toString(8)) as any
|
|
7
|
+
}
|
|
8
|
+
return String(mode).replace(/^0?([0-7]{3,4})$/, "0$1") as any
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function cloneInstance(orig: any) {
|
|
2
|
+
return Object.assign(Object.create(Object.getPrototypeOf(orig)), orig)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isClass(obj: any) {
|
|
6
|
+
if (typeof obj !== "function") return false
|
|
7
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, "prototype")
|
|
8
|
+
if (!descriptor) return false
|
|
9
|
+
|
|
10
|
+
return !descriptor.writable
|
|
11
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { OsArch } from "../interfaces/provider.interface"
|
|
2
|
+
|
|
3
|
+
export function normalizeOs(
|
|
4
|
+
s: string
|
|
5
|
+
): "linux" | "darwin" | "freebsd" | "windows" | "unknown" {
|
|
6
|
+
const v = s.toLowerCase()
|
|
7
|
+
if (v.startsWith("linux")) return "linux"
|
|
8
|
+
if (v.startsWith("darwin") || v.startsWith("mac")) return "darwin"
|
|
9
|
+
if (v.startsWith("freebsd")) return "freebsd"
|
|
10
|
+
if (v.startsWith("win") || v.includes("windows")) return "windows"
|
|
11
|
+
if (v === "win32") return "windows"
|
|
12
|
+
return "unknown"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeArch(a: string): OsArch {
|
|
16
|
+
const v = a.toLowerCase().trim()
|
|
17
|
+
|
|
18
|
+
// common aliases
|
|
19
|
+
if (["x64", "x86_64", "amd64"].includes(v)) return "x86_64"
|
|
20
|
+
if (["aarch64", "arm64"].includes(v)) return "arm64"
|
|
21
|
+
if (["armv7l", "armv7", "armhf"].includes(v)) return "armv7"
|
|
22
|
+
if (["armv6l", "armv6"].includes(v)) return "armv6"
|
|
23
|
+
if (["i386", "i686", "ia32"].includes(v)) return "i386"
|
|
24
|
+
if (["ppc64le"].includes(v)) return "ppc64le"
|
|
25
|
+
if (["s390x"].includes(v)) return "s390x"
|
|
26
|
+
if (["riscv64"].includes(v)) return "riscv64"
|
|
27
|
+
if (["loongarch64"].includes(v)) return "loongarch64"
|
|
28
|
+
|
|
29
|
+
// fallback to raw
|
|
30
|
+
return a as any
|
|
31
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import TwigEngine, { type TwigOptions } from "./twig"
|
|
2
|
+
|
|
3
|
+
export async function evalTemplate(
|
|
4
|
+
template: string,
|
|
5
|
+
data: Record<string, any> = {},
|
|
6
|
+
options: Partial<TwigOptions> = {}
|
|
7
|
+
) {
|
|
8
|
+
return TwigEngine(options)
|
|
9
|
+
.twig({ async: true, options, data: template })
|
|
10
|
+
.render(data, undefined, true)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function evalExpr(
|
|
14
|
+
expression: string,
|
|
15
|
+
variables: Record<string, any> = {},
|
|
16
|
+
options?: Partial<TwigOptions>
|
|
17
|
+
) {
|
|
18
|
+
expression = expression.trim()
|
|
19
|
+
if (expression.startsWith("{{") && expression.endsWith("}}")) {
|
|
20
|
+
expression = expression.slice(2, -2)
|
|
21
|
+
}
|
|
22
|
+
const twig = TwigEngine(options)
|
|
23
|
+
const compiled = twig.expression.compile({
|
|
24
|
+
value: expression
|
|
25
|
+
})
|
|
26
|
+
const innerOptions = {
|
|
27
|
+
template: {
|
|
28
|
+
options: {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return await (twig.expression as any)["parseAsync"].call(
|
|
32
|
+
innerOptions,
|
|
33
|
+
compiled.stack,
|
|
34
|
+
variables as any
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function evalObjectVals(
|
|
39
|
+
val: unknown,
|
|
40
|
+
variables: Record<string, any> = {},
|
|
41
|
+
options?: Partial<TwigOptions>
|
|
42
|
+
) {
|
|
43
|
+
return JSON.parse(await evalTemplate(JSON.stringify(val), variables, options))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function evalIterative(
|
|
47
|
+
val: unknown,
|
|
48
|
+
opts: { scope: Record<string, unknown>; deep?: boolean }
|
|
49
|
+
) {
|
|
50
|
+
// TODO: configurable twig delimiters
|
|
51
|
+
if (typeof val === "string") {
|
|
52
|
+
// Only render if it looks like a template. Otherwise keep literal strings intact.
|
|
53
|
+
if (/\{\{.*\}\}/.test(val)) {
|
|
54
|
+
try {
|
|
55
|
+
return await evalExpr(val, opts.scope)
|
|
56
|
+
} catch {
|
|
57
|
+
// If evaluation fails, keep original literal to avoid surprising failures.
|
|
58
|
+
return val
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return val
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (opts.deep === false) return val
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(val)) {
|
|
67
|
+
const out: unknown[] = []
|
|
68
|
+
for (const item of val) {
|
|
69
|
+
out.push(await evalIterative(item, opts))
|
|
70
|
+
}
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (val && typeof val === "object") {
|
|
75
|
+
const input = val as Record<string, unknown>
|
|
76
|
+
const out: Record<string, unknown> = {}
|
|
77
|
+
for (const [k, v] of Object.entries(input)) {
|
|
78
|
+
// merge progress into scope to allow left-to-right references
|
|
79
|
+
|
|
80
|
+
out[k] = await evalIterative(v, {
|
|
81
|
+
...opts,
|
|
82
|
+
scope: { ...opts.scope, ...out }
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
return out
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return val
|
|
89
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { Twig } from "twig"
|
|
2
|
+
|
|
3
|
+
import core from "twig/src/twig.core"
|
|
4
|
+
import compiler from "twig/src/twig.compiler"
|
|
5
|
+
import expression from "twig/src/twig.expression"
|
|
6
|
+
import filters from "twig/src/twig.filters"
|
|
7
|
+
import functionsMod from "twig/src/twig.functions"
|
|
8
|
+
import lib from "twig/src/twig.lib"
|
|
9
|
+
import logic from "twig/src/twig.logic"
|
|
10
|
+
import parserSource from "twig/src/twig.parser.source"
|
|
11
|
+
import parserTwig from "twig/src/twig.parser.twig"
|
|
12
|
+
import pathMod from "twig/src/twig.path"
|
|
13
|
+
import testMod from "twig/src/twig.tests"
|
|
14
|
+
import asyncMod from "twig/src/twig.async"
|
|
15
|
+
import exportsMod from "twig/src/twig.exports"
|
|
16
|
+
|
|
17
|
+
import esToolkit from "es-toolkit/compat"
|
|
18
|
+
|
|
19
|
+
import * as localFunctions from "./render_functions"
|
|
20
|
+
export interface TwigOptions {
|
|
21
|
+
delimiters: Partial<{
|
|
22
|
+
comment: [string, string]
|
|
23
|
+
block: [string, string]
|
|
24
|
+
variable: [string, string]
|
|
25
|
+
interpolation: [string, string]
|
|
26
|
+
}>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const InstanceCache = {} as Record<string, any>
|
|
30
|
+
export default function (
|
|
31
|
+
opts: Partial<TwigOptions> = {}
|
|
32
|
+
): Twig["exports"] & { expression: Twig["expression"] } {
|
|
33
|
+
const k = JSON.stringify(opts)
|
|
34
|
+
if (InstanceCache[k]) {
|
|
35
|
+
return InstanceCache[k]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const Twig = {
|
|
39
|
+
VERSION: "1.17.1"
|
|
40
|
+
} as any
|
|
41
|
+
|
|
42
|
+
core(Twig)
|
|
43
|
+
compiler(Twig)
|
|
44
|
+
expression(Twig)
|
|
45
|
+
filters(Twig)
|
|
46
|
+
functionsMod(Twig)
|
|
47
|
+
lib(Twig)
|
|
48
|
+
logic(Twig)
|
|
49
|
+
parserSource(Twig)
|
|
50
|
+
parserTwig(Twig)
|
|
51
|
+
pathMod(Twig)
|
|
52
|
+
testMod(Twig)
|
|
53
|
+
asyncMod(Twig)
|
|
54
|
+
exportsMod(Twig)
|
|
55
|
+
|
|
56
|
+
const delimiters = Object.assign(
|
|
57
|
+
{
|
|
58
|
+
comment: ["{#", "#}"],
|
|
59
|
+
block: ["{%", "%}"],
|
|
60
|
+
variable: ["{{", "}}"],
|
|
61
|
+
interpolation: ["#{", "}"]
|
|
62
|
+
},
|
|
63
|
+
opts.delimiters
|
|
64
|
+
) as Required<TwigOptions["delimiters"]>
|
|
65
|
+
|
|
66
|
+
Twig.token.definitions = [
|
|
67
|
+
{
|
|
68
|
+
type: Twig.token.type.raw,
|
|
69
|
+
open: `${delimiters.block[0]} raw ${delimiters.block[1]}`,
|
|
70
|
+
close: `${delimiters.block[0]} endraw ${delimiters.block[1]}`
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: Twig.token.type.raw,
|
|
74
|
+
open: `${delimiters.block[0]} verbatim ${delimiters.block[1]}`,
|
|
75
|
+
close: `${delimiters.block[0]} endverbatim ${delimiters.block[1]}`
|
|
76
|
+
},
|
|
77
|
+
// *Whitespace type tokens*
|
|
78
|
+
//
|
|
79
|
+
// These typically take the form `{{- expression -}}` or `{{- expression }}` or `{{ expression -}}`.
|
|
80
|
+
{
|
|
81
|
+
type: Twig.token.type.outputWhitespacePre,
|
|
82
|
+
open: `${delimiters.variable[0]}-`,
|
|
83
|
+
close: `${delimiters.variable[1]}`
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: Twig.token.type.outputWhitespacePost,
|
|
87
|
+
open: `${delimiters.variable[0]}`,
|
|
88
|
+
close: `-${delimiters.variable[1]}`
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: Twig.token.type.outputWhitespaceBoth,
|
|
92
|
+
open: `${delimiters.variable[0]}-`,
|
|
93
|
+
close: `-${delimiters.variable[1]}`
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: Twig.token.type.logicWhitespacePre,
|
|
97
|
+
open: `${delimiters.block[0]}-`,
|
|
98
|
+
close: `${delimiters.block[1]}`
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: Twig.token.type.logicWhitespacePost,
|
|
102
|
+
open: `${delimiters.block[0]}`,
|
|
103
|
+
close: `-${delimiters.block[1]}`
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: Twig.token.type.logicWhitespaceBoth,
|
|
107
|
+
open: `${delimiters.block[0]}-`,
|
|
108
|
+
close: `-${delimiters.block[1]}`
|
|
109
|
+
},
|
|
110
|
+
// *Output type tokens*
|
|
111
|
+
// These typically take the form `{{ expression }}`.
|
|
112
|
+
{
|
|
113
|
+
type: Twig.token.type.output,
|
|
114
|
+
open: `${delimiters.variable[0]}`,
|
|
115
|
+
close: `${delimiters.variable[1]}`
|
|
116
|
+
},
|
|
117
|
+
// *Logic type tokens*
|
|
118
|
+
// These typically take a form like `{% if expression %}` or `{% endif %}`
|
|
119
|
+
{
|
|
120
|
+
type: Twig.token.type.logic,
|
|
121
|
+
open: delimiters.block[0],
|
|
122
|
+
close: delimiters.block[1]
|
|
123
|
+
},
|
|
124
|
+
// *Comment type tokens*
|
|
125
|
+
// These take the form `{# anything #}`
|
|
126
|
+
{
|
|
127
|
+
type: Twig.token.type.comment,
|
|
128
|
+
open: delimiters.comment[0],
|
|
129
|
+
close: delimiters.comment[1]
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
Twig.functions["indent"] = Twig.filters["indent"] = function (
|
|
134
|
+
text?: string,
|
|
135
|
+
params: any[] = []
|
|
136
|
+
) {
|
|
137
|
+
const [count = 2] = params
|
|
138
|
+
const spaces = " ".repeat(count)
|
|
139
|
+
|
|
140
|
+
return text
|
|
141
|
+
?.split("\n")
|
|
142
|
+
.map((line) => (line ? spaces + line : line))
|
|
143
|
+
.join("\n")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Twig.filter = function (filter: string, value: any, params = []) {
|
|
147
|
+
if (filter === "replaceAll") {
|
|
148
|
+
return value.replaceAll(...params)
|
|
149
|
+
}
|
|
150
|
+
if (
|
|
151
|
+
filter in esToolkit &&
|
|
152
|
+
typeof (esToolkit as any)[filter] === "function"
|
|
153
|
+
) {
|
|
154
|
+
return (esToolkit as any)[filter](
|
|
155
|
+
value,
|
|
156
|
+
...(typeof params === "object" ? params : [params])
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
if (!Twig.filters[filter]) {
|
|
160
|
+
throw new Twig.Error("Unable to find filter " + filter)
|
|
161
|
+
}
|
|
162
|
+
return Twig.filters[filter].call(this, value, params || [])
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
Twig.functions = new Proxy(
|
|
166
|
+
{ ...Twig.functions },
|
|
167
|
+
{
|
|
168
|
+
get(target, prop) {
|
|
169
|
+
if (
|
|
170
|
+
prop in localFunctions &&
|
|
171
|
+
typeof (localFunctions as any)[prop] === "function"
|
|
172
|
+
) {
|
|
173
|
+
return (localFunctions as any)[prop]
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
prop in esToolkit &&
|
|
177
|
+
typeof (esToolkit as any)[prop] === "function"
|
|
178
|
+
) {
|
|
179
|
+
return (esToolkit as any)[prop]
|
|
180
|
+
}
|
|
181
|
+
if (target[prop]) {
|
|
182
|
+
return target[prop]
|
|
183
|
+
}
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
Twig.exports["expression"] = Twig.expression
|
|
189
|
+
InstanceCache[k] = Twig.exports
|
|
190
|
+
return Twig.exports
|
|
191
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function parseLines(content: string, keepEmpty = false): string[] {
|
|
2
|
+
const lines = content.split(/\r?\n/)
|
|
3
|
+
|
|
4
|
+
if (keepEmpty) return lines
|
|
5
|
+
|
|
6
|
+
return lines.map((l) => l.trim()).filter((l) => l.length > 0)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeLine(line: string): string {
|
|
10
|
+
return line.replace(/\s+/g, " ").trim()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function stringifyLines(lines: string[]): string {
|
|
14
|
+
return lines.join("\n") + "\n"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shell-safe quoting via JSON stringification.
|
|
19
|
+
* @param v String to quote
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export function quote(v: string) {
|
|
23
|
+
return JSON.stringify(v)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const escapeRegex = (str: string) =>
|
|
27
|
+
str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1")
|
|
28
|
+
|
|
29
|
+
export function wildcardMatch(str: string, rule: string) {
|
|
30
|
+
return new RegExp(
|
|
31
|
+
"^" + rule.split("*").map(escapeRegex).join(".*") + "$"
|
|
32
|
+
).test(str)
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events"
|
|
2
|
+
|
|
3
|
+
export class TypedEventEmitter<TEvents extends Record<string, any>> {
|
|
4
|
+
private emitter = new EventEmitter()
|
|
5
|
+
|
|
6
|
+
emit<TEventName extends keyof TEvents & string>(
|
|
7
|
+
eventName: TEventName,
|
|
8
|
+
...eventArg: TEvents[TEventName]
|
|
9
|
+
) {
|
|
10
|
+
this.emitter.emit(eventName, ...(eventArg as []))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
on<TEventName extends keyof TEvents & string>(
|
|
14
|
+
eventName: TEventName,
|
|
15
|
+
handler: (...eventArg: TEvents[TEventName]) => void
|
|
16
|
+
) {
|
|
17
|
+
this.emitter.on(eventName, handler as any)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
off<TEventName extends keyof TEvents & string>(
|
|
21
|
+
eventName: TEventName,
|
|
22
|
+
handler: (...eventArg: TEvents[TEventName]) => void
|
|
23
|
+
) {
|
|
24
|
+
this.emitter.off(eventName, handler as any)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { SSHProvider } from "../providers/ssh/ssh.provider"
|
|
2
|
+
import { baseName, targetDir } from "./path.utils"
|
|
3
|
+
import { toOctal } from "./number.utils"
|
|
4
|
+
import type { Katmer } from "../interfaces/task.interface"
|
|
5
|
+
|
|
6
|
+
export const UnixComms = {
|
|
7
|
+
escapePOSIX(s: string): string {
|
|
8
|
+
return s.replace(/(["$`\\])/g, "\\$1")
|
|
9
|
+
},
|
|
10
|
+
async fileExists(ctx: Katmer.TaskContext<SSHProvider>, p: string) {
|
|
11
|
+
const r = await ctx.execSafe(`test -e ${JSON.stringify(p)}`)
|
|
12
|
+
return r.code === 0
|
|
13
|
+
},
|
|
14
|
+
/**
|
|
15
|
+
* Checks for the existence of one or more commands on the remote system in a single operation.
|
|
16
|
+
* @param {any} ctx - Katmer task context.
|
|
17
|
+
* @param {string[]} commands - An array of command names to check (e.g., ["git", "node", "npm"]).
|
|
18
|
+
* @returns {Promise<string[]>} A promise that resolves to an array of the command names that were NOT found.
|
|
19
|
+
* An empty array means all commands were found.
|
|
20
|
+
*/
|
|
21
|
+
async findMissingCommands(
|
|
22
|
+
ctx: Katmer.TaskContext<any>,
|
|
23
|
+
commands: string[]
|
|
24
|
+
): Promise<string[]> {
|
|
25
|
+
if (!commands || commands.length === 0) {
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const commandList = commands.join(" ")
|
|
30
|
+
const checkScript = `for cmd in ${commandList}; do command -v "$cmd" >/dev/null 2>&1 || echo "$cmd"; done`
|
|
31
|
+
|
|
32
|
+
const { stdout } = await ctx.exec(checkScript)
|
|
33
|
+
|
|
34
|
+
return stdout
|
|
35
|
+
.split("\n")
|
|
36
|
+
.map((s: string) => s.trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async mkdirp(ctx: Katmer.TaskContext<any>, dir: string): Promise<void> {
|
|
41
|
+
await ctx.execSafe(`bash -lc 'mkdir -p "${dir}"'`)
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async pathIsFile(ctx: Katmer.TaskContext<any>, p: string): Promise<boolean> {
|
|
45
|
+
const { code } = await ctx.execSafe(`bash -lc '[ -f "${p}" ]'`)
|
|
46
|
+
return code === 0
|
|
47
|
+
},
|
|
48
|
+
async pathIsSymlink(
|
|
49
|
+
ctx: Katmer.TaskContext<any>,
|
|
50
|
+
p: string
|
|
51
|
+
): Promise<boolean> {
|
|
52
|
+
const { code } = await ctx.execSafe(`bash -lc '[ -L "${p}" ]'`)
|
|
53
|
+
return code === 0
|
|
54
|
+
},
|
|
55
|
+
async readFileUtf8(ctx: Katmer.TaskContext<any>, p: string): Promise<string> {
|
|
56
|
+
const { code, stdout } = await ctx.execSafe(
|
|
57
|
+
`bash -lc '[[ -f "${p}" ]] && cat "${p}" || true'`
|
|
58
|
+
)
|
|
59
|
+
if (code !== 0) return ""
|
|
60
|
+
return stdout
|
|
61
|
+
},
|
|
62
|
+
async readlink(
|
|
63
|
+
ctx: Katmer.TaskContext<any>,
|
|
64
|
+
p: string
|
|
65
|
+
): Promise<string | null> {
|
|
66
|
+
const { code, stdout } = await ctx.execSafe(
|
|
67
|
+
`bash -lc 'readlink "${p}" || true'`
|
|
68
|
+
)
|
|
69
|
+
return code === 0 ? stdout.trim() : null
|
|
70
|
+
},
|
|
71
|
+
async removePath(ctx: Katmer.TaskContext<any>, p: string): Promise<void> {
|
|
72
|
+
await ctx.execSafe(`bash -lc 'rm -f "${p}" || true'`)
|
|
73
|
+
},
|
|
74
|
+
async writeFileAtomic(
|
|
75
|
+
ctx: Katmer.TaskContext<any>,
|
|
76
|
+
target: string,
|
|
77
|
+
content: string,
|
|
78
|
+
mode?: number
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
const dir = targetDir(target)
|
|
81
|
+
const base = baseName(target)
|
|
82
|
+
const tmp = `${dir}/.${base}.${Date.now()}.${Math.random().toString(36).slice(2)}`
|
|
83
|
+
await ctx.exec(
|
|
84
|
+
`bash -lc 'set -euo pipefail; dir="${UnixComms.escapePOSIX(dir)}"; tmp="${UnixComms.escapePOSIX(tmp)}"; target="${UnixComms.escapePOSIX(
|
|
85
|
+
target
|
|
86
|
+
)}"; mkdir -p "$dir"; : > "$tmp"; cat > "$tmp" << "EOF"\n${content}EOF\n${
|
|
87
|
+
mode != null ? `chmod ${toOctal(mode)} "$tmp"` : ""
|
|
88
|
+
}\nmv -f "$tmp" "$target"'`
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { SSHProvider } from "../providers/ssh/ssh.provider"
|
|
2
|
+
import type { Katmer } from "../interfaces/task.interface"
|
|
3
|
+
|
|
4
|
+
export const WindowsComms = {
|
|
5
|
+
/**
|
|
6
|
+
* Basic PowerShell string literal quoting using single quotes.
|
|
7
|
+
* Single quotes inside get doubled per PowerShell rules.
|
|
8
|
+
*/
|
|
9
|
+
psQuote(s: string): string {
|
|
10
|
+
return "'" + String(s).replace(/'/g, "''") + "'"
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async fileExists(ctx: Katmer.TaskContext<SSHProvider>, p: string) {
|
|
14
|
+
const q = this.psQuote(p)
|
|
15
|
+
const cmd = `powershell -NoProfile -NonInteractive -Command "if (Test-Path -LiteralPath ${q}) { exit 0 } else { exit 1 }"`
|
|
16
|
+
const r = await ctx.exec(cmd)
|
|
17
|
+
return r.code === 0
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async ensureDir(ctx: Katmer.TaskContext<SSHProvider>, dir: string) {
|
|
21
|
+
const q = this.psQuote(dir)
|
|
22
|
+
const cmd = `powershell -NoProfile -NonInteractive -Command "New-Item -ItemType Directory -Force -Path ${q} | Out-Null"`
|
|
23
|
+
const r = await ctx.exec(cmd)
|
|
24
|
+
if (r.code !== 0) throw new Error(r.stderr || "ensureDir failed")
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async sha256File(ctx: Katmer.TaskContext<SSHProvider>, p: string) {
|
|
28
|
+
const q = this.psQuote(p)
|
|
29
|
+
const cmd = `powershell -NoProfile -NonInteractive -Command "if (Test-Path -LiteralPath ${q}) { (Get-FileHash -Algorithm SHA256 -LiteralPath ${q}).Hash }"`
|
|
30
|
+
const r = await ctx.exec(cmd)
|
|
31
|
+
if (r.code === 0) {
|
|
32
|
+
const h = (r.stdout || "").trim()
|
|
33
|
+
return h ? h.toLowerCase() : null
|
|
34
|
+
}
|
|
35
|
+
return null
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stage bytes from base64 into a file atomically.
|
|
40
|
+
*/
|
|
41
|
+
async writeBase64ToFile(
|
|
42
|
+
ctx: Katmer.TaskContext<SSHProvider>,
|
|
43
|
+
dest: string,
|
|
44
|
+
base64: string
|
|
45
|
+
) {
|
|
46
|
+
const qDest = this.psQuote(dest)
|
|
47
|
+
const qB64 = this.psQuote(base64)
|
|
48
|
+
const script = [
|
|
49
|
+
"param($p,$b)",
|
|
50
|
+
"$dir = Split-Path -LiteralPath $p -Parent",
|
|
51
|
+
"$tmp = [System.IO.Path]::Combine($dir, [System.IO.Path]::GetRandomFileName())",
|
|
52
|
+
"[IO.File]::WriteAllBytes($tmp, [Convert]::FromBase64String($b))",
|
|
53
|
+
"Move-Item -Force -LiteralPath $tmp -Destination $p"
|
|
54
|
+
].join("; ")
|
|
55
|
+
const cmd = `powershell -NoProfile -NonInteractive -Command "${script}" -p ${qDest} -b ${qB64}`
|
|
56
|
+
const r = await ctx.exec(cmd)
|
|
57
|
+
if (r.code !== 0) throw new Error(r.stderr || "writeBase64ToFile failed")
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async copyFile(
|
|
61
|
+
ctx: Katmer.TaskContext<SSHProvider>,
|
|
62
|
+
src: string,
|
|
63
|
+
dest: string
|
|
64
|
+
) {
|
|
65
|
+
const qSrc = this.psQuote(src)
|
|
66
|
+
const qDest = this.psQuote(dest)
|
|
67
|
+
const cmd = `powershell -NoProfile -NonInteractive -Command "Copy-Item -Force -LiteralPath ${qSrc} -Destination ${qDest}"`
|
|
68
|
+
const r = await ctx.exec(cmd)
|
|
69
|
+
if (r.code !== 0) throw new Error(r.stderr || "copyFile failed")
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async moveFile(
|
|
73
|
+
ctx: Katmer.TaskContext<SSHProvider>,
|
|
74
|
+
src: string,
|
|
75
|
+
dest: string
|
|
76
|
+
) {
|
|
77
|
+
const qSrc = this.psQuote(src)
|
|
78
|
+
const qDest = this.psQuote(dest)
|
|
79
|
+
const cmd = `powershell -NoProfile -NonInteractive -Command "Move-Item -Force -LiteralPath ${qSrc} -Destination ${qDest}"`
|
|
80
|
+
const r = await ctx.exec(cmd)
|
|
81
|
+
if (r.code !== 0) throw new Error(r.stderr || "moveFile failed")
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async backupIfExists(ctx: Katmer.TaskContext<SSHProvider>, dest: string) {
|
|
85
|
+
const exists = await this.fileExists(ctx, dest)
|
|
86
|
+
if (!exists) return null
|
|
87
|
+
const ts = Date.now()
|
|
88
|
+
const bak = `${dest}.bak.${ts}`
|
|
89
|
+
await this.copyFile(ctx, dest, bak)
|
|
90
|
+
return bak
|
|
91
|
+
}
|
|
92
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@katmer/core",
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"description": "Manage your infrastructure with ease.",
|
|
5
|
+
"version": "0.0.3",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"types": "index.ts",
|
|
9
|
+
"repository": {
|
|
10
|
+
"url": "https://github.com/katmer-io/katmer",
|
|
11
|
+
"directory": "packages/core"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"katmer": "cli/katmer.js"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
"bun": "./index.ts"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"cli",
|
|
21
|
+
"lib/interfaces",
|
|
22
|
+
"lib",
|
|
23
|
+
"index.ts"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "bun ./scripts/build.ts",
|
|
27
|
+
"dev:server": "bun run --watch index.ts",
|
|
28
|
+
"test": "vitest"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@fastify/deepmerge": "^3.1.0",
|
|
32
|
+
"adm-zip": "^0.5.16",
|
|
33
|
+
"ajv": "^8.17.1",
|
|
34
|
+
"ajv-errors": "^3.0.0",
|
|
35
|
+
"commander": "^14.0.2",
|
|
36
|
+
"convict": "^6.2.4",
|
|
37
|
+
"es-toolkit": "^1.43.0",
|
|
38
|
+
"fast-equals": "^6.0.0",
|
|
39
|
+
"fs-extra": "^11.3.3",
|
|
40
|
+
"isomorphic-git": "^1.36.1",
|
|
41
|
+
"json5": "^2.2.3",
|
|
42
|
+
"jsrsasign": "^11.1.0",
|
|
43
|
+
"lodash": "^4.17.21",
|
|
44
|
+
"node-ssh": "^13.2.1",
|
|
45
|
+
"pino": "^10.1.1",
|
|
46
|
+
"pino-pretty": "^13.1.3",
|
|
47
|
+
"semver": "^7.7.3",
|
|
48
|
+
"ssh2": "^1.17.0",
|
|
49
|
+
"stable-hash": "^0.0.6",
|
|
50
|
+
"twig": "^1.17.1",
|
|
51
|
+
"uint8array-extras": "^1.5.0",
|
|
52
|
+
"validator": "^13.15.26"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/bun": "^1.3.5",
|
|
56
|
+
"@types/convict": "^6.1.6",
|
|
57
|
+
"@types/semver": "^7.7.1",
|
|
58
|
+
"@types/ssh2": "^1.15.5",
|
|
59
|
+
"@types/twig": "^1.12.17"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"bun": ">=1.3.0"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
}
|
|
67
|
+
}
|