@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,363 @@
|
|
|
1
|
+
import { get } from "es-toolkit/compat"
|
|
2
|
+
import { parseLines } from "../../utils/string.utils"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import type { Katmer } from "../../interfaces/task.interface"
|
|
5
|
+
import type { SSHProvider } from "../../providers/ssh/ssh.provider"
|
|
6
|
+
import { UnixComms } from "../../utils/unix.utils"
|
|
7
|
+
|
|
8
|
+
type SourceEntry = {
|
|
9
|
+
valid: boolean
|
|
10
|
+
enabled: boolean
|
|
11
|
+
source: string
|
|
12
|
+
comment: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const VALID_SOURCE_TYPES = new Set(["deb", "deb-src"])
|
|
16
|
+
|
|
17
|
+
export class InvalidSource extends Error {
|
|
18
|
+
constructor(line: string) {
|
|
19
|
+
super(`Invalid or disabled APT source: ${line}`)
|
|
20
|
+
this.name = "InvalidSource"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SourcesList {
|
|
25
|
+
files: Record<string, (SourceEntry & { n: number })[]> = {}
|
|
26
|
+
files_map: Record<string, string> = {}
|
|
27
|
+
new_repos = new Set<string>()
|
|
28
|
+
default_file!: string
|
|
29
|
+
sources_dir!: string
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private aptConfig: Record<string, any>,
|
|
33
|
+
private ctx: Katmer.TaskContext<SSHProvider>
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
async init() {
|
|
37
|
+
const rootDir = this.aptConfig["Dir"]
|
|
38
|
+
const aptDir = this.aptConfig["Dir::Etc"]
|
|
39
|
+
const sourcesDir = this.aptConfig["Dir::Etc::sourceparts"]
|
|
40
|
+
const sourcesList = this.aptConfig["Dir::Etc::sourcelist"]
|
|
41
|
+
|
|
42
|
+
this.default_file = path.posix.join(rootDir, aptDir, sourcesList)
|
|
43
|
+
if (await UnixComms.pathIsFile(this.ctx, this.default_file)) {
|
|
44
|
+
await this.load(this.default_file)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.sources_dir = path.posix.join(rootDir, aptDir, sourcesDir)
|
|
48
|
+
const { stdout } = await this.ctx.exec(
|
|
49
|
+
`bash -lc 'shopt -s nullglob; for f in "${this.sources_dir}"/*.list; do echo "$f"; done'`
|
|
50
|
+
)
|
|
51
|
+
const files = stdout
|
|
52
|
+
.split("\n")
|
|
53
|
+
.map((s) => s.trim())
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
if (await UnixComms.pathIsSymlink(this.ctx, file)) {
|
|
58
|
+
const link = await UnixComms.readlink(this.ctx, file)
|
|
59
|
+
if (link) this.files_map[file] = link
|
|
60
|
+
}
|
|
61
|
+
await this.load(file)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async load(sourcesFile: string): Promise<void> {
|
|
66
|
+
const group: (SourceEntry & { n: number })[] = []
|
|
67
|
+
const fileContents = await UnixComms.readFileUtf8(this.ctx, sourcesFile)
|
|
68
|
+
if (fileContents) {
|
|
69
|
+
const lines = parseLines(fileContents)
|
|
70
|
+
for (let i = 0; i < lines.length; i++) {
|
|
71
|
+
const res = this._parse_source_line(lines[i])
|
|
72
|
+
group.push({ n: i, ...res })
|
|
73
|
+
}
|
|
74
|
+
this.files[sourcesFile] = group
|
|
75
|
+
} else {
|
|
76
|
+
this.files[sourcesFile] = []
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async save(): Promise<void> {
|
|
81
|
+
// Build desired contents
|
|
82
|
+
const desired: Record<string, string> = {}
|
|
83
|
+
for (const [filename, sources] of Object.entries(this.files)) {
|
|
84
|
+
if (!sources || sources.length === 0) continue
|
|
85
|
+
const lines: string[] = []
|
|
86
|
+
for (const { enabled, source, comment } of sources) {
|
|
87
|
+
const chunks: string[] = []
|
|
88
|
+
if (!enabled) chunks.push("# ")
|
|
89
|
+
chunks.push(source)
|
|
90
|
+
if (comment) {
|
|
91
|
+
chunks.push(" # ")
|
|
92
|
+
chunks.push(comment)
|
|
93
|
+
}
|
|
94
|
+
chunks.push("\n")
|
|
95
|
+
lines.push(chunks.join(""))
|
|
96
|
+
}
|
|
97
|
+
desired[filename] = lines.join("")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Read current contents for comparison
|
|
101
|
+
const current: Record<string, string> = {}
|
|
102
|
+
const filenames = new Set<string>([
|
|
103
|
+
...Object.keys(this.files),
|
|
104
|
+
...Object.keys(this.files_map)
|
|
105
|
+
])
|
|
106
|
+
for (const filename of filenames) {
|
|
107
|
+
const target = this.files_map[filename] ?? filename
|
|
108
|
+
current[filename] = (await UnixComms.readFileUtf8(this.ctx, target)) ?? ""
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Write only changed files
|
|
112
|
+
for (const [filename, sources] of Object.entries(this.files)) {
|
|
113
|
+
const target = this.files_map[filename] ?? filename
|
|
114
|
+
const want = desired[filename] ?? ""
|
|
115
|
+
const have = current[filename] ?? ""
|
|
116
|
+
|
|
117
|
+
if (sources && sources.length > 0) {
|
|
118
|
+
if (want === have) continue
|
|
119
|
+
|
|
120
|
+
const dir = filename.substring(0, filename.lastIndexOf("/")) || "/"
|
|
121
|
+
await UnixComms.mkdirp(this.ctx, dir)
|
|
122
|
+
|
|
123
|
+
const modeRaw = get(this.aptConfig, "mode") as
|
|
124
|
+
| number
|
|
125
|
+
| string
|
|
126
|
+
| undefined
|
|
127
|
+
const numeric =
|
|
128
|
+
modeRaw !== undefined ?
|
|
129
|
+
typeof modeRaw === "number" ?
|
|
130
|
+
modeRaw
|
|
131
|
+
: parseInt(String(modeRaw), 8)
|
|
132
|
+
: undefined
|
|
133
|
+
|
|
134
|
+
await UnixComms.writeFileAtomic(
|
|
135
|
+
this.ctx,
|
|
136
|
+
target,
|
|
137
|
+
want,
|
|
138
|
+
Number.isNaN(numeric as any) ? undefined : numeric
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Remove only files that became empty (and only if they existed or are mapped)
|
|
144
|
+
for (const [filename, sources] of Object.entries(this.files)) {
|
|
145
|
+
if (sources && sources.length > 0) continue
|
|
146
|
+
delete this.files[filename]
|
|
147
|
+
await UnixComms.removePath(this.ctx, filename)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
modify(
|
|
152
|
+
file: string,
|
|
153
|
+
n: number,
|
|
154
|
+
enabled?: boolean,
|
|
155
|
+
source?: string,
|
|
156
|
+
comment?: string
|
|
157
|
+
): void {
|
|
158
|
+
const current = this.files[file]?.[n]
|
|
159
|
+
if (!current) return
|
|
160
|
+
const valid = current.valid
|
|
161
|
+
const enabledOld = current.enabled
|
|
162
|
+
const sourceOld = current.source
|
|
163
|
+
const commentOld = current.comment
|
|
164
|
+
this.files[file][n] = {
|
|
165
|
+
n,
|
|
166
|
+
valid,
|
|
167
|
+
enabled: this._choice(enabled, enabledOld),
|
|
168
|
+
source: this._choice(source, sourceOld),
|
|
169
|
+
comment: this._choice(comment, commentOld)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
add_source(line: string, comment = "", file?: string | null) {
|
|
174
|
+
const { source } = this._parse_source_line(line, true)
|
|
175
|
+
const suggested = this._suggest_filename(source)
|
|
176
|
+
this._add_valid_source(source, comment, file || suggested)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
remove_source(line?: string, regexp?: string) {
|
|
180
|
+
if (regexp) {
|
|
181
|
+
this._remove_valid_source(undefined, regexp)
|
|
182
|
+
} else if (line) {
|
|
183
|
+
const { source } = this._parse_source_line(line, true)
|
|
184
|
+
this._remove_valid_source(source)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
dump(): Record<string, string> {
|
|
189
|
+
const out: Record<string, string> = {}
|
|
190
|
+
for (const [filename, sources] of Object.entries(this.files)) {
|
|
191
|
+
if (!sources || sources.length === 0) continue
|
|
192
|
+
const lines: string[] = []
|
|
193
|
+
for (const { enabled, source, comment } of sources) {
|
|
194
|
+
const chunks: string[] = []
|
|
195
|
+
if (!enabled) chunks.push("# ")
|
|
196
|
+
chunks.push(source)
|
|
197
|
+
if (comment) {
|
|
198
|
+
chunks.push(" # ")
|
|
199
|
+
chunks.push(comment)
|
|
200
|
+
}
|
|
201
|
+
chunks.push("\n")
|
|
202
|
+
lines.push(chunks.join(""))
|
|
203
|
+
}
|
|
204
|
+
out[filename] = lines.join("")
|
|
205
|
+
}
|
|
206
|
+
return out
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
protected _parse_source_line(
|
|
210
|
+
lineIn: string,
|
|
211
|
+
raiseIfInvalidOrDisabled = false
|
|
212
|
+
): SourceEntry {
|
|
213
|
+
let valid = false
|
|
214
|
+
let enabled = true
|
|
215
|
+
let source = ""
|
|
216
|
+
let comment = ""
|
|
217
|
+
|
|
218
|
+
let line = lineIn.trim()
|
|
219
|
+
if (line.startsWith("#")) {
|
|
220
|
+
enabled = false
|
|
221
|
+
line = line.slice(1)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const hashIdx = line.indexOf("#")
|
|
225
|
+
if (hashIdx > 0) {
|
|
226
|
+
comment = line.slice(hashIdx + 1).trim()
|
|
227
|
+
line = line.slice(0, hashIdx)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
source = line.trim()
|
|
231
|
+
if (source) {
|
|
232
|
+
const chunks = source.split(/\s+/).filter(Boolean)
|
|
233
|
+
if (chunks.length > 0 && VALID_SOURCE_TYPES.has(chunks[0])) {
|
|
234
|
+
valid = true
|
|
235
|
+
source = chunks.join(" ")
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (raiseIfInvalidOrDisabled && (!valid || !enabled)) {
|
|
240
|
+
throw new InvalidSource(lineIn)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { valid, enabled, source, comment }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
protected _expand_path(filename: string): string {
|
|
247
|
+
filename = filename.endsWith(".list") ? filename : `${filename}.list`
|
|
248
|
+
if (filename.includes("/")) return filename
|
|
249
|
+
return `${this.sources_dir.replace(/\/+$/, "")}/${filename}`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
protected _suggest_filename(
|
|
253
|
+
line: string,
|
|
254
|
+
params?: { filename?: string }
|
|
255
|
+
): string {
|
|
256
|
+
const cleanupFilename = (s: string) => {
|
|
257
|
+
const explicit = params?.filename
|
|
258
|
+
if (explicit != null) return explicit
|
|
259
|
+
return s
|
|
260
|
+
.replace(/[^a-zA-Z0-9]/g, " ")
|
|
261
|
+
.trim()
|
|
262
|
+
.split(/\s+/)
|
|
263
|
+
.join("_")
|
|
264
|
+
}
|
|
265
|
+
const stripUserPass = (s: string) => {
|
|
266
|
+
if (s.includes("@")) {
|
|
267
|
+
const parts = s.split("@")
|
|
268
|
+
return parts[parts.length - 1]
|
|
269
|
+
}
|
|
270
|
+
return s
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let work = line.replace(/\[[^\]]+\]/g, "")
|
|
274
|
+
work = work.replace(/\w+:\/\//g, "")
|
|
275
|
+
|
|
276
|
+
const parts = work
|
|
277
|
+
.split(/\s+/)
|
|
278
|
+
.filter((p) => p && !VALID_SOURCE_TYPES.has(p))
|
|
279
|
+
|
|
280
|
+
if (parts.length > 0) {
|
|
281
|
+
parts[0] = stripUserPass(parts[0])
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const base = cleanupFilename(parts.slice(0, 1).join(" "))
|
|
285
|
+
return `${base}.list`
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
protected _choice<T>(n: T | undefined | null, old: T): T {
|
|
289
|
+
return n == null ? old : n
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
protected _add_valid_source(
|
|
293
|
+
source_new: string,
|
|
294
|
+
comment_new: string,
|
|
295
|
+
file?: string | null
|
|
296
|
+
) {
|
|
297
|
+
let found = false
|
|
298
|
+
for (const [filename, n, _enabled, src] of this) {
|
|
299
|
+
if (src === source_new) {
|
|
300
|
+
this.modify(filename, n, true)
|
|
301
|
+
found = true
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!found) {
|
|
306
|
+
let targetFile: string
|
|
307
|
+
if (!file) {
|
|
308
|
+
targetFile = this.default_file
|
|
309
|
+
} else {
|
|
310
|
+
targetFile = this._expand_path(file)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!this.files[targetFile]) {
|
|
314
|
+
this.files[targetFile] = []
|
|
315
|
+
}
|
|
316
|
+
const list = this.files[targetFile]
|
|
317
|
+
list.push({
|
|
318
|
+
n: list.length,
|
|
319
|
+
valid: true,
|
|
320
|
+
enabled: true,
|
|
321
|
+
source: source_new,
|
|
322
|
+
comment: comment_new
|
|
323
|
+
})
|
|
324
|
+
this.new_repos.add(targetFile)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
protected _remove_valid_source(source: string | undefined, regexp?: string) {
|
|
329
|
+
for (const [filename, n, enabled, src] of this) {
|
|
330
|
+
if (enabled) {
|
|
331
|
+
if (src === source || (regexp && new RegExp(regexp).test(src))) {
|
|
332
|
+
this.files[filename].splice(n, 1)
|
|
333
|
+
this.files[filename] = this.files[filename].map((e, idx) => ({
|
|
334
|
+
...e,
|
|
335
|
+
n: idx
|
|
336
|
+
}))
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
*[Symbol.iterator](): IterableIterator<
|
|
343
|
+
[file: string, n: number, enabled: boolean, source: string, comment: string]
|
|
344
|
+
> {
|
|
345
|
+
for (const [file, sources] of Object.entries(this.files)) {
|
|
346
|
+
for (const entry of sources) {
|
|
347
|
+
if (entry.valid) {
|
|
348
|
+
yield [file, entry.n, entry.enabled, entry.source, entry.comment]
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
toJSON() {
|
|
355
|
+
return {
|
|
356
|
+
files: this.files,
|
|
357
|
+
files_map: this.files_map,
|
|
358
|
+
default_file: this.default_file,
|
|
359
|
+
sources_dir: this.sources_dir,
|
|
360
|
+
new_repos: Array.from(this.new_repos)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|