@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/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Main sources of `katmer` cli and runtime are located under this folder.
|
package/cli/katmer.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander"
|
|
4
|
+
import { version, description } from "../package.json"
|
|
5
|
+
import run from "./run"
|
|
6
|
+
|
|
7
|
+
const cli = new Command()
|
|
8
|
+
cli
|
|
9
|
+
.name("katmer")
|
|
10
|
+
.description(description)
|
|
11
|
+
.option(
|
|
12
|
+
"-t, --target [files...]",
|
|
13
|
+
"Path to config file",
|
|
14
|
+
"/etc/katmer/config.yaml"
|
|
15
|
+
)
|
|
16
|
+
.option("--cwd [dir]", "Override working directory")
|
|
17
|
+
.version(version, "-v, --version")
|
|
18
|
+
.helpOption("--help")
|
|
19
|
+
|
|
20
|
+
run(cli)
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await cli.parseAsync(process.argv)
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error(e)
|
|
26
|
+
console.error(e.message || e)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
package/cli/run.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from "commander"
|
|
2
|
+
import { KatmerCore, type KatmerInitOptions } from "../lib/katmer"
|
|
3
|
+
|
|
4
|
+
export default function (cli: Command) {
|
|
5
|
+
const command = new Command("run")
|
|
6
|
+
.description("Execute katmer task file")
|
|
7
|
+
.argument("<file>", "The file to run")
|
|
8
|
+
.action(async (file, opts) => {
|
|
9
|
+
const options = cli.opts<KatmerInitOptions>()
|
|
10
|
+
await using instance = new KatmerCore(options)
|
|
11
|
+
await instance.init()
|
|
12
|
+
await instance.run(file)
|
|
13
|
+
})
|
|
14
|
+
cli.addCommand(command)
|
|
15
|
+
return cli
|
|
16
|
+
}
|
package/index.ts
ADDED
package/lib/config.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import configSchema from "./schemas/katmer_config.schema.json" with { type: "json" }
|
|
2
|
+
|
|
3
|
+
import Ajv from "ajv/dist/2020"
|
|
4
|
+
import ajvErrors from "ajv-errors"
|
|
5
|
+
import { toMerged } from "es-toolkit"
|
|
6
|
+
|
|
7
|
+
import { parseKatmerFile, readKatmerFile } from "./utils/file.utils"
|
|
8
|
+
import { wrapInArray } from "./utils/json.utils"
|
|
9
|
+
import { HttpModule } from "./modules/http/http.local.module"
|
|
10
|
+
import { LocalProvider } from "./providers/local.provider"
|
|
11
|
+
import { normalizeAjvError } from "./utils/ajv.utils"
|
|
12
|
+
import type { KatmerConfig } from "./interfaces/config.interface"
|
|
13
|
+
|
|
14
|
+
const ajv = ajvErrors(
|
|
15
|
+
new Ajv({
|
|
16
|
+
allErrors: true,
|
|
17
|
+
allowMatchingProperties: true,
|
|
18
|
+
allowUnionTypes: true
|
|
19
|
+
}),
|
|
20
|
+
{
|
|
21
|
+
singleError: false
|
|
22
|
+
}
|
|
23
|
+
).addSchema(configSchema)
|
|
24
|
+
|
|
25
|
+
export const KatmerConfigLoader = {
|
|
26
|
+
async load(
|
|
27
|
+
target: string | object | (string | object)[],
|
|
28
|
+
opts?: {
|
|
29
|
+
cwd?: string
|
|
30
|
+
}
|
|
31
|
+
): Promise<KatmerConfig> {
|
|
32
|
+
const configTargets = wrapInArray(target)
|
|
33
|
+
const cwd = opts?.cwd || process.cwd()
|
|
34
|
+
|
|
35
|
+
let config = {} as KatmerConfig
|
|
36
|
+
|
|
37
|
+
for (const configTarget of configTargets) {
|
|
38
|
+
let loadedConfig: Record<string, any> = {}
|
|
39
|
+
if (typeof configTarget === "string") {
|
|
40
|
+
const parsed = await readKatmerFile(configTarget, {
|
|
41
|
+
cwd,
|
|
42
|
+
process: false,
|
|
43
|
+
errorMessage: `Failed to load config file from: ${configTarget}`
|
|
44
|
+
})
|
|
45
|
+
loadedConfig = this.validate(parsed, configTarget)
|
|
46
|
+
} else {
|
|
47
|
+
const fetch = new HttpModule(configTarget as any, new LocalProvider({}))
|
|
48
|
+
const { body, url } = await fetch.execute({} as any)
|
|
49
|
+
|
|
50
|
+
const filename = `${url.origin}${url.pathname}`
|
|
51
|
+
loadedConfig = this.validate(
|
|
52
|
+
await parseKatmerFile(filename, body),
|
|
53
|
+
filename
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (loadedConfig.include) {
|
|
58
|
+
loadedConfig = toMerged(
|
|
59
|
+
loadedConfig,
|
|
60
|
+
await KatmerConfigLoader.load(loadedConfig.include, opts)
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
loadedConfig.include = undefined
|
|
64
|
+
config = toMerged(config, loadedConfig)
|
|
65
|
+
}
|
|
66
|
+
config.cwd = opts?.cwd || process.cwd()
|
|
67
|
+
return config
|
|
68
|
+
},
|
|
69
|
+
validate(obj: any, filename?: string) {
|
|
70
|
+
const configValidator = ajv.getSchema(
|
|
71
|
+
"https://katmer.dev/schemas/katmer-config.schema.json"
|
|
72
|
+
)!
|
|
73
|
+
configValidator(obj)
|
|
74
|
+
if (configValidator.errors) {
|
|
75
|
+
const err = configValidator.errors[0]
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Invalid configuration${filename ? ` [${filename}]` : ""}: ${normalizeAjvError(err)} at path: ${err.instancePath}`
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
return obj
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import pino from "pino"
|
|
2
|
+
|
|
3
|
+
export interface KatmerCLIOptions {
|
|
4
|
+
cwd?: string
|
|
5
|
+
target: string[]
|
|
6
|
+
}
|
|
7
|
+
// Connection variants
|
|
8
|
+
export interface SSHConfig {
|
|
9
|
+
connection: "ssh"
|
|
10
|
+
hostname: string
|
|
11
|
+
port?: number
|
|
12
|
+
username?: string
|
|
13
|
+
password?: string
|
|
14
|
+
private_key?: string
|
|
15
|
+
private_key_password?: string
|
|
16
|
+
[k: string]: unknown
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LocalConfig {
|
|
20
|
+
connection: "local"
|
|
21
|
+
[k: string]: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type KatmerHostInput = SSHConfig | LocalConfig
|
|
25
|
+
|
|
26
|
+
export type KatmerHostResolved = (SSHConfig | LocalConfig) & {
|
|
27
|
+
name: string
|
|
28
|
+
variables?: Record<string, unknown>
|
|
29
|
+
environment?: Record<string, string>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Reserved labels
|
|
33
|
+
export type KatmerReservedKey =
|
|
34
|
+
| "all"
|
|
35
|
+
| "children"
|
|
36
|
+
| "settings"
|
|
37
|
+
| "hosts"
|
|
38
|
+
| "variables"
|
|
39
|
+
|
|
40
|
+
// Group settings (anything mergeable; no required `connection`)
|
|
41
|
+
export type KatmerGroupSettings = {
|
|
42
|
+
[k: string]: unknown
|
|
43
|
+
} & Partial<
|
|
44
|
+
Omit<SSHConfig, "connection" | "hostname"> & Omit<LocalConfig, "connection">
|
|
45
|
+
>
|
|
46
|
+
|
|
47
|
+
export type KatmerGroupVariables = Record<string, unknown>
|
|
48
|
+
export type KatmerGroupEnvironment = Record<string, unknown>
|
|
49
|
+
|
|
50
|
+
// Strict host map for the **root** form
|
|
51
|
+
export type KatmerHostsRoot = Record<string, KatmerHostInput>
|
|
52
|
+
|
|
53
|
+
// Shorthand-friendly host map for **groups** (allows `{}` etc.)
|
|
54
|
+
export type KatmerHostShorthand = Partial<KatmerHostInput>
|
|
55
|
+
export type KatmerHostsInGroup = Record<string, KatmerHostShorthand>
|
|
56
|
+
|
|
57
|
+
// Children reference map
|
|
58
|
+
export type KatmerChildren = Record<string, {} | undefined>
|
|
59
|
+
|
|
60
|
+
// A single group
|
|
61
|
+
export interface KatmerGroup {
|
|
62
|
+
children?: KatmerChildren
|
|
63
|
+
hosts?: KatmerHostsInGroup
|
|
64
|
+
settings?: KatmerGroupSettings // ← accepts {}
|
|
65
|
+
variables?: KatmerGroupVariables
|
|
66
|
+
environment?: KatmerGroupEnvironment
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Root “hosts/settings” form (implicit 'ungrouped')
|
|
70
|
+
export interface KatmerTargetsRootForm {
|
|
71
|
+
hosts: KatmerHostsRoot // ← strict here
|
|
72
|
+
settings?: KatmerGroupSettings
|
|
73
|
+
variables?: KatmerGroupVariables
|
|
74
|
+
environment?: KatmerGroupEnvironment
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Grouped form (forbid reserved top-level keys here)
|
|
78
|
+
export type KatmerTargetsGroupedForm = Record<string, KatmerGroup> & {
|
|
79
|
+
all?: never
|
|
80
|
+
children?: never
|
|
81
|
+
hosts?: never
|
|
82
|
+
settings?: never
|
|
83
|
+
variables?: never
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Top-level targets
|
|
87
|
+
export type KatmerTargets = KatmerTargetsRootForm | KatmerTargetsGroupedForm
|
|
88
|
+
|
|
89
|
+
export interface KatmerConfig {
|
|
90
|
+
cwd?: string
|
|
91
|
+
logging?: {
|
|
92
|
+
dir?: string
|
|
93
|
+
level?: "trace" | "debug" | "info" | "warn" | "error" | "silent"
|
|
94
|
+
}
|
|
95
|
+
targets: KatmerTargets
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Optional: normalizer output helpers
|
|
99
|
+
export interface KatmerNormalizedTargets {
|
|
100
|
+
groups: Map<string, Set<string>>
|
|
101
|
+
hosts: Map<string, KatmerHostResolved>
|
|
102
|
+
allNames: Set<string>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface StandardLogger {
|
|
106
|
+
trace: pino.LogFn
|
|
107
|
+
debug: pino.LogFn
|
|
108
|
+
info: pino.LogFn
|
|
109
|
+
warn: pino.LogFn
|
|
110
|
+
error: pino.LogFn
|
|
111
|
+
fatal: pino.LogFn
|
|
112
|
+
child: (bindings: Record<string, unknown>) => StandardLogger
|
|
113
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type TwigExpression = `{{${string}}}`
|
|
2
|
+
// common interface so AptRepositoryModule does not depend on SSH
|
|
3
|
+
export interface Executor {
|
|
4
|
+
run(
|
|
5
|
+
command: string,
|
|
6
|
+
options?: {
|
|
7
|
+
cwd?: string
|
|
8
|
+
encoding?: BufferEncoding
|
|
9
|
+
onStdout?: (line: string) => void
|
|
10
|
+
onStderr?: (line: string) => void
|
|
11
|
+
}
|
|
12
|
+
): Promise<{ stdout: string; stderr: string; code: number }>
|
|
13
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { OsArch, OsFamily } from "./provider.interface"
|
|
2
|
+
|
|
3
|
+
export type PackageManager =
|
|
4
|
+
| "apt"
|
|
5
|
+
| "dnf"
|
|
6
|
+
| "yum"
|
|
7
|
+
| "zypper"
|
|
8
|
+
| "apk"
|
|
9
|
+
| "pacman"
|
|
10
|
+
| "brew"
|
|
11
|
+
| "port"
|
|
12
|
+
| "choco"
|
|
13
|
+
| "winget"
|
|
14
|
+
| "unknown"
|
|
15
|
+
|
|
16
|
+
export interface PackageConstraint {
|
|
17
|
+
/** Package name as seen by the package manager(s). */
|
|
18
|
+
name: string
|
|
19
|
+
/** Exact version (distro version string is OK), e.g. "1.5.2-1ubuntu1". */
|
|
20
|
+
version?: string
|
|
21
|
+
/**
|
|
22
|
+
* Semver-like or comparator range, e.g. ">=1.5.0 <2".
|
|
23
|
+
* If semver is not available, a simple comparator fallback is used.
|
|
24
|
+
*/
|
|
25
|
+
range?: string
|
|
26
|
+
/** One or more preferred package managers; omit to auto-detect. */
|
|
27
|
+
manager?: PackageManager | PackageManager[]
|
|
28
|
+
/**
|
|
29
|
+
* Alternatives: any of these packages satisfying the constraints is acceptable.
|
|
30
|
+
* Useful for cross-distro names (e.g., cron | cronie | dcron).
|
|
31
|
+
*/
|
|
32
|
+
alternatives?: PackageConstraint[]
|
|
33
|
+
/**
|
|
34
|
+
* Optional custom test that returns 0 if installed and prints the version.
|
|
35
|
+
* Example: `dpkg-query -W -f='${Version}' cron`
|
|
36
|
+
*/
|
|
37
|
+
testCmd?: string
|
|
38
|
+
/** When using testCmd, regex to extract the version from stdout (first capture group). */
|
|
39
|
+
versionRegex?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BinaryConstraint {
|
|
43
|
+
/** Command to locate (e.g., "crontab", "bash", "powershell"). */
|
|
44
|
+
cmd: string
|
|
45
|
+
/** Optional args to run for version probe; default tries "--version" or "-V". */
|
|
46
|
+
args?: string[]
|
|
47
|
+
/** Regex to parse a version string from stdout/stderr (first capture group used). */
|
|
48
|
+
versionRegex?: string | RegExp
|
|
49
|
+
/** Version range to satisfy (same rules as PackageConstraint.range). */
|
|
50
|
+
range?: string
|
|
51
|
+
/** Alternatives: if any child passes, this binary constraint is satisfied. */
|
|
52
|
+
or?: BinaryConstraint[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ModulePlatformConstraint {
|
|
56
|
+
/** Supported architectures; default = ["any"]. */
|
|
57
|
+
arch?: OsArch[]
|
|
58
|
+
/** Family-level packages (before per-distro overrides). */
|
|
59
|
+
packages?: Array<PackageConstraint | string>
|
|
60
|
+
/** Required binaries/shells present in PATH. */
|
|
61
|
+
binaries?: BinaryConstraint[]
|
|
62
|
+
/** Require root (POSIX) or Administrator (Windows). */
|
|
63
|
+
requireRoot?: boolean
|
|
64
|
+
/** Optional minimal kernel version (POSIX) or OS version (Windows/macOS). */
|
|
65
|
+
minKernel?: string // e.g., ">=4.15"
|
|
66
|
+
minOsVersion?: string // e.g., ">=10.15" for macOS, ">=10.0" for Windows
|
|
67
|
+
/**
|
|
68
|
+
* Per-distro overrides/extensions. Keys are normalized IDs like:
|
|
69
|
+
* "debian", "ubuntu", "rhel", "centos", "rocky", "fedora", "alpine",
|
|
70
|
+
* "arch", "opensuse", "sles", "amzn", "amazon", or "any".
|
|
71
|
+
*/
|
|
72
|
+
distro?: {
|
|
73
|
+
[distroId: string]: true | false | Omit<ModulePlatformConstraint, "distro">
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* New constraints structure:
|
|
79
|
+
* - platform is a map: any|linux|darwin|windows|freebsd|... → true|false|ModulePlatformConstraint
|
|
80
|
+
* - Each platform may also have distro overrides.
|
|
81
|
+
*/
|
|
82
|
+
export interface ModuleConstraints {
|
|
83
|
+
platform?: {
|
|
84
|
+
[family in OsFamily | "any" | "local"]?:
|
|
85
|
+
| true
|
|
86
|
+
| false
|
|
87
|
+
| ModulePlatformConstraint
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type ModulePlatformConstraintLegacy = never // (kept only if you referenced it elsewhere)
|
|
92
|
+
|
|
93
|
+
export interface ModuleOptionsObject {
|
|
94
|
+
[key: string]: any // allow provider-specific options
|
|
95
|
+
}
|
|
96
|
+
export type ModuleOptions = any
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Standard result shape returned by modules.
|
|
100
|
+
*
|
|
101
|
+
* @remarks
|
|
102
|
+
* - `changed` indicates whether the module made any modifications on the target.
|
|
103
|
+
* - `failed` flags an error condition; when `true`, execution is considered unsuccessful.
|
|
104
|
+
* - `stdout`/`stderr` carry raw process output when applicable.
|
|
105
|
+
* - Timing fields (`start`, `end`, `delta`) are best-effort and ISO8601 for start/end.
|
|
106
|
+
* - `attempts`/`retries` are used by controls like `until` to report how many times an action ran.
|
|
107
|
+
* - Extra module-specific keys are allowed via the index signature.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```yaml
|
|
111
|
+
* - name: fetch metadata
|
|
112
|
+
* register: meta
|
|
113
|
+
* http:
|
|
114
|
+
* url: "https://example.com/meta"
|
|
115
|
+
* fail_on_http_error: false
|
|
116
|
+
*
|
|
117
|
+
* - name: inspect result
|
|
118
|
+
* debug:
|
|
119
|
+
* vars:
|
|
120
|
+
* ok: "{{ not meta.failed }}"
|
|
121
|
+
* changed: "{{ meta.changed }}"
|
|
122
|
+
* status: "{{ meta.status }}"
|
|
123
|
+
* took: "{{ meta.delta }}"
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @public
|
|
127
|
+
*/
|
|
128
|
+
export interface ModuleCommonReturn {
|
|
129
|
+
/** Whether this module changed anything on the target. */
|
|
130
|
+
changed?: boolean
|
|
131
|
+
|
|
132
|
+
/** Whether the module failed (non-zero exit code, validation failure, etc.). */
|
|
133
|
+
failed?: boolean
|
|
134
|
+
|
|
135
|
+
/** Human-readable message describing the outcome or error. */
|
|
136
|
+
msg?: string
|
|
137
|
+
|
|
138
|
+
/** Captured standard output, if available. */
|
|
139
|
+
stdout?: string
|
|
140
|
+
|
|
141
|
+
/** Captured standard error, if available. */
|
|
142
|
+
stderr?: string
|
|
143
|
+
|
|
144
|
+
/** ISO8601 timestamp when execution started (best-effort). */
|
|
145
|
+
start?: string
|
|
146
|
+
|
|
147
|
+
/** ISO8601 timestamp when execution ended (best-effort). */
|
|
148
|
+
end?: string
|
|
149
|
+
|
|
150
|
+
/** Duration string, e.g., "0:00:00.123" (best-effort). */
|
|
151
|
+
delta?: string
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Number of attempts performed so far (e.g., by `until`).
|
|
155
|
+
* Typically increments from 1 upwards.
|
|
156
|
+
*/
|
|
157
|
+
attempts?: number
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Maximum retries allowed for this operation (e.g., by `until`).
|
|
161
|
+
* This reflects the configured ceiling, not the attempts taken.
|
|
162
|
+
*/
|
|
163
|
+
retries?: number
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Additional module-specific data.
|
|
167
|
+
* Modules may extend the result with fields such as `status`, `url`, `dest`, etc.
|
|
168
|
+
*/
|
|
169
|
+
[key: string]: unknown
|
|
170
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { ProviderResponse } from "../providers/provider_response"
|
|
2
|
+
import type { StandardLogger } from "./config.interface"
|
|
3
|
+
|
|
4
|
+
export type OsFamily =
|
|
5
|
+
| "any"
|
|
6
|
+
| "linux"
|
|
7
|
+
| "darwin"
|
|
8
|
+
| "windows"
|
|
9
|
+
| "freebsd"
|
|
10
|
+
| "openbsd"
|
|
11
|
+
| "netbsd"
|
|
12
|
+
| "aix"
|
|
13
|
+
| "solaris"
|
|
14
|
+
| "unknown"
|
|
15
|
+
|
|
16
|
+
export type OsArch =
|
|
17
|
+
| "x86_64"
|
|
18
|
+
| "arm64"
|
|
19
|
+
| "armv7"
|
|
20
|
+
| "armv6"
|
|
21
|
+
| "i386"
|
|
22
|
+
| "ppc64le"
|
|
23
|
+
| "s390x"
|
|
24
|
+
| "riscv64"
|
|
25
|
+
| "loongarch64"
|
|
26
|
+
| "any"
|
|
27
|
+
| "unknown"
|
|
28
|
+
|
|
29
|
+
// Added powershell/cmd; keep "none" for raw passthrough
|
|
30
|
+
export type SupportedShell =
|
|
31
|
+
| "bash"
|
|
32
|
+
| "sh"
|
|
33
|
+
| "zsh"
|
|
34
|
+
| "dash"
|
|
35
|
+
| "ksh"
|
|
36
|
+
| "mksh"
|
|
37
|
+
| "fish"
|
|
38
|
+
| "powershell"
|
|
39
|
+
| "cmd"
|
|
40
|
+
| "none"
|
|
41
|
+
export interface ProviderOptions {
|
|
42
|
+
name?: string
|
|
43
|
+
shell?: SupportedShell
|
|
44
|
+
timeout?: number
|
|
45
|
+
retries?: number
|
|
46
|
+
[key: string]: any
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface OsInfo {
|
|
50
|
+
family: OsFamily
|
|
51
|
+
arch: OsArch
|
|
52
|
+
kernel?: string
|
|
53
|
+
distroId?: string
|
|
54
|
+
versionId?: string
|
|
55
|
+
prettyName?: string
|
|
56
|
+
source: "posix" | "powershell" | "unknown"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Abstract base class for all Katmer providers.
|
|
61
|
+
* Providers define how tasks are executed (SSH, Local, AWS SSM, GCP, etc.)
|
|
62
|
+
*/
|
|
63
|
+
export abstract class KatmerProvider<
|
|
64
|
+
TOptions extends ProviderOptions = ProviderOptions
|
|
65
|
+
> {
|
|
66
|
+
static readonly name: string
|
|
67
|
+
type: string
|
|
68
|
+
|
|
69
|
+
defaultShell: SupportedShell = "sh"
|
|
70
|
+
os: OsInfo = {
|
|
71
|
+
family: "unknown",
|
|
72
|
+
arch: "unknown",
|
|
73
|
+
source: "unknown"
|
|
74
|
+
}
|
|
75
|
+
logger!: StandardLogger
|
|
76
|
+
options: TOptions
|
|
77
|
+
|
|
78
|
+
connected = false
|
|
79
|
+
initialized = false
|
|
80
|
+
|
|
81
|
+
variables: Record<string, any> = {}
|
|
82
|
+
environment: Record<string, string> = {}
|
|
83
|
+
|
|
84
|
+
constructor(options: TOptions) {
|
|
85
|
+
this.options = options
|
|
86
|
+
this.type = this.constructor.name
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate configuration before use (e.g., check required fields).
|
|
91
|
+
*/
|
|
92
|
+
abstract check(): Promise<void>
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Initialize resources (e.g., allocate clients, prepare temp dirs).
|
|
96
|
+
*/
|
|
97
|
+
abstract initialize(): Promise<void>
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Establish connection/session.
|
|
101
|
+
*/
|
|
102
|
+
abstract connect(): Promise<void>
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Execute a command within this provider's context.
|
|
106
|
+
*/
|
|
107
|
+
abstract executor(
|
|
108
|
+
options?: Record<string, any>
|
|
109
|
+
): (
|
|
110
|
+
command: string,
|
|
111
|
+
options?: Record<string, any>
|
|
112
|
+
) => Promise<ProviderResponse>
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Tear down connection/session (but keep reusable state).
|
|
116
|
+
*/
|
|
117
|
+
abstract destroy(): Promise<void>
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Cleanup all allocated resources (irreversible).
|
|
121
|
+
*/
|
|
122
|
+
abstract cleanup(): Promise<void>
|
|
123
|
+
|
|
124
|
+
/** Probe remote OS/arch as soon as we connect; called automatically on `ensureReady()` */
|
|
125
|
+
abstract getOsInfo(): Promise<OsInfo>
|
|
126
|
+
|
|
127
|
+
/** Pick the best shell based on OS and availability; sets `this.options.shell`. */
|
|
128
|
+
async decideDefaultShell(): Promise<SupportedShell> {
|
|
129
|
+
const execRaw = this.executor({ shell: "none", timeout: 4000 })
|
|
130
|
+
|
|
131
|
+
// Windows → prefer PowerShell, fallback to cmd
|
|
132
|
+
if (this.os.family === "windows") {
|
|
133
|
+
try {
|
|
134
|
+
const r = await execRaw(
|
|
135
|
+
`powershell -NoProfile -NonInteractive -Command "$PSVersionTable.PSVersion.Major"`
|
|
136
|
+
)
|
|
137
|
+
if (r.code === 0) {
|
|
138
|
+
this.defaultShell = "powershell"
|
|
139
|
+
return "powershell"
|
|
140
|
+
}
|
|
141
|
+
} catch {}
|
|
142
|
+
this.defaultShell = "cmd"
|
|
143
|
+
return "cmd"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// POSIX → choose first available, prefer bash/zsh
|
|
147
|
+
const probe =
|
|
148
|
+
'for s in bash zsh ksh mksh dash sh fish; do command -v "$s" >/dev/null 2>&1 && { echo "$s"; exit 0; }; done; echo sh'
|
|
149
|
+
let chosen = "sh"
|
|
150
|
+
try {
|
|
151
|
+
const r = await execRaw(`sh -c '${probe}'`)
|
|
152
|
+
if (r.code === 0 && r.stdout?.trim()) chosen = r.stdout.trim()
|
|
153
|
+
} catch {
|
|
154
|
+
// fallback to bash if sh probing failed
|
|
155
|
+
try {
|
|
156
|
+
const r2 = await execRaw(`bash -lc '${probe}'`)
|
|
157
|
+
if (r2.code === 0 && r2.stdout?.trim()) chosen = r2.stdout.trim()
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// normalize to SupportedShell
|
|
162
|
+
const asShell =
|
|
163
|
+
(["bash", "zsh", "ksh", "mksh", "dash", "sh", "fish"].find(
|
|
164
|
+
(s) => s === chosen
|
|
165
|
+
) as SupportedShell) || "sh"
|
|
166
|
+
|
|
167
|
+
this.defaultShell = asShell
|
|
168
|
+
return asShell
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Helper to ensure full lifecycle (for convenience in orchestrators).
|
|
173
|
+
*/
|
|
174
|
+
async ensureReady(): Promise<this> {
|
|
175
|
+
if (!this.initialized) {
|
|
176
|
+
await this.check()
|
|
177
|
+
await this.initialize()
|
|
178
|
+
this.initialized = true
|
|
179
|
+
}
|
|
180
|
+
if (!this.connected) {
|
|
181
|
+
await this.connect()
|
|
182
|
+
|
|
183
|
+
this.os = await this.getOsInfo()
|
|
184
|
+
await this.decideDefaultShell()
|
|
185
|
+
|
|
186
|
+
this.connected = true
|
|
187
|
+
}
|
|
188
|
+
return this
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Safe shutdown wrapper that handles errors gracefully.
|
|
193
|
+
*/
|
|
194
|
+
async safeShutdown(): Promise<void> {
|
|
195
|
+
try {
|
|
196
|
+
await this.destroy()
|
|
197
|
+
this.connected = false
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.warn(`[Provider:${this.options.name}] destroy() failed:`, err)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await this.cleanup()
|
|
204
|
+
this.initialized = false
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.warn(`[Provider:${this.options.name}] cleanup() failed:`, err)
|
|
207
|
+
}
|
|
208
|
+
this.logger.trace(`Disconnected from provider: ${this.options.name}`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async [Symbol.asyncDispose]() {
|
|
212
|
+
await this.safeShutdown()
|
|
213
|
+
}
|
|
214
|
+
}
|