@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,336 @@
|
|
|
1
|
+
// local-provider.ts
|
|
2
|
+
import {
|
|
3
|
+
KatmerProvider,
|
|
4
|
+
type OsInfo,
|
|
5
|
+
type ProviderOptions,
|
|
6
|
+
type SupportedShell
|
|
7
|
+
} from "../interfaces/provider.interface"
|
|
8
|
+
import * as child_process from "node:child_process"
|
|
9
|
+
import type { ChildProcess } from "node:child_process"
|
|
10
|
+
import { makeLineEmitter } from "./ssh/ssh.utils"
|
|
11
|
+
import { merge } from "es-toolkit/compat"
|
|
12
|
+
import { ProviderResponse } from "./provider_response"
|
|
13
|
+
|
|
14
|
+
// NEW: extra imports for OS detection
|
|
15
|
+
import * as os from "node:os"
|
|
16
|
+
import * as fs from "node:fs/promises"
|
|
17
|
+
import { normalizeArch, normalizeOs } from "../utils/os.utils"
|
|
18
|
+
|
|
19
|
+
export interface LocalProviderOptions extends ProviderOptions {}
|
|
20
|
+
|
|
21
|
+
export interface CommandOptions {
|
|
22
|
+
cwd?: string
|
|
23
|
+
shell?: SupportedShell
|
|
24
|
+
timeout?: number
|
|
25
|
+
env?: Record<string, string>
|
|
26
|
+
encoding?: BufferEncoding
|
|
27
|
+
onChannel?: (clientChannel: ChildProcess) => void
|
|
28
|
+
onStdout?: (line: string) => void
|
|
29
|
+
onStderr?: (line: string) => void
|
|
30
|
+
|
|
31
|
+
rewriteCommand?: (preparedCommand: string) => string
|
|
32
|
+
promptMarker?: string
|
|
33
|
+
interactivePassword?: string
|
|
34
|
+
hidePromptLine?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Conservative prompt patterns (generic)
|
|
38
|
+
const GENERIC_PROMPT_RX = /\b(password|passphrase)\s*(for [^\s:]+)?\s*:\s*$/i
|
|
39
|
+
const FAILURE_RX =
|
|
40
|
+
/\b(sorry,\s*try\s*again|incorrect\s*password|permission\s*denied)\b/i
|
|
41
|
+
|
|
42
|
+
export class LocalProvider extends KatmerProvider<LocalProviderOptions> {
|
|
43
|
+
static name = "local"
|
|
44
|
+
|
|
45
|
+
async check(): Promise<void> {}
|
|
46
|
+
async initialize(): Promise<void> {}
|
|
47
|
+
async connect(): Promise<void> {}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Detect controller OS/arch quickly using Node APIs with light fallbacks.
|
|
51
|
+
* Populates `this.os` and returns the same object.
|
|
52
|
+
*/
|
|
53
|
+
async getOsInfo(): Promise<OsInfo> {
|
|
54
|
+
// Base facts from Node
|
|
55
|
+
const kernelRaw = os.type() || "" // 'Linux' | 'Darwin' | 'Windows_NT'
|
|
56
|
+
const family = normalizeOs(kernelRaw)
|
|
57
|
+
// Prefer PROCESSOR_ARCHITECTURE on Windows; otherwise process.arch
|
|
58
|
+
const archRaw =
|
|
59
|
+
process.platform === "win32" ?
|
|
60
|
+
process.env.PROCESSOR_ARCHITECTURE || process.arch
|
|
61
|
+
: process.arch
|
|
62
|
+
const info: OsInfo = {
|
|
63
|
+
family,
|
|
64
|
+
arch: normalizeArch(String(archRaw || "")),
|
|
65
|
+
kernel: kernelRaw,
|
|
66
|
+
source: "posix"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
if (family === "linux") {
|
|
71
|
+
const file =
|
|
72
|
+
(await fileIfExists("/etc/os-release")) ??
|
|
73
|
+
(await fileIfExists("/usr/lib/os-release"))
|
|
74
|
+
if (file) {
|
|
75
|
+
const env = parseOsRelease(await fs.readFile(file, "utf8"))
|
|
76
|
+
info.distroId = env.ID ?? info.distroId
|
|
77
|
+
info.versionId = env.VERSION_ID ?? info.versionId
|
|
78
|
+
info.prettyName = env.PRETTY_NAME ?? info.prettyName
|
|
79
|
+
}
|
|
80
|
+
} else if (family === "darwin") {
|
|
81
|
+
// sw_vers is standard on macOS
|
|
82
|
+
const [productName, productVersion] = await Promise.all([
|
|
83
|
+
tryExec("sw_vers", ["-productName"]),
|
|
84
|
+
tryExec("sw_vers", ["-productVersion"])
|
|
85
|
+
])
|
|
86
|
+
if (productName || productVersion) {
|
|
87
|
+
info.distroId = "macos"
|
|
88
|
+
info.versionId = productVersion?.trim() || undefined
|
|
89
|
+
info.prettyName = [productName?.trim(), productVersion?.trim()]
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join(" ")
|
|
92
|
+
}
|
|
93
|
+
} else if (family === "windows") {
|
|
94
|
+
// Best-effort pretty/version via PowerShell (if available)
|
|
95
|
+
const ps = await tryExec("powershell", [
|
|
96
|
+
"-NoProfile",
|
|
97
|
+
"-NonInteractive",
|
|
98
|
+
"-ExecutionPolicy",
|
|
99
|
+
"Bypass",
|
|
100
|
+
"-Command",
|
|
101
|
+
[
|
|
102
|
+
"$cap=(Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue).Caption;",
|
|
103
|
+
"if(-not $cap){$cap=(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion' -ErrorAction SilentlyContinue).ProductName};",
|
|
104
|
+
"$ver=(Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue).Version;",
|
|
105
|
+
"[Console]::OutputEncoding=[Text.UTF8Encoding]::UTF8;",
|
|
106
|
+
"Write-Output ($cap);",
|
|
107
|
+
"Write-Output ($ver)"
|
|
108
|
+
].join(" ")
|
|
109
|
+
])
|
|
110
|
+
if (ps) {
|
|
111
|
+
const lines = ps.split(/\r?\n/)
|
|
112
|
+
info.distroId = "windows"
|
|
113
|
+
info.prettyName = (lines[0] || "").trim() || info.prettyName
|
|
114
|
+
info.versionId = (lines[1] || "").trim() || info.versionId
|
|
115
|
+
info.source = "powershell"
|
|
116
|
+
} else {
|
|
117
|
+
info.distroId = "windows"
|
|
118
|
+
info.source = "powershell"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// keep minimal info; we already have family/arch/kernel
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.os = info
|
|
126
|
+
return info
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
executor(options: CommandOptions = {}) {
|
|
130
|
+
return async (
|
|
131
|
+
command: string,
|
|
132
|
+
execOpts: CommandOptions = {}
|
|
133
|
+
): Promise<ProviderResponse> => {
|
|
134
|
+
const opts = merge(
|
|
135
|
+
{
|
|
136
|
+
encoding: "utf-8",
|
|
137
|
+
interactivePassword: "",
|
|
138
|
+
hidePromptLine: true
|
|
139
|
+
},
|
|
140
|
+
this.options,
|
|
141
|
+
options,
|
|
142
|
+
execOpts
|
|
143
|
+
) as CommandOptions
|
|
144
|
+
|
|
145
|
+
const prepareCommand = (command: string): string => {
|
|
146
|
+
const withCwd =
|
|
147
|
+
opts.cwd ? `cd ${JSON.stringify(opts.cwd)} && ${command}` : command
|
|
148
|
+
return `${withCwd.replace(/'/g, "'\\''")}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const prepared = prepareCommand(command)
|
|
152
|
+
const finalCommand = opts.rewriteCommand?.(prepared) ?? prepared
|
|
153
|
+
|
|
154
|
+
return new Promise<ProviderResponse>(async (resolve, reject) => {
|
|
155
|
+
let timeoutId: NodeJS.Timeout | null = null
|
|
156
|
+
|
|
157
|
+
this.logger.trace(`[exec] %s`, finalCommand)
|
|
158
|
+
|
|
159
|
+
// FIX: if opts.shell === "none", do NOT pass the literal string to Node.
|
|
160
|
+
// Use platform default shell (true) instead.
|
|
161
|
+
const shellOpt =
|
|
162
|
+
opts.shell && opts.shell !== "none" ? (opts.shell as any) : true
|
|
163
|
+
|
|
164
|
+
const channel = child_process.spawn(finalCommand, {
|
|
165
|
+
shell: shellOpt,
|
|
166
|
+
stdio: "pipe",
|
|
167
|
+
env: opts.env,
|
|
168
|
+
cwd: opts.cwd
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
if (opts.timeout && opts.timeout > 0) {
|
|
172
|
+
timeoutId = setTimeout(() => {
|
|
173
|
+
try {
|
|
174
|
+
channel.kill()
|
|
175
|
+
} catch {}
|
|
176
|
+
reject(
|
|
177
|
+
new ProviderResponse({
|
|
178
|
+
command: finalCommand,
|
|
179
|
+
stderr: `Command timed out after ${opts.timeout}ms`,
|
|
180
|
+
code: 1
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
}, opts.timeout)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
opts.onChannel?.(channel)
|
|
187
|
+
|
|
188
|
+
const stdoutLines: string[] = []
|
|
189
|
+
const stderrLines: string[] = []
|
|
190
|
+
const emitStdout = makeLineEmitter((line) => {
|
|
191
|
+
if (
|
|
192
|
+
!(
|
|
193
|
+
opts.hidePromptLine &&
|
|
194
|
+
opts.promptMarker &&
|
|
195
|
+
line.includes(opts.promptMarker)
|
|
196
|
+
)
|
|
197
|
+
) {
|
|
198
|
+
opts.onStdout?.(line)
|
|
199
|
+
stdoutLines.push(line)
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
const emitStderr = makeLineEmitter((line) => {
|
|
203
|
+
if (
|
|
204
|
+
!(
|
|
205
|
+
opts.hidePromptLine &&
|
|
206
|
+
opts.promptMarker &&
|
|
207
|
+
line.includes(opts.promptMarker)
|
|
208
|
+
)
|
|
209
|
+
) {
|
|
210
|
+
opts.onStderr?.(line)
|
|
211
|
+
stderrLines.push(line)
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
let pwSent = false
|
|
216
|
+
let genericPwSent = false
|
|
217
|
+
let authDenied = false
|
|
218
|
+
let buffer = ""
|
|
219
|
+
|
|
220
|
+
const handlePrompts = (text: string) => {
|
|
221
|
+
buffer += text
|
|
222
|
+
|
|
223
|
+
if (
|
|
224
|
+
!pwSent &&
|
|
225
|
+
opts.promptMarker &&
|
|
226
|
+
buffer.includes(opts.promptMarker)
|
|
227
|
+
) {
|
|
228
|
+
channel.stdin?.write(opts.interactivePassword + "\n")
|
|
229
|
+
pwSent = true
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!genericPwSent && GENERIC_PROMPT_RX.test(buffer)) {
|
|
233
|
+
channel.stdin?.write(opts.interactivePassword + "\n")
|
|
234
|
+
genericPwSent = true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (FAILURE_RX.test(buffer)) authDenied = true
|
|
238
|
+
if (buffer.length > 4096) buffer = buffer.slice(-2048)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
channel.stdout?.on("data", (chunk: Buffer) => {
|
|
242
|
+
const text = chunk.toString(opts.encoding)
|
|
243
|
+
handlePrompts(text)
|
|
244
|
+
emitStdout(text)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
channel.stderr?.on("data", (chunk: Buffer) => {
|
|
248
|
+
const text = chunk.toString(opts.encoding)
|
|
249
|
+
handlePrompts(text)
|
|
250
|
+
emitStderr(text)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
let code: number | null = null
|
|
254
|
+
channel.on("exit", (c: any) => {
|
|
255
|
+
code = c ?? null
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
channel.on("close", () => {
|
|
259
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
260
|
+
const result = new ProviderResponse({
|
|
261
|
+
command: finalCommand,
|
|
262
|
+
stdout: stdoutLines.join("\n"),
|
|
263
|
+
stderr: stderrLines.join("\n"),
|
|
264
|
+
code: code ?? (authDenied ? 1 : -1)
|
|
265
|
+
})
|
|
266
|
+
if (result.code === 0) {
|
|
267
|
+
resolve(result)
|
|
268
|
+
} else {
|
|
269
|
+
reject(result as unknown as Error)
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
channel.on("error", (e: any) => {
|
|
274
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
275
|
+
reject(
|
|
276
|
+
new ProviderResponse({
|
|
277
|
+
command: finalCommand,
|
|
278
|
+
stdout: stdoutLines.join("\n"),
|
|
279
|
+
stderr: String(e?.message ?? e),
|
|
280
|
+
code: 1
|
|
281
|
+
})
|
|
282
|
+
)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async destroy(): Promise<void> {}
|
|
289
|
+
async cleanup(): Promise<void> {}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* ───────────────────────── helpers ───────────────────────── */
|
|
293
|
+
|
|
294
|
+
// read first existing file path (returns path or null)
|
|
295
|
+
async function fileIfExists(p: string): Promise<string | null> {
|
|
296
|
+
try {
|
|
297
|
+
await fs.access(p)
|
|
298
|
+
return p
|
|
299
|
+
} catch {
|
|
300
|
+
return null
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function parseOsRelease(src: string): Record<string, string> {
|
|
305
|
+
const out: Record<string, string> = {}
|
|
306
|
+
for (const line of src.split(/\r?\n/)) {
|
|
307
|
+
const m = line.match(/^([A-Z0-9_]+)=(.*)$/)
|
|
308
|
+
if (!m) continue
|
|
309
|
+
const k = m[1]
|
|
310
|
+
let v = m[2]
|
|
311
|
+
// strip quotes if present
|
|
312
|
+
if (
|
|
313
|
+
(v.startsWith('"') && v.endsWith('"')) ||
|
|
314
|
+
(v.startsWith("'") && v.endsWith("'"))
|
|
315
|
+
) {
|
|
316
|
+
v = v.slice(1, -1)
|
|
317
|
+
}
|
|
318
|
+
out[k] = v
|
|
319
|
+
}
|
|
320
|
+
return out
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function tryExec(cmd: string, args: string[]): Promise<string | null> {
|
|
324
|
+
return await new Promise((resolve) => {
|
|
325
|
+
const child = child_process.execFile(
|
|
326
|
+
cmd,
|
|
327
|
+
args,
|
|
328
|
+
{ windowsHide: true },
|
|
329
|
+
(err, stdout) => {
|
|
330
|
+
if (err) return resolve(null)
|
|
331
|
+
resolve(stdout?.toString() ?? "")
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
child.on("error", () => resolve(null))
|
|
335
|
+
})
|
|
336
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ExecutionResult {
|
|
2
|
+
command: string
|
|
3
|
+
code: number
|
|
4
|
+
stdout?: string
|
|
5
|
+
stderr?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ProviderResponse implements ExecutionResult {
|
|
9
|
+
constructor(opts: ExecutionResult) {
|
|
10
|
+
Object.assign(this, opts)
|
|
11
|
+
}
|
|
12
|
+
toString() {
|
|
13
|
+
return (this.stderr || this.stdout || "")?.trim()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
code!: number
|
|
17
|
+
command!: string
|
|
18
|
+
stderr!: string
|
|
19
|
+
stdout!: string
|
|
20
|
+
}
|