@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,298 @@
1
+ import { type ModuleCommonReturn } from "../../interfaces/module.interface"
2
+ import { SSHProvider } from "../../providers/ssh/ssh.provider"
3
+ import type { KatmerProvider } from "../../interfaces/provider.interface"
4
+
5
+ import { HttpModule as HTTPCurlModule } from "./http.curl.module"
6
+ import { HttpModule as HTTPLocalModule } from "./http.local.module"
7
+
8
+ declare module "../../interfaces/task.interface" {
9
+ export namespace Katmer {
10
+ export interface TaskActions {
11
+ /**
12
+ * Perform an HTTP(S) request.
13
+ * See HttpModuleOptions for all parameters.
14
+ */
15
+ http?: HttpModuleOptions
16
+ }
17
+ }
18
+ }
19
+
20
+ /**
21
+ * HTTP methods
22
+ * @public
23
+ */
24
+ type HttpMethod =
25
+ | "GET"
26
+ | "POST"
27
+ | "PUT"
28
+ | "PATCH"
29
+ | "DELETE"
30
+ | "HEAD"
31
+ | "OPTIONS"
32
+ | "TRACE"
33
+ | "CONNECT"
34
+
35
+ type HttpBasicAuth = {
36
+ /**
37
+ * Use Basic authentication.
38
+ * @defaultValue "basic"
39
+ */
40
+ type: "basic"
41
+ /**
42
+ * Username used for basic auth.
43
+ */
44
+ username: string
45
+ /**
46
+ * Password used for basic auth.
47
+ */
48
+ password: string
49
+ }
50
+ type HttpBearerAuth = {
51
+ /**
52
+ * Use Bearer token authentication.
53
+ * @defaultValue "bearer"
54
+ */
55
+ type: "bearer"
56
+ /**
57
+ * Bearer token to send in Authorization header.
58
+ */
59
+ token: string
60
+ }
61
+ /**
62
+ * Authentication configuration.
63
+ * @public
64
+ */
65
+ export type HttpAuth = HttpBasicAuth | HttpBearerAuth
66
+
67
+ /**
68
+ * Retry configuration.
69
+ * @public
70
+ */
71
+ export interface HttpRetry {
72
+ /**
73
+ * Total number of retry attempts on transient failures.
74
+ * Maps to: --retry [tries]
75
+ */
76
+ tries?: number
77
+ /**
78
+ * Delay in seconds between retries.
79
+ * Maps to: --retry-delay [seconds]
80
+ */
81
+ delay?: number
82
+ /**
83
+ * Total time limit in seconds for all retries.
84
+ * Maps to: --retry-max-time [seconds]
85
+ */
86
+ max_time?: number
87
+ }
88
+
89
+ /**
90
+ * Output configuration for the response body.
91
+ * @public
92
+ */
93
+ export interface HttpOutput {
94
+ /**
95
+ * Remote path to save the response body to.
96
+ * If set, the module writes to a temporary file and atomically moves it into place.
97
+ */
98
+ toFile?: string
99
+ /**
100
+ * Additionally capture the response body in the module result (body field).
101
+ * Note: may increase memory usage for large responses.
102
+ */
103
+ captureBody?: boolean
104
+ }
105
+
106
+ /**
107
+ * Options for the http module.
108
+ * @public
109
+ */
110
+ export interface HttpModuleOptions {
111
+ /**
112
+ * Target URL
113
+ */
114
+ url: string | URL
115
+ /**
116
+ * HTTP method to use.
117
+ * @defaultValue "GET"
118
+ *
119
+ * Special handling:
120
+ * - When method is "HEAD" and writing to file, uses `-X HEAD`.
121
+ * - When method is "HEAD" and not writing to file, uses `-I`.
122
+ */
123
+ method?: HttpMethod
124
+ /**
125
+ * Additional HTTP headers to send.
126
+ * @example
127
+ * ```ts
128
+ * headers: { "Accept": "application/json", "User-Agent": "katmer" }
129
+ * ```
130
+ */
131
+ headers?: Record<string, string>
132
+ /**
133
+ * Key-value query parameters to append to the URL.
134
+ * Null or undefined values are skipped.
135
+ */
136
+ query?: Record<string, string | number | boolean | null | undefined>
137
+ /**
138
+ * Request body.
139
+ * - If an object is provided, it is JSON-encoded and `Content-Type: application/json` is added unless already set.
140
+ * - If a string is provided, it is sent via `--data-binary "string"`.
141
+ * - For binary payloads, prefer {@link HttpModuleOptions.bodyFile | bodyFile}.
142
+ */
143
+ body?: string | Record<string, any> | Uint8Array
144
+ /**
145
+ * Remote path to a file whose content will be sent via `--data-binary @[file]`.
146
+ */
147
+ bodyFile?: string
148
+ /**
149
+ * Authentication configuration (basic or bearer).
150
+ */
151
+ auth?: HttpAuth
152
+ /**
153
+ * Total request timeout in seconds.
154
+ * Maps to: `--max-time [seconds]`
155
+ * @defaultValue 30
156
+ */
157
+ timeout?: number
158
+ /**
159
+ * Follow redirects.
160
+ * Maps to: `-L`
161
+ * @defaultValue true
162
+ */
163
+ follow_redirects?: boolean
164
+ /**
165
+ * Validate TLS certificates. If false, passes `--insecure`.
166
+ * @defaultValue true
167
+ */
168
+ validate_certs?: boolean
169
+ /**
170
+ * Output configuration. A string is shorthand for `output.toFile`.
171
+ */
172
+ output?: HttpOutput | string
173
+ /**
174
+ * Remote path to save response headers.
175
+ * Maps to: `-D [file]`
176
+ */
177
+ save_headers_to?: string
178
+ /**
179
+ * Treat non-2xx HTTP codes as fatal.
180
+ * Maps to: `--fail-with-body`
181
+ * @defaultValue true
182
+ */
183
+ fail_on_http_error?: boolean
184
+ /**
185
+ * Retry settings.
186
+ */
187
+ retry?: HttpRetry
188
+ /**
189
+ * Additional raw arguments to append to the curl command. Only works with Local provider
190
+ */
191
+ extra_args?: string[]
192
+ /**
193
+ * File mode to set on output file (when `output.toFile` is set).
194
+ * Accepts octal number (e.g., `0o644`) or string (e.g., `"0644"`).
195
+ */
196
+ mode?: string | number
197
+ /**
198
+ * File owner to set on the output file (`chown`).
199
+ */
200
+ owner?: string
201
+ /**
202
+ * File group to set on the output file (`chown`).
203
+ */
204
+ group?: string
205
+ }
206
+
207
+ /**
208
+ * Result returned by the http module.
209
+ * @public
210
+ */
211
+ export interface HttpModuleResult extends ModuleCommonReturn {
212
+ /**
213
+ * Parsed whatwg-url URL object.
214
+ */
215
+ url: URL
216
+ /**
217
+ * Parsed HTTP status code when available (best-effort).
218
+ */
219
+ status?: number
220
+ /**
221
+ * Raw response headers if saved via {@link HttpModuleOptions.save_headers_to | save_headers_to}.
222
+ */
223
+ headers?: Record<string, string | string[]>
224
+ /**
225
+ * Response body if `captureBody=true`, or when output is not used and `captureBody=true`.
226
+ */
227
+ body?: string
228
+ /**
229
+ * Destination path when `output.toFile` (or string shorthand) was used.
230
+ */
231
+ dest?: string
232
+ }
233
+
234
+ /**
235
+ * Execute HTTP(S) requests.
236
+ *
237
+ *
238
+ * @remarks
239
+ * - Uses `curl` when running with ssh provider
240
+ * - Follows redirects by default (`-L`).
241
+ * - Validates TLS by default; set {@link HttpModuleOptions.validate_certs | validate_certs}: false to pass `--insecure`.
242
+ * - When {@link HttpModuleOptions.output | output.toFile} is set, writes to a temporary file then moves atomically; `changed=true` on success.
243
+ * - If {@link HttpModuleOptions.fail_on_http_error | fail_on_http_error} is true (default), non-2xx responses cause a failure using `--fail-with-body`.
244
+ * - Best-effort status parsing is performed from saved headers or stderr when available.
245
+ *
246
+ * @examples
247
+ * ```yaml
248
+ * - name: Download a file to a path (like get_url)
249
+ * http:
250
+ * url: "https://example.com/app.tar.gz"
251
+ * output: "/opt/app/app.tar.gz"
252
+ * mode: "0644"
253
+ *
254
+ * - name: GET JSON with headers and save response headers
255
+ * http:
256
+ * url: "https://api.example.com/meta"
257
+ * headers:
258
+ * Accept: "application/json"
259
+ * save_headers_to: "/tmp/meta.headers"
260
+ * output:
261
+ * toFile: "/tmp/meta.json"
262
+ * captureBody: true
263
+ *
264
+ * - name: POST JSON with bearer token
265
+ * http:
266
+ * url: "https://api.example.com/resources"
267
+ * method: "POST"
268
+ * headers:
269
+ * Accept: "application/json"
270
+ * body:
271
+ * name: "demo"
272
+ * enabled: true
273
+ * auth:
274
+ * type: "bearer"
275
+ * token: "{{ MY_API_TOKEN }}"
276
+ * fail_on_http_error: true
277
+ *
278
+ * - name: Download with basic auth
279
+ * http:
280
+ * url: "https://intranet.local/file.bin"
281
+ * auth:
282
+ * type: "basic"
283
+ * username: "user"
284
+ * password: "pass"
285
+ * validate_certs: false
286
+ * retry:
287
+ * tries: 5
288
+ * delay: 2
289
+ * ```
290
+ * @public
291
+ */
292
+ export function HttpModule(opts: HttpModuleOptions, provider: KatmerProvider) {
293
+ if (provider instanceof SSHProvider) {
294
+ return new HTTPCurlModule(opts, provider)
295
+ } else {
296
+ return new HTTPLocalModule(opts, provider)
297
+ }
298
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./apt.module"
2
+ export * from "./apt-repository/apt-repository.module"
3
+ export * from "./archive.module"
4
+ export * from "./become.module"
5
+ export * from "./copy.module"
6
+ export * from "./gather_facts.module"
7
+ export * from "./git.module"
8
+ export * from "./debug.module"
9
+ export * from "./http/http.module"
10
+ export * from "./hostname.module"
11
+ export * from "./package.module"
12
+ export * from "./script.module"
13
+ export * from "./set_fact.module"
14
+ export * from "./template.module"
@@ -0,0 +1,283 @@
1
+ import {
2
+ type ModuleCommonReturn,
3
+ type ModuleConstraints
4
+ } from "../interfaces/module.interface"
5
+ import type { Katmer } from "../interfaces/task.interface"
6
+ import type { KatmerProvider } from "../interfaces/provider.interface"
7
+ import { KatmerModule } from "../module"
8
+
9
+ /**
10
+ * Unified package management module.
11
+ *
12
+ * @remarks
13
+ * This module provides a **single, portable interface** for installing,
14
+ * updating, or removing packages across different operating systems
15
+ * and package managers.
16
+ *
17
+ * Supported package managers:
18
+ *
19
+ * - **Linux**: apt, dnf, yum, pacman, apk, zypper
20
+ * - **macOS**: brew
21
+ * - **Windows**: winget, choco
22
+ *
23
+ * The module automatically:
24
+ * - Detects the target OS via {@link KatmerProvider.os}
25
+ * - Probes for available package managers
26
+ * - Selects the most appropriate one
27
+ *
28
+ * @examples
29
+ * ```yaml
30
+ * - name: Install curl
31
+ * package:
32
+ * name: curl
33
+ *
34
+ * - name: Ensure git is removed
35
+ * package:
36
+ * name: git
37
+ * state: absent
38
+ *
39
+ * - name: Upgrade docker
40
+ * package:
41
+ * name: docker
42
+ * state: latest
43
+ *
44
+ * - name: Install multiple packages
45
+ * package:
46
+ * name:
47
+ * - curl
48
+ * - git
49
+ * - jq
50
+ * ```
51
+ */
52
+ export class PackageModule extends KatmerModule<
53
+ PackageModuleOptions,
54
+ PackageModuleResult
55
+ > {
56
+ static name = "package" as const
57
+
58
+ constraints = {
59
+ platform: {
60
+ any: true
61
+ }
62
+ } satisfies ModuleConstraints
63
+
64
+ async check(): Promise<void> {
65
+ const o = normalizeOptions(this.params)
66
+ if (!o.name || (Array.isArray(o.name) && o.name.length === 0)) {
67
+ throw new Error("package: 'name' is required")
68
+ }
69
+ }
70
+
71
+ async initialize(): Promise<void> {}
72
+ async cleanup(): Promise<void> {}
73
+
74
+ async execute(ctx: Katmer.TaskContext): Promise<PackageModuleResult> {
75
+ const o = normalizeOptions(this.params)
76
+ const names = Array.isArray(o.name) ? o.name : [o.name]
77
+
78
+ const pm = await detectPackageManager(ctx)
79
+ if (!pm) {
80
+ return {
81
+ changed: false,
82
+ failed: true,
83
+ msg: "No supported package manager detected on target"
84
+ }
85
+ }
86
+
87
+ const cmd = buildCommand(pm, o.state, names)
88
+ if (!cmd) {
89
+ return {
90
+ changed: false,
91
+ failed: true,
92
+ msg: `Unsupported operation for package manager: ${pm}`
93
+ }
94
+ }
95
+
96
+ const r = await ctx.execSafe(cmd)
97
+
98
+ return {
99
+ changed: r.code === 0,
100
+ failed: r.code !== 0,
101
+ stdout: r.stdout,
102
+ stderr: r.stderr,
103
+ manager: pm
104
+ }
105
+ }
106
+ }
107
+
108
+ /* ───────────────────────────────────────────────────────────── */
109
+ /* Options & Result Types */
110
+ /* ───────────────────────────────────────────────────────────── */
111
+
112
+ /**
113
+ * Options for the {@link PackageModule | `package`} module.
114
+ *
115
+ * @public
116
+ */
117
+ export type PackageModuleOptions =
118
+ | string
119
+ | {
120
+ /**
121
+ * Package name or list of packages.
122
+ */
123
+ name: string | string[]
124
+
125
+ /**
126
+ * Desired state of the package(s).
127
+ *
128
+ * - `present`: ensure installed (default)
129
+ * - `absent`: ensure removed
130
+ * - `latest`: upgrade to latest version
131
+ *
132
+ * @defaultValue "present"
133
+ */
134
+ state?: "present" | "absent" | "latest"
135
+ }
136
+
137
+ /**
138
+ * Result returned by the {@link PackageModule | `package`} module.
139
+ *
140
+ * @public
141
+ */
142
+ export interface PackageModuleResult extends ModuleCommonReturn {
143
+ /** Package manager that was used */
144
+ manager?: PackageManager
145
+ }
146
+
147
+ /* ───────────────────────────────────────────────────────────── */
148
+ /* Task context augmentation */
149
+ /* ───────────────────────────────────────────────────────────── */
150
+
151
+ declare module "../interfaces/task.interface" {
152
+ export namespace Katmer {
153
+ export interface TaskActions {
154
+ package?: PackageModuleOptions
155
+ }
156
+ }
157
+ }
158
+
159
+ /* ───────────────────────────────────────────────────────────── */
160
+ /* Internals */
161
+ /* ───────────────────────────────────────────────────────────── */
162
+
163
+ type PackageManager =
164
+ | "apt"
165
+ | "dnf"
166
+ | "yum"
167
+ | "pacman"
168
+ | "apk"
169
+ | "zypper"
170
+ | "brew"
171
+ | "winget"
172
+ | "choco"
173
+
174
+ function normalizeOptions(p: PackageModuleOptions): {
175
+ name: string | string[]
176
+ state: "present" | "absent" | "latest"
177
+ } {
178
+ if (typeof p === "string") {
179
+ return { name: p, state: "present" }
180
+ }
181
+ return {
182
+ name: p.name,
183
+ state: p.state ?? "present"
184
+ }
185
+ }
186
+
187
+ async function detectPackageManager(
188
+ ctx: Katmer.TaskContext
189
+ ): Promise<PackageManager | null> {
190
+ const fam = ctx.provider.os.family
191
+
192
+ const probes: Array<[PackageManager, string]> = []
193
+
194
+ if (fam === "linux") {
195
+ probes.push(
196
+ ["apt", "command -v apt-get"],
197
+ ["dnf", "command -v dnf"],
198
+ ["yum", "command -v yum"],
199
+ ["pacman", "command -v pacman"],
200
+ ["apk", "command -v apk"],
201
+ ["zypper", "command -v zypper"]
202
+ )
203
+ } else if (fam === "darwin") {
204
+ probes.push(["brew", "command -v brew"])
205
+ } else if (fam === "windows") {
206
+ probes.push(["winget", "where winget"], ["choco", "where choco"])
207
+ }
208
+
209
+ for (const [pm, probe] of probes) {
210
+ const r = await ctx.execSafe(probe)
211
+ if (r.code === 0) return pm
212
+ }
213
+
214
+ return null
215
+ }
216
+
217
+ function buildCommand(
218
+ pm: PackageManager,
219
+ state: "present" | "absent" | "latest",
220
+ pkgs: string[]
221
+ ): string | null {
222
+ const list = pkgs.join(" ")
223
+
224
+ switch (pm) {
225
+ case "apt":
226
+ if (state === "present")
227
+ return `apt-get update -y && apt-get install -y ${list}`
228
+ if (state === "latest")
229
+ return `apt-get update -y && apt-get install -y --only-upgrade ${list}`
230
+ return `apt-get remove -y ${list}`
231
+
232
+ case "dnf":
233
+ return (
234
+ state === "absent" ? `dnf remove -y ${list}`
235
+ : state === "latest" ? `dnf upgrade -y ${list}`
236
+ : `dnf install -y ${list}`
237
+ )
238
+
239
+ case "yum":
240
+ return (
241
+ state === "absent" ? `yum remove -y ${list}`
242
+ : state === "latest" ? `yum update -y ${list}`
243
+ : `yum install -y ${list}`
244
+ )
245
+
246
+ case "pacman":
247
+ return state === "absent" ?
248
+ `pacman -R --noconfirm ${list}`
249
+ : `pacman -S --noconfirm ${list}`
250
+
251
+ case "apk":
252
+ return state === "absent" ? `apk del ${list}` : `apk add ${list}`
253
+
254
+ case "zypper":
255
+ return state === "absent" ?
256
+ `zypper remove -y ${list}`
257
+ : `zypper install -y ${list}`
258
+
259
+ case "brew":
260
+ return (
261
+ state === "absent" ? `brew uninstall ${list}`
262
+ : state === "latest" ? `brew upgrade ${list}`
263
+ : `brew install ${list}`
264
+ )
265
+
266
+ case "winget":
267
+ return (
268
+ state === "absent" ? `winget uninstall --silent ${list}`
269
+ : state === "latest" ? `winget upgrade --silent ${list}`
270
+ : `winget install --silent ${list}`
271
+ )
272
+
273
+ case "choco":
274
+ return (
275
+ state === "absent" ? `choco uninstall -y ${list}`
276
+ : state === "latest" ? `choco upgrade -y ${list}`
277
+ : `choco install -y ${list}`
278
+ )
279
+
280
+ default:
281
+ return null
282
+ }
283
+ }
@@ -0,0 +1,121 @@
1
+ import {
2
+ type ModuleCommonReturn,
3
+ type ModuleConstraints
4
+ } from "../interfaces/module.interface"
5
+ import type { Katmer } from "../interfaces/task.interface"
6
+ import { evalTemplate } from "../utils/renderer/renderer"
7
+ import type { KatmerProvider } from "../interfaces/provider.interface"
8
+ import { KatmerModule } from "../module"
9
+
10
+ /**
11
+ * Execute an ad-hoc shell script on the target.
12
+ *
13
+ * @remarks
14
+ * - Accepts either a **string** (backwards compatible) or an **object** with a `content` string
15
+ * and a `render` flag to enable/disable template rendering.
16
+ * - When `render` is `true` (default), the script string is rendered with Twig against `ctx.variables`
17
+ * before execution. When `false`, the string is executed **as-is**.
18
+ * - Uses the provider's shell via {@link Katmer.TaskContext.exec | `ctx.exec`}.
19
+ * - Return semantics are simple: `changed` is always `false`; `failed` is set when the exit code is non-zero.
20
+ * - Standard output and error are surfaced as `stdout` and `stderr`.
21
+ *
22
+ * @examples
23
+ * ```yaml
24
+ * - name: Simple one-liner (rendering enabled by default):
25
+ * script: "echo Hello {{ env | default('world') }}"
26
+ *
27
+ * - name: Multi-line script (rendering enabled by default):
28
+ * script: |
29
+ * set -euo pipefail
30
+ * echo "cwd={{ cwd }}"
31
+ * ls -la
32
+ *
33
+ * - name: Disable templating (execute exactly the given text):
34
+ * script:
35
+ * content: "echo {{ literally-not-rendered }}"
36
+ * render: false
37
+ *
38
+ * - name: Conditional logic - restart service in prod
39
+ * when: env == 'prod'
40
+ * script: "systemctl restart myapp || true"
41
+ * ```
42
+ */
43
+ export class ScriptModule extends KatmerModule<
44
+ ScriptModuleOptions,
45
+ ScriptModuleResult,
46
+ KatmerProvider
47
+ > {
48
+ static name = "script" as const
49
+
50
+ constraints = {
51
+ platform: {
52
+ any: true
53
+ }
54
+ } satisfies ModuleConstraints
55
+
56
+ async check(_ctx: Katmer.TaskContext): Promise<void> {
57
+ const o = normalizeOptions(this.params)
58
+ if (!o.content || typeof o.content !== "string") {
59
+ throw new Error("script: 'content' must be a non-empty string")
60
+ }
61
+ }
62
+
63
+ async initialize(_ctx: Katmer.TaskContext): Promise<void> {}
64
+ async cleanup(_ctx: Katmer.TaskContext): Promise<void> {}
65
+
66
+ async execute(ctx: Katmer.TaskContext): Promise<ScriptModuleResult> {
67
+ const { content, render } = normalizeOptions(this.params)
68
+
69
+ const scriptText =
70
+ render ? await evalTemplate(content, ctx.variables) : content
71
+ const r = await ctx.exec(scriptText)
72
+
73
+ return {
74
+ failed: r.code !== undefined && r.code !== 0,
75
+ changed: false,
76
+ stdout: r.stdout,
77
+ stderr: r.stderr
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * You can pass a **raw string** which will be rendered using template engine,
84
+ * or an **object** to control rendering explicitly.
85
+ *
86
+ * @public
87
+ */
88
+ export type ScriptModuleOptions =
89
+ | string
90
+ | {
91
+ /** Inline script content to execute. */
92
+ content: string
93
+ /**
94
+ * Whether to render the script with Twig against `ctx.variables` before execution.
95
+ * @defaultValue true
96
+ */
97
+ render?: boolean
98
+ }
99
+
100
+ /**
101
+ * Result of the script execution.
102
+ *
103
+ * @public
104
+ */
105
+ export interface ScriptModuleResult extends ModuleCommonReturn {
106
+ // inherits: changed, failed?, skipped?, msg?, stdout?, stderr?
107
+ }
108
+
109
+ // ────────────────────────────────────────────────────────────────────────────────
110
+ // internals
111
+ // ────────────────────────────────────────────────────────────────────────────────
112
+
113
+ function normalizeOptions(p: ScriptModuleOptions): {
114
+ content: string
115
+ render: boolean
116
+ } {
117
+ if (typeof p === "string") return { content: p, render: true }
118
+ const content = p?.content ?? ""
119
+ const render = p?.render ?? true
120
+ return { content, render }
121
+ }