@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
@@ -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,9 @@
1
+ export function targetDir(p: string): string {
2
+ const idx = p.lastIndexOf("/")
3
+ return idx >= 0 ? p.slice(0, idx) || "/" : "."
4
+ }
5
+
6
+ export function baseName(p: string): string {
7
+ const idx = p.lastIndexOf("/")
8
+ return idx >= 0 ? p.slice(idx + 1) : p
9
+ }
@@ -0,0 +1,3 @@
1
+ import { Lookup } from "../../lookup"
2
+
3
+ export const lookup = Lookup.execute
@@ -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
+ }