@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,420 @@
|
|
|
1
|
+
// ssh-provider.ts
|
|
2
|
+
import { NodeSSH, type Config } from "node-ssh"
|
|
3
|
+
import type { ClientChannel, ExecOptions } from "ssh2"
|
|
4
|
+
import {
|
|
5
|
+
KatmerProvider,
|
|
6
|
+
type OsInfo,
|
|
7
|
+
type ProviderOptions,
|
|
8
|
+
type SupportedShell
|
|
9
|
+
} from "../../interfaces/provider.interface"
|
|
10
|
+
import { makeLineEmitter } from "./ssh.utils"
|
|
11
|
+
import { pick } from "es-toolkit"
|
|
12
|
+
import { merge } from "es-toolkit/compat"
|
|
13
|
+
|
|
14
|
+
export interface SSHProviderOptions extends ProviderOptions {
|
|
15
|
+
hostname?: string
|
|
16
|
+
port?: number
|
|
17
|
+
username?: string
|
|
18
|
+
password?: string
|
|
19
|
+
private_key?: string
|
|
20
|
+
private_key_password?: string
|
|
21
|
+
shell?: SupportedShell
|
|
22
|
+
timeout?: number // ms
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
import { version } from "../../../package.json"
|
|
26
|
+
import { ProviderResponse } from "../provider_response"
|
|
27
|
+
import { normalizeArch, normalizeOs } from "../../utils/os.utils"
|
|
28
|
+
|
|
29
|
+
export interface CommandOptions extends ExecOptions {
|
|
30
|
+
cwd?: string
|
|
31
|
+
shell?: SupportedShell
|
|
32
|
+
timeout?: number
|
|
33
|
+
encoding?: BufferEncoding
|
|
34
|
+
onChannel?: (clientChannel: ClientChannel) => void
|
|
35
|
+
onStdout?: (line: string) => void
|
|
36
|
+
onStderr?: (line: string) => void
|
|
37
|
+
rewriteCommand?: (preparedCommand: string) => string
|
|
38
|
+
promptMarker?: string
|
|
39
|
+
interactivePassword?: string
|
|
40
|
+
hidePromptLine?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Detect already-wrapped commands (avoid double wrap)
|
|
44
|
+
const SHELL_WRAPPED_RX =
|
|
45
|
+
/^\s*(?:ba?sh|zsh|dash|ksh|mksh|sh|fish)\s+-l?c\s+['"]/i
|
|
46
|
+
const PS_WRAPPED_RX = /^\s*powershell(?:\.exe)?\b.*?-Command\s+['"]/i
|
|
47
|
+
const CMD_WRAPPED_RX = /^\s*cmd(?:\.exe)?\s+\/(?:d\s+)?\/s\s+\/c\s+["']/i
|
|
48
|
+
|
|
49
|
+
// Conservative prompt patterns (generic)
|
|
50
|
+
const GENERIC_PROMPT_RX = /\b(password|passphrase)\s*(for [^\s:]+)?\s*:\s*$/i
|
|
51
|
+
const FAILURE_RX =
|
|
52
|
+
/\b(sorry,\s*try\s*again|incorrect\s*password|permission\s*denied)\b/i
|
|
53
|
+
|
|
54
|
+
export class SSHProvider extends KatmerProvider<SSHProviderOptions> {
|
|
55
|
+
static name = "ssh"
|
|
56
|
+
client: NodeSSH | null = null
|
|
57
|
+
|
|
58
|
+
async check(): Promise<void> {
|
|
59
|
+
if (!this.options.hostname)
|
|
60
|
+
throw new Error("SSHProvider requires a hostname.")
|
|
61
|
+
if (!this.options.username)
|
|
62
|
+
throw new Error("SSHProvider requires a username.")
|
|
63
|
+
if (!this.options.password && !this.options.private_key) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"SSHProvider requires either a password or a private_key."
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async initialize(): Promise<void> {
|
|
71
|
+
this.client = new NodeSSH()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async connect(): Promise<void> {
|
|
75
|
+
if (!this.client) throw new Error("SSH client is not initialized.")
|
|
76
|
+
const sshConfig: Config = {
|
|
77
|
+
host: this.options.hostname!,
|
|
78
|
+
ident: `katmer_${version}`,
|
|
79
|
+
port: Number(this.options.port ?? 22),
|
|
80
|
+
username: this.options.username!
|
|
81
|
+
}
|
|
82
|
+
if (this.options.password)
|
|
83
|
+
sshConfig.password = String(this.options.password)
|
|
84
|
+
if (this.options.private_key)
|
|
85
|
+
sshConfig.privateKey = this.options.private_key
|
|
86
|
+
if (this.options.private_key_password)
|
|
87
|
+
sshConfig.passphrase = this.options.private_key_password
|
|
88
|
+
|
|
89
|
+
this.logger.debug(
|
|
90
|
+
`Connecting to "${this.options.name || this.options.hostname}"`
|
|
91
|
+
)
|
|
92
|
+
await this.client.connect(sshConfig)
|
|
93
|
+
this.connected = true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Expose a safe exec builder (same as before)
|
|
97
|
+
executor(options: CommandOptions = {}) {
|
|
98
|
+
return async (
|
|
99
|
+
command: string,
|
|
100
|
+
execOpts: CommandOptions = {}
|
|
101
|
+
): Promise<ProviderResponse> => {
|
|
102
|
+
const opts = merge(
|
|
103
|
+
{
|
|
104
|
+
// default shell may be decided dynamically
|
|
105
|
+
shell: (this.options.shell ??
|
|
106
|
+
this.defaultShell ??
|
|
107
|
+
"bash") as SupportedShell,
|
|
108
|
+
encoding: "utf-8",
|
|
109
|
+
interactivePassword: "",
|
|
110
|
+
hidePromptLine: true
|
|
111
|
+
},
|
|
112
|
+
this.options,
|
|
113
|
+
options,
|
|
114
|
+
execOpts
|
|
115
|
+
) as CommandOptions
|
|
116
|
+
|
|
117
|
+
const prepareCommand = (command: string): string => {
|
|
118
|
+
const withCwd =
|
|
119
|
+
opts.cwd ? `cd ${JSON.stringify(opts.cwd)} && ${command}` : command
|
|
120
|
+
const shell = opts.shell
|
|
121
|
+
|
|
122
|
+
// If already shell-wrapped, don't touch
|
|
123
|
+
if (
|
|
124
|
+
shell === "none" ||
|
|
125
|
+
SHELL_WRAPPED_RX.test(withCwd) ||
|
|
126
|
+
PS_WRAPPED_RX.test(withCwd) ||
|
|
127
|
+
CMD_WRAPPED_RX.test(withCwd)
|
|
128
|
+
) {
|
|
129
|
+
return withCwd
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// POSIX family
|
|
133
|
+
if (
|
|
134
|
+
shell === "bash" ||
|
|
135
|
+
shell === "zsh" ||
|
|
136
|
+
shell === "sh" ||
|
|
137
|
+
shell === "dash" ||
|
|
138
|
+
shell === "ksh" ||
|
|
139
|
+
shell === "mksh" ||
|
|
140
|
+
shell === "fish"
|
|
141
|
+
) {
|
|
142
|
+
const flag = shell === "bash" || shell === "zsh" ? "-lc" : "-c"
|
|
143
|
+
const singleQuoted = withCwd.replace(/'/g, "'\\''")
|
|
144
|
+
return `${shell} ${flag} '${singleQuoted}'`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Windows shells
|
|
148
|
+
if (shell === "powershell") {
|
|
149
|
+
const ps = withCwd.replace(/'/g, "''")
|
|
150
|
+
return `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command '${ps}'`
|
|
151
|
+
}
|
|
152
|
+
if (shell === "cmd") {
|
|
153
|
+
const dq = withCwd.replace(/"/g, '\\"')
|
|
154
|
+
return `cmd /d /s /c "${dq}"`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Fallback: no wrapping
|
|
158
|
+
return withCwd
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!this.client?.isConnected())
|
|
162
|
+
throw new Error("SSH client is not connected.")
|
|
163
|
+
|
|
164
|
+
const prepared = prepareCommand(command)
|
|
165
|
+
const finalCommand = opts.rewriteCommand?.(prepared) ?? prepared
|
|
166
|
+
|
|
167
|
+
return new Promise<ProviderResponse>((resolve, reject) => {
|
|
168
|
+
const connection = this.client!.connection!
|
|
169
|
+
let timeoutId: NodeJS.Timeout | null = null
|
|
170
|
+
|
|
171
|
+
const execOnlyOpts = pick(opts || {}, [
|
|
172
|
+
"allowHalfOpen",
|
|
173
|
+
"env",
|
|
174
|
+
"pty",
|
|
175
|
+
"x11"
|
|
176
|
+
])
|
|
177
|
+
this.logger.trace({
|
|
178
|
+
msg: `[exec] ${finalCommand}`,
|
|
179
|
+
options: execOnlyOpts
|
|
180
|
+
})
|
|
181
|
+
connection.exec(finalCommand, execOnlyOpts, (err, channel) => {
|
|
182
|
+
if (err)
|
|
183
|
+
return reject(
|
|
184
|
+
new ProviderResponse({
|
|
185
|
+
command: finalCommand,
|
|
186
|
+
stderr: err.message,
|
|
187
|
+
code: 1
|
|
188
|
+
})
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if (opts.timeout && opts.timeout > 0) {
|
|
192
|
+
timeoutId = setTimeout(() => {
|
|
193
|
+
try {
|
|
194
|
+
channel.close()
|
|
195
|
+
} catch {}
|
|
196
|
+
reject(
|
|
197
|
+
new ProviderResponse({
|
|
198
|
+
command: finalCommand,
|
|
199
|
+
stderr: `Command timed out after ${opts.timeout}ms`,
|
|
200
|
+
code: 1
|
|
201
|
+
})
|
|
202
|
+
)
|
|
203
|
+
}, opts.timeout)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
opts.onChannel?.(channel)
|
|
207
|
+
|
|
208
|
+
const stdoutLines: string[] = []
|
|
209
|
+
const stderrLines: string[] = []
|
|
210
|
+
const emitStdout = makeLineEmitter((line) => {
|
|
211
|
+
if (
|
|
212
|
+
!(
|
|
213
|
+
opts.hidePromptLine &&
|
|
214
|
+
opts.promptMarker &&
|
|
215
|
+
line.includes(opts.promptMarker)
|
|
216
|
+
)
|
|
217
|
+
) {
|
|
218
|
+
opts.onStdout?.(line)
|
|
219
|
+
stdoutLines.push(line)
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
const emitStderr = makeLineEmitter((line) => {
|
|
223
|
+
if (
|
|
224
|
+
!(
|
|
225
|
+
opts.hidePromptLine &&
|
|
226
|
+
opts.promptMarker &&
|
|
227
|
+
line.includes(opts.promptMarker)
|
|
228
|
+
)
|
|
229
|
+
) {
|
|
230
|
+
opts.onStderr?.(line)
|
|
231
|
+
stderrLines.push(line)
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
let pwSent = false
|
|
236
|
+
let genericPwSent = false
|
|
237
|
+
let authDenied = false
|
|
238
|
+
let buffer = ""
|
|
239
|
+
|
|
240
|
+
const handlePrompts = (text: string) => {
|
|
241
|
+
buffer += text
|
|
242
|
+
|
|
243
|
+
const hasPrompt =
|
|
244
|
+
opts.promptMarker && text.includes(opts.promptMarker)
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
!pwSent &&
|
|
248
|
+
opts.promptMarker &&
|
|
249
|
+
buffer.includes(opts.promptMarker)
|
|
250
|
+
) {
|
|
251
|
+
channel.stdin.write(opts.interactivePassword + "\n")
|
|
252
|
+
pwSent = true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!genericPwSent && GENERIC_PROMPT_RX.test(buffer)) {
|
|
256
|
+
channel.stdin.write(opts.interactivePassword + "\n")
|
|
257
|
+
genericPwSent = true
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (FAILURE_RX.test(buffer)) authDenied = true
|
|
261
|
+
if (buffer.length > 4096) buffer = buffer.slice(-2048)
|
|
262
|
+
return hasPrompt
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
channel.on("data", (chunk: Buffer) => {
|
|
266
|
+
const text = chunk.toString(opts.encoding)
|
|
267
|
+
handlePrompts(text)
|
|
268
|
+
emitStdout(text)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
channel.stderr.on("data", (chunk: Buffer) => {
|
|
272
|
+
const text = chunk.toString(opts.encoding)
|
|
273
|
+
if (!handlePrompts(text)) {
|
|
274
|
+
emitStderr(text)
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
let code: number | null = null
|
|
279
|
+
channel.on("exit", (c: any) => {
|
|
280
|
+
code = c ?? null
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
channel.on("close", () => {
|
|
284
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
285
|
+
const result = new ProviderResponse({
|
|
286
|
+
command: finalCommand,
|
|
287
|
+
stdout: stdoutLines.join("\n").trim(),
|
|
288
|
+
stderr: stderrLines.join("\n").trim(),
|
|
289
|
+
code: code ?? (authDenied ? 1 : -1)
|
|
290
|
+
})
|
|
291
|
+
if (result.code === 0) {
|
|
292
|
+
resolve(result)
|
|
293
|
+
} else {
|
|
294
|
+
reject(result)
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
channel.on("error", (e: any) => {
|
|
299
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
300
|
+
reject(
|
|
301
|
+
new ProviderResponse({
|
|
302
|
+
command: finalCommand,
|
|
303
|
+
stdout: stdoutLines.join("\n").trim(),
|
|
304
|
+
stderr: String(e?.message ?? e),
|
|
305
|
+
code: 1
|
|
306
|
+
})
|
|
307
|
+
)
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async destroy(): Promise<void> {
|
|
315
|
+
if (this.client?.isConnected()) {
|
|
316
|
+
this.client.connection?.end()
|
|
317
|
+
}
|
|
318
|
+
this.connected = false
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async cleanup(): Promise<void> {
|
|
322
|
+
this.client?.dispose()
|
|
323
|
+
this.client = null
|
|
324
|
+
this.initialized = false
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Probe remote OS/arch as soon as we connect; stores results in `this.osInfo`. */
|
|
328
|
+
async getOsInfo(): Promise<OsInfo> {
|
|
329
|
+
const execRaw = this.executor({ shell: "none", timeout: 5000 })
|
|
330
|
+
|
|
331
|
+
// POSIX probe (uname + /etc/os-release)
|
|
332
|
+
const posixScript =
|
|
333
|
+
'OS="$(uname -s 2>/dev/null || true)"; ARCH="$(uname -m 2>/dev/null || true)"; F=""; ' +
|
|
334
|
+
'[ -r /etc/os-release ] && F=/etc/os-release; [ -z "$F" ] && [ -r /usr/lib/os-release ] && F=/usr/lib/os-release; ' +
|
|
335
|
+
'ID=""; VERSION_ID=""; PRETTY_NAME=""; [ -n "$F" ] && . "$F"; ' +
|
|
336
|
+
'printf "__os=%s\\n__arch=%s\\n__id=%s\\n__ver=%s\\n__pretty=%s\\n" "$OS" "$ARCH" "$ID" "$VERSION_ID" "$PRETTY_NAME"'
|
|
337
|
+
|
|
338
|
+
let out: ProviderResponse | null = null
|
|
339
|
+
try {
|
|
340
|
+
out = await execRaw(`sh -c '${posixScript.replace(/'/g, "'\\''")}'`)
|
|
341
|
+
} catch {
|
|
342
|
+
// try bash if /bin/sh is missing (rare)
|
|
343
|
+
try {
|
|
344
|
+
out = await execRaw(`bash -lc '${posixScript.replace(/'/g, "'\\''")}'`)
|
|
345
|
+
} catch {
|
|
346
|
+
out = null
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (out && out.code === 0 && out.stdout) {
|
|
351
|
+
const kv = parseTagged(out.stdout)
|
|
352
|
+
const kernel = (kv.__os || "").trim()
|
|
353
|
+
const archRaw = (kv.__arch || "").trim()
|
|
354
|
+
const fam = normalizeOs(kernel)
|
|
355
|
+
const res: OsInfo = {
|
|
356
|
+
family: fam,
|
|
357
|
+
arch: normalizeArch(archRaw),
|
|
358
|
+
kernel,
|
|
359
|
+
distroId: fam === "windows" ? "windows" : kv.__id || undefined,
|
|
360
|
+
versionId: fam === "windows" ? undefined : kv.__ver || undefined,
|
|
361
|
+
prettyName: kv.__pretty || undefined,
|
|
362
|
+
source: "posix"
|
|
363
|
+
}
|
|
364
|
+
this.os = res
|
|
365
|
+
return res
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// PowerShell probe (Windows)
|
|
369
|
+
const ps = [
|
|
370
|
+
"$arch=$env:PROCESSOR_ARCHITECTURE;",
|
|
371
|
+
"$osCaption=(Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue).Caption;",
|
|
372
|
+
"if(-not $osCaption){$osCaption=(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion' -ErrorAction SilentlyContinue).ProductName}",
|
|
373
|
+
"$ver=(Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue).Version;",
|
|
374
|
+
"$obj=[ordered]@{os='Windows';arch=$arch;id='windows';version=$ver;pretty=$osCaption};",
|
|
375
|
+
"$obj|ConvertTo-Json -Compress"
|
|
376
|
+
].join(" ")
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const r = await execRaw(
|
|
380
|
+
`powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command '${ps.replace(
|
|
381
|
+
/'/g,
|
|
382
|
+
"''"
|
|
383
|
+
)}'`
|
|
384
|
+
)
|
|
385
|
+
const data = JSON.parse(r.stdout || "{}")
|
|
386
|
+
const res: OsInfo = {
|
|
387
|
+
family: "windows",
|
|
388
|
+
arch: normalizeArch(String(data.arch || "")),
|
|
389
|
+
kernel: "Windows",
|
|
390
|
+
distroId: "windows",
|
|
391
|
+
versionId: data.version || undefined,
|
|
392
|
+
prettyName: data.pretty || undefined,
|
|
393
|
+
source: "powershell"
|
|
394
|
+
}
|
|
395
|
+
this.os = res
|
|
396
|
+
return res
|
|
397
|
+
} catch {
|
|
398
|
+
// fallthrough
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const res: OsInfo = {
|
|
402
|
+
family: "unknown",
|
|
403
|
+
arch: "unknown",
|
|
404
|
+
source: "unknown"
|
|
405
|
+
}
|
|
406
|
+
this.os = res
|
|
407
|
+
return res
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* ───────────────────────── utilities ───────────────────────── */
|
|
412
|
+
|
|
413
|
+
function parseTagged(s: string): Record<string, string> {
|
|
414
|
+
const out: Record<string, string> = {}
|
|
415
|
+
for (const line of String(s).split(/\r?\n/)) {
|
|
416
|
+
const m = line.match(/^(__[a-z]+)=(.*)$/)
|
|
417
|
+
if (m) out[m[1]] = m[2]
|
|
418
|
+
}
|
|
419
|
+
return out
|
|
420
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SSHProvider } from "./ssh.provider"
|
|
2
|
+
import { baseName, targetDir } from "../../utils/path.utils"
|
|
3
|
+
import { toOctal } from "../../utils/number.utils"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Split string into lines while keeping separators.
|
|
7
|
+
*/
|
|
8
|
+
function splitLinesIncludeSeparators(str: string): string[] {
|
|
9
|
+
const linesWithSeparators: string[] = []
|
|
10
|
+
const parts = str.split(/(\r\n|\r+|\n)/)
|
|
11
|
+
for (let i = 0; i < Math.ceil(parts.length / 2); i++) {
|
|
12
|
+
linesWithSeparators.push(parts[2 * i] + (parts[2 * i + 1] ?? ""))
|
|
13
|
+
}
|
|
14
|
+
return linesWithSeparators
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns a handler that buffers chunks and emits complete lines.
|
|
19
|
+
*/
|
|
20
|
+
export function makeLineEmitter(cb: (line: string, buffer: string) => void) {
|
|
21
|
+
let buffer = ""
|
|
22
|
+
return (chunk: string) => {
|
|
23
|
+
buffer += chunk
|
|
24
|
+
const lines = splitLinesIncludeSeparators(buffer)
|
|
25
|
+
const last = lines[lines.length - 1]
|
|
26
|
+
const hasSep = last.endsWith("\n") || last.endsWith("\r")
|
|
27
|
+
const emit = hasSep ? lines : lines.slice(0, -1)
|
|
28
|
+
buffer = hasSep ? "" : last
|
|
29
|
+
for (const line of emit) cb(line, buffer)
|
|
30
|
+
}
|
|
31
|
+
}
|