hostctl 0.1.40 → 0.1.41

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 CHANGED
@@ -1,1039 +1,92 @@
1
1
  # hostctl
2
2
 
3
- `hostctl` is a modern, lightweight alternative to tools like Ansible, designed with simplicity and ease of use at its core. The primary goal of `hostctl` is to provide a straightforward and consistent way to execute scripts on both local and remote hosts. It aims for portability across all POSIX-style systems.
4
-
5
- For detailed information on Hostctl's architecture and design, please see the [ARCHITECTURE.md](ARCHITECTURE.md) document.
6
- For notes on developing and contributing to Hostctl, refer to the [CONTRIBUTING.md](CONTRIBUTING.md) document.
7
-
8
- ## Quick start
9
-
10
- ### Install age
11
-
12
- ```
13
- brew install age
14
-
15
- ```
16
-
17
- ### Generate keys
18
-
19
- ```
20
- ~
21
- ❯ mkdir -p ~/.hostctl/age
22
-
23
- ~
24
- cd .hostctl
25
-
26
- ~/.hostctl
27
- age-keygen -o age/johndoe.priv
28
- Public key: age1jx3ec0y5hctkfwfut4p8cj08ulcezqex8r8slwlwgwy8k2plx5ksdzy8ll
29
-
30
- ~/.hostctl
31
- age-keygen -y age/johndoe.priv
32
- age1jx3ec0y5hctkfwfut4p8cj08ulcezqex8r8slwlwgwy8k2plx5ksdzy8ll
33
-
34
- ~/.hostctl
35
- ❯ age-keygen -y age/johndoe.priv > age/johndoe.pub
36
-
37
- ~/.hostctl
38
- cat age/johndoe.priv
39
- # created: 2025-05-24T14:24:12-05:00
40
- # public key: age1jx3ec0y5hctkfwfut4p8cj08ulcezqex8r8slwlwgwy8k2plx5ksdzy8ll
41
- AGE-SECRET-KEY-1G403WGS9Z599V2NMK923LWCUFJGRR6LXZ72A3UT95U7AMM5SADESZH7M7P
42
-
43
- ~/.hostctl
44
- ❯ cat age/johndoe.pub
45
- age1jx3ec0y5hctkfwfut4p8cj08ulcezqex8r8slwlwgwy8k2plx5ksdzy8ll
46
-
47
- ~/.hostctl
48
- age-keygen -o age/janedoe.priv
49
- Public key: age1qdqv054hmfmnxk56dkjm3cq8qz4lxfl7tvqfp0rqdl0nyygjlccqw0ly40
50
-
51
- ~/.hostctl
52
- age-keygen -y age/janedoe.priv > age/janedoe.pub
53
-
54
- ~/.hostctl
55
- cat age/janedoe.priv
56
- # created: 2025-05-24T14:26:36-05:00
57
- # public key: age1qdqv054hmfmnxk56dkjm3cq8qz4lxfl7tvqfp0rqdl0nyygjlccqw0ly40
58
- AGE-SECRET-KEY-1M4P4NQY8HEUGRJMWSPQ35J5W2SMUR7VLQULYX7HQ639C6XX8NGPQQ8Z5M7
59
-
60
- ~/.hostctl
61
- ❯ cat age/janedoe.pub
62
- age1qdqv054hmfmnxk56dkjm3cq8qz4lxfl7tvqfp0rqdl0nyygjlccqw0ly40
63
- ```
64
-
65
- ### Create hostctl config file
66
-
67
- ```
68
- ~
69
- mkdir -p ~/.hostctl/age
70
-
71
- ~
72
- cd .hostctl
73
-
74
- ~/.hostctl
75
- cat <<EOF > hostctl.yaml
76
- hosts:
77
- debian-vm:
78
- hostname: 192.168.56.11
79
- user: vagrant
80
- password: !secret vagrant-password
81
- tags: [debian, testvm]
82
-
83
- ubuntu-vm:
84
- hostname: 192.168.56.15
85
- user: vagrant
86
- password: !secret vagrant-password
87
- tags: [ubuntu, testvm]
88
-
89
- secrets:
90
- vagrant-password:
91
- ids: johndoe, janedoe
92
- value: vagrant
93
-
94
- ids:
95
- johndoe: age1jx3ec0y5hctkfwfut4p8cj08ulcezqex8r8slwlwgwy8k2plx5ksdzy8ll
96
- janedoe: age1qdqv054hmfmnxk56dkjm3cq8qz4lxfl7tvqfp0rqdl0nyygjlccqw0ly40
97
- EOF
98
-
99
- ~/.hostctl
100
- ❯ tree
101
- .
102
- ├── age
103
- │   ├── janedoe.priv
104
- │   ├── janedoe.pub
105
- │   ├── johndoe.priv
106
- │   └── johndoe.pub
107
- └── hostctl.yaml
108
-
109
- 2 directories, 5 files
110
- ```
111
-
112
- #### Host Configuration Details
113
-
114
- When defining hosts in your `hostctl.yaml`:
115
-
116
- - The **key** for each entry under the `hosts:` map **is** the network identifier (hostname or IP address) for that host. This key is directly used as the `hostname` for connection and identification purposes.
117
-
118
- For example:
119
-
120
- ```yaml
121
- hosts:
122
- # The key '192.168.56.11' is the hostname.
123
- 192.168.56.11:
124
- alias: debian1 # An optional alias for convenience
125
- user: vagrant
126
- # Any 'hostname: some.other.address' field here would be ignored for connection.
127
-
128
- # The key 'my-web-server' is treated as the hostname.
129
- # Ensure 'my-web-server' is resolvable if it's not an IP.
130
- my-web-server:
131
- user: admin
132
- ```
133
-
134
- The `Host` object within `hostctl` stores this key as its `hostname` property. When the configuration is saved (e.g., after decryption or other modifications), this `hostname` (which was the original key) will be written to an explicit `hostname` field in the YAML output for that host entry. This ensures that even if the key was, for example, an IP address, it's preserved and clearly identifiable in the output YAML.
135
-
136
- ### Run a task
137
-
138
- The `hostctl run <script_path> [params...]` command executes the specified script on your local machine. Within the script, you can use the `context.ssh(...)` method to connect to remote hosts defined in your inventory and perform operations on them.
139
-
140
- export AGE_IDS="~/.hostctl/age/johndoe.priv ~/.hostctl/age/janedoe.priv"
141
-
142
- hostctl on  HEAD (dd3d7ff) [?] is 📦 v0.1.31 via 🥟 v1.2.10 via 🦕 via  v23.11.0 took 13s
143
- ❯ hostctl run example/reachable.ts
144
- run: /home/david/sync/projects/monopod/hostctl/example/reachable.ts {}
145
- ✔ Enter your password
146
- === Evaluation ===
147
- ✓ Reachable
148
- ✓ echo foo,bar,baz
149
- ✓ echo foo,bar,baz
150
- ✓ os
151
- ✓ echo 1,1,1
152
- ✓ echo 2,2,2
153
- ✓ echo 3,3,4
154
- === Result ===
155
- { value: 'foo' }
156
-
157
- hostctl on  HEAD (dd3d7ff) [?] is 📦 v0.1.31 via 🥟 v1.2.10 via 🦕 via  v23.11.0 took 17s
158
- ❯ hostctl run example/echo.ts args:foo,bar
159
- run: /home/david/sync/projects/monopod/hostctl/example/echo.ts { args: [ 'foo', 'bar' ] }
160
- === Evaluation ===
161
- ✓ echo foo,bar
162
- === Result ===
163
- undefined
164
-
165
- ```
166
-
167
- ### Encrypt and decrypt inventory
168
-
169
-
170
- ```
171
-
172
- hostctl on  main [!] is 📦 v0.1.31 via 🥟 v1.2.10 via  v23.11.0
173
- ❯ hostctl inventory -c example/hostctl.yaml encrypt
174
- Encrypting inventory file: example/hostctl.yaml
175
-
176
- hostctl on  main [!] is 📦 v0.1.31 via 🥟 v1.2.10 via  v23.11.0
177
- ❯ cat example/hostctl.yaml
178
- hosts:
179
- debian-vm:
180
- user: vagrant
181
- password: !secret vagrant-password
182
- tags: - debian - test - example - debian-vm
183
- ubuntu-vm:
184
- user: vagrant
185
- password: !secret vagrant-password
186
- tags: - ubuntu - test - example - ubuntu-vm
187
- secrets:
188
- vagrant-password:
189
- ids: - johndoe - janedoe
190
- value: |
191
- -----BEGIN AGE ENCRYPTED FILE-----
192
- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRc0NING5YRlpJdFFoQjJS
193
- MGdzdVIzWElkMDBPM2RkNVE2ZUpucDhGVVN3CnpsdlpxQzJWaTRwSXl0WmxUT2tJ
194
- bER5SEk5Wm9IWmhTVXkrNFRpL3BsbzAKLT4gWDI1NTE5IFZ2OU94aThZQXJQNjl5
195
- ZEk1eWg0QWE5T2pKQVA2a1dvOC9FeXAvL3Q2RFEKbHpGUzJiU1dMNEV4OHFkd0pt
196
- dS9TSVEwRlU2ZW9mUnhNdk9JdmEwVGU1RQotLS0gdkM3VmwvYzNEejE4OUU2blJI
197
- d3NzVzZsREJ6Q3NjbEh2ai9qc3JxMGVOVQrgAqyjMEvOF2ifd02P/fr/AJzVKGdO
198
- qjcfnTSO6aztc8oLxqvvb+w=
199
- -----END AGE ENCRYPTED FILE-----
200
- ids:
201
- johndoe: - age1jx3ec0y5hctkfwfut4p8cj08ulcezqex8r8slwlwgwy8k2plx5ksdzy8ll
202
- janedoe: - age1qdqv054hmfmnxk56dkjm3cq8qz4lxfl7tvqfp0rqdl0nyygjlccqw0ly40
203
-
204
- hostctl on  main [!] is 📦 v0.1.31 via 🥟 v1.2.10 via  v23.11.0
205
- ❯ AGE_IDS="example/sample-age-ids/\*.priv" hostctl inventory -c example/hostctl.yaml decrypt -v
206
- Decrypting inventory file: example/hostctl.yaml
207
- Decrypted with one or more of the following private keys:
208
- example/sample-age-ids/johndoe.priv
209
- example/sample-age-ids/janedoe.priv
210
-
211
- hostctl on  main [!] is 📦 v0.1.31 via 🥟 v1.2.10 via  v23.11.0
212
- ❯ cat example/hostctl.yaml
213
- hosts:
214
- debian-vm:
215
- user: vagrant
216
- password: !secret vagrant-password
217
- tags: - debian - test - example - debian-vm
218
- ubuntu-vm:
219
- user: vagrant
220
- password: !secret vagrant-password
221
- tags: - ubuntu - test - example - ubuntu-vm
222
- secrets:
223
- vagrant-password:
224
- ids: - johndoe - janedoe
225
- value: vagrant
226
- ids:
227
- johndoe: - age1jx3ec0y5hctkfwfut4p8cj08ulcezqex8r8slwlwgwy8k2plx5ksdzy8ll
228
- janedoe: - age1qdqv054hmfmnxk56dkjm3cq8qz4lxfl7tvqfp0rqdl0nyygjlccqw0ly40
229
-
230
- ````
231
-
232
- ## Vision
233
-
234
- The vision for `hostctl` is to be an Ansible replacement that is significantly easier to use, with simple and intuitive semantics. It empowers users to manage and interact with hosts efficiently, whether for development, operations, or personal use.
235
-
236
- ## Key Features
237
-
238
- * **Simplified Host Interaction:** Run scripts seamlessly on local or remote machines.
239
- * **Cross-Platform Portability:** Designed to work consistently across POSIX-compliant systems.
240
- * **Flexible Scripting:** Write `hostctl` scripts in JavaScript or TypeScript.
241
-
242
- ## Core Tasks
243
-
244
- `hostctl` includes a library of core tasks that can be used within your scripts to perform common system administration operations. These tasks are designed to be idempotent and portable across POSIX-compliant systems.
245
-
246
- ### File and Directory Management
247
- - **`core.dir.copy`**: Recursively copies a directory.
248
- - **`core.dir.create`**: Creates a directory.
249
- - **`core.dir.exists`**: Checks if a directory exists.
250
- - **`core.file.chgrp`**: Changes the group of a file.
251
- - **`core.file.chmod`**: Changes the mode of a file.
252
- - **`core.file.chown`**: Changes the owner of a file.
253
- - **`core.file.copy`**: Copies a file.
254
- - **`core.file.delete`**: Deletes a file.
255
- - **`core.file.edit`**: Edits a file in place.
256
- - **`core.file.exists`**: Checks if a file exists.
257
- - **`core.file.grep`**: Searches for a pattern in a file.
258
- - **`core.file.link`**: Creates a symbolic link.
259
- - **`core.file.touch`**: Creates an empty file.
260
-
261
- ### System and Host Management
262
- - **`core.host.hostname`**: Get or set the hostname.
263
- - **`core.host.info`**: Gathers information about the host (OS, architecture, etc.).
264
- - **`core.host.os`**: Provides OS-specific information.
265
- - **`core.hosts_file.add_entry`**: Adds an entry to `/etc/hosts`.
266
- - **`core.system.reboot`**: Reboots the system.
267
- - **`core.system.reboot_if_needed`**: Reboots the system if a reboot is required.
268
- - **`core.system.reboot_needed`**: Checks if a reboot is required.
269
- - **`core.system.shutdown`**: Shuts down the system.
270
-
271
- ### User and Group Management
272
- - **`core.group.create`**: Creates a user group.
273
- - **`core.group.delete`**: Deletes a user group.
274
- - **`core.group.exists`**: Checks if a user group exists.
275
- - **`core.group.list`**: Lists user groups.
276
- - **`core.group.modify`**: Modifies a user group.
277
- - **`core.user.add_groups`**: Adds a user to groups.
278
- - **`core.user.create`**: Creates a user.
279
- - **`core.user.delete`**: Deletes a user.
280
- - **`core.user.exists`**: Checks if a user exists.
281
- - **`core.user.get_gid`**: Gets the group ID of a user.
282
- - **`core.user.get_groups`**: Gets the groups a user belongs to.
283
- - **`core.user.get_uid`**: Gets the user ID of a user.
284
- - **`core.user.get_username`**: Gets the username of the current user.
285
- - **`core.user.home_dir`**: Gets the home directory of a user.
286
- - **`core.user.set_groups`**: Sets the groups for a user.
287
- - **`core.user.set_shell`**: Sets the login shell for a user.
288
- - **`core.whoami`**: Gets the current user's username.
289
-
290
- ### Package Management
291
- - **`core.pkg.info`**: Gathers information about a package.
292
- - **`core.pkg.install`**: Installs a package.
293
- - **`core.pkg.is_installed`**: Checks if a package is installed.
294
- - **`core.pkg.remove`**: Removes a package.
295
- - **`core.pkg.update`**: Updates a package.
296
-
297
- #### Abstract Package Management (Cross-Platform, Non-Interactive)
298
- - **`core.pkg.abstract-install`**: Install packages using auto-detected or specified package manager (non-interactive, no progress bars).
299
- - **`core.pkg.abstract-uninstall`**: Uninstall packages using auto-detected or specified package manager (non-interactive, no progress bars).
300
- - **`core.pkg.abstract-update`**: Update package sources using auto-detected or specified package manager (non-interactive, no progress bars).
301
- - **`core.pkg.abstract-upgrade`**: Upgrade packages using auto-detected or specified package manager (non-interactive, no progress bars).
302
- - **`core.pkg.abstract-search`**: Search for packages using auto-detected or specified package manager (non-interactive, no progress bars).
303
- - **`core.pkg.abstract-list`**: List installed packages using auto-detected or specified package manager (non-interactive, no progress bars).
304
- - **`core.pkg.abstract-clean`**: Clean package cache using auto-detected or specified package manager (non-interactive, no progress bars).
305
-
306
- **Supported Package Managers**: Apt, Apk, Yum, Dnf, Pacman, Zypper, Eopkg, Winget, Choco, Brew
307
-
308
- **Non-Interactive Features**: All abstract package management tasks run completely non-interactively with:
309
- - No user prompts or confirmations
310
- - No progress bars or interactive indicators
311
- - No color output or terminal formatting
312
- - Automatic confirmations via environment variables and command flags
313
- - Clean, machine-readable output suitable for CI/CD pipelines
314
-
315
- ### Service and Process Management
316
- - **`core.systemd.disable`**: Disables a systemd service.
317
- - **`core.systemd.enable`**: Enables a systemd service.
318
- - **`core.systemd.reload`**: Reloads a systemd service.
319
- - **`core.systemd.restart`**: Restarts a systemd service.
320
- - **`core.systemd.start`**: Starts a systemd service.
321
- - **`core.systemd.status`**: Gets the status of a systemd service.
322
- - **`core.systemd.stop`**: Stops a systemd service.
323
-
324
- ### Version Control
325
- - **`core.git.checkout`**: Checks out a git branch or commit.
326
- - **`core.git.clone`**: Clones a git repository.
327
- - **`core.git.pull`**: Pulls changes from a git repository.
328
-
329
- ### Networking and Firewall
330
- - **`core.ufw.deny`**: Denies traffic with UFW.
331
- - **`core.ufw.disable`**: Disables UFW.
332
- - **`core.ufw.enable`**: Enables UFW.
333
- - **`core.ufw.install`**: Installs UFW.
334
- - **`core.ufw.reload`**: Reloads UFW rules.
335
-
336
- ### Miscellaneous
337
- - **`core.echo`**: Prints a message.
338
- - **`core.remote.runAllRemote`**: Runs a task on all remote hosts.
339
- - **`core.ssh.copy_id`**: Copies an SSH key to a remote host.
340
- - **`core.sudoers.check`**: Checks the sudoers file for a user.
341
- - **`core.sudoers.grant-nopasswd`**: Grants passwordless sudo to a user.
342
- - **`core.template.write`**: Writes a file from a template.
343
- - **`core.k3s.k3sup-install`**: Installs k3s using k3sup.
344
-
345
- ## CLI Usage
346
-
347
- The `hostctl` command-line interface (CLI) is the primary way to interact with the tool.
348
-
349
- ### Global Options
350
-
351
- These options can be applied to most `hostctl` commands:
352
-
353
- * `-q, --quiet`: Reduces the verbosity of output. Can be specified multiple times (e.g., `-qq`) for even less output.
354
- * `-v, --verbose`: Increases the verbosity of output. Can be specified multiple times (e.g., `-vv`, `-vvv`) for more detailed logs. The default level typically shows warnings and errors.
355
- * `-c, --config <path>`: Specifies the path to the configuration file (e.g., `my-config.yaml`) or an HTTP/HTTPS URL for a remote configuration. Defaults to `./hostctl.yaml`.
356
- * `-o, --output <style>`: Sets the output style. Supported styles are `plain` (default, human-readable) and `json` (machine-readable).
357
- * `--json`: A convenient shortcut for `-o json`.
358
- * `--api`: Enables API mode, which implies JSON output and may include more structured data suitable for programmatic consumption.
359
- * `-p, --password`: If set, `hostctl` will prompt the user for a password (e.g., for `sudo` access on hosts) if a task requires it.
360
- * `-t, --tag <tags...>`: Filters the target hosts based on tags defined in the inventory. Only hosts matching all specified tags will be selected. For example, `-t webapp backend` or `-t region:europe`.
361
-
362
- ### Commands
363
-
364
- #### `run` (alias: `r`)
365
-
366
- This is the **default command**. It executes a `hostctl` script.
367
-
368
- **Usage:**
369
-
370
- ```bash
371
- hostctl run [package_or_bundle] [script] [script_arguments...]
372
- ````
373
-
374
- - **`package_or_bundle`** (optional):
375
- - A path to a local directory containing the script or package (e.g., `./scripts`, `/opt/myproject`).
376
- - A URL to a Git repository (e.g., `https://github.com/yourorg/hostctl-scripts.git`). `hostctl` will clone it temporarily.
377
- - A path to a local `.zip` file (a "bundle") created by the `bundle` command.
378
- - **`script`** (optional, especially if `package_or_bundle` is a bundle with a default script):
379
- - The name or path of the script file to execute (e.g., `main.ts`, `deploy.js`).
380
- - If `package_or_bundle` is provided, this is relative to the package root.
381
- - If `package_or_bundle` is omitted, this is treated as a path relative to the current working directory.
382
- - **`script_arguments...`** (optional):
383
- - Any additional arguments passed directly to the script being executed. These can be simple values or key-value pairs.
384
-
385
- **Options:**
386
-
387
- - `-f, --file <path>`: Loads script parameters from a specified JSON file. The file should contain a single JSON object.
388
- - `--params "<json_string>"`: Supplies script parameters as a JSON string directly on the command line (e.g., `--params '{"target_env":"production","retries":3}'`).
389
- - `-r, --remote`: Executes the script on remote hosts (as defined in the inventory and filtered by tags) instead of on the local machine. Defaults to `false` (local execution).
390
-
391
- **Examples:**
392
-
393
- ```bash
394
- # Run a local script
395
- hostctl run deploy.ts
396
-
397
- # Run a script from a local directory, passing arguments
398
- hostctl run ./my-scripts provision.js server_name=app01 zone=us-west
399
-
400
- # Run a script from a Git repository with parameters from a file
401
- hostctl run https://github.com/user/hostctl-scripts.git setup-db.ts -f params.json --remote
402
-
403
- # Run a script on remote hosts tagged 'database'
404
- hostctl run -t database -r check-replication.ts
405
- ```
406
-
407
- **Task Script Return Value**
408
-
409
- When a task script is executed using `hostctl run`, the value returned by the main task function (typically the default export of your script) is considered the primary result of the task.
410
-
411
- - **Standard Output:** By default, `hostctl` will display this result along with other evaluation details.
412
- - **JSON Output (`--json` or `-o json`):** If the `--json` flag (or its equivalent `-o json`) is specified, `hostctl` will output a JSON-serialized representation of the script's return value. When in JSON mode, this serialized result is the _only_ value rendered to standard output, suppressing other logs and evaluation details to ensure clean, machine-readable output. This is particularly useful for programmatic consumption of task results.
413
-
414
- #### `exec` (alias: `e`)
415
-
416
- Executes an ad-hoc shell command on the selected hosts.
417
-
418
- **Usage:**
419
-
420
- ```bash
421
- hostctl exec "<command_string>"
422
- ```
423
-
424
- - **`<command_string>`**: The shell command to execute. Enclose in quotes if it contains spaces or special characters.
425
-
426
- **Examples:**
427
-
428
- ```bash
429
- # Check disk space on all targeted hosts
430
- hostctl exec "df -h"
431
-
432
- # Restart a service on hosts tagged 'webserver'
433
- hostctl exec -t webserver "sudo systemctl restart nginx"
434
- ```
435
-
436
- #### `bundle` (alias: `b`)
437
-
438
- Packages a directory (typically a project with scripts and a `package.json`) into a deployable `.zip` bundle.
439
-
440
- **Usage:**
441
-
442
- ```bash
443
- hostctl bundle [path]
444
- ```
445
-
446
- - **`[path]`** (optional): The path to the directory to bundle. Defaults to the current directory (`.`).
447
-
448
- **Examples:**
449
-
450
- ```bash
451
- # Bundle the project in the current directory
452
- hostctl bundle
453
-
454
- # Bundle a project in a specific directory
455
- hostctl bundle ./my-hostctl-project
456
- ```
457
-
458
- #### `pkg`
459
-
460
- Manages `hostctl` packages with a comprehensive package management system that maintains a manifest of installed packages and their tasks.
461
-
462
- **Usage:**
463
-
464
- ```bash
465
- hostctl pkg [subcommand]
466
- ```
467
-
468
- **Subcommands:**
469
-
470
- - **`create <package-name>`**
471
- - Scaffolds a new `hostctl` package in the current directory.
472
- - **Usage:** `hostctl pkg create my-new-task`
473
- - **Options:**
474
- - `--lang <language>`: Specify the language for the package (`typescript` or `javascript`). Defaults to `typescript`.
475
-
476
- - **`install <package-source>`**
477
- - Installs a `hostctl` package and maintains it in the package manifest.
478
- - The `<package-source>` can be a local path, a Git URL, or an npm package name.
479
- - **Usage:**
480
-
481
- ```bash
482
- # Install from a local directory
483
- hostctl pkg install ./my-local-task
484
-
485
- # Install from a Git repository
486
- hostctl pkg install https://github.com/user/my-hostctl-task.git
487
-
488
- # Install from npm
489
- hostctl pkg install my-npm-hostctl-task
490
- ```
491
-
492
- - **`list`** (alias: `ls`)
493
- - Lists all installed `hostctl` packages with their discovered tasks.
494
- - **Usage:** `hostctl pkg list` or `hostctl pkg ls`
495
- - **Output Example:**
496
-
497
- ```
498
- Found 1 package(s):
499
-
500
- • hostsfile (v1.0.0) - hostsfile package [https_github.com_davidkellis_hostsfile]
501
- └─ add_entry
502
- ```
503
-
504
- - **`remove <package-identifier>`** (alias: `rm`)
505
- - Removes an installed `hostctl` package and updates the manifest.
506
- - The `<package-identifier>` can be either the package name or the directory name.
507
- - **Usage:** `hostctl pkg remove my-package` or `hostctl pkg rm my-package`
508
-
509
- ## Package Management System
510
-
511
- `hostctl` includes a comprehensive package management system that allows you to install, manage, and run tasks from external packages. This system maintains a manifest of installed packages and provides intelligent task discovery and resolution.
512
-
513
- ### Package Manifest
514
-
515
- The package management system maintains a `manifest.json` file in `~/.hostctl/packages/` that tracks all installed packages. This manifest contains:
516
-
517
- - **Package Source Mapping**: Each package source (URL, Git repo, local path) is mapped to package information
518
- - **Package Metadata**: Name, version, description, and installation directory
519
- - **Task Discovery**: Automatically discovered tasks within each package
520
- - **Persistent Storage**: Package information persists across sessions
521
-
522
- ### Task Discovery
523
-
524
- When a package is installed, `hostctl` automatically discovers tasks within the package by scanning for:
525
-
526
- - **TypeScript/JavaScript Files**: `.ts` and `.js` files in the package root
527
- - **Subdirectories**: Tasks in `tasks/` and `src/` subdirectories
528
- - **Default Tasks**: Common default task files like `index.ts`, `main.ts`, etc.
529
-
530
- ### Task Resolution
531
-
532
- The `hostctl run` command now supports multiple resolution strategies:
533
-
534
- 1. **Built-in Core Tasks**: `hostctl run core.echo hello`
535
- 2. **Installed Package Tasks**:
536
- - Default task: `hostctl run https://github.com/user/repo`
537
- - Specific task: `hostctl run https://github.com/user/repo:taskname`
538
- 3. **Relative Paths**: `hostctl run ./script.ts`
539
-
540
- ### Package Resolution Examples
541
-
542
- ```bash
543
- # Install a package
544
- hostctl pkg install https://github.com/davidkellis/hostsfile
545
-
546
- # List installed packages and their tasks
547
- hostctl pkg list
548
-
549
- # Run the default task from an installed package
550
- hostctl run https://github.com/davidkellis/hostsfile names:foo,bar
551
-
552
- # Run a specific task from an installed package
553
- hostctl run https://github.com/davidkellis/hostsfile:add_entry names:foo,bar
554
-
555
- # Run core tasks
556
- hostctl run core.echo hello
557
-
558
- # Run relative paths (still works)
559
- hostctl run example/echo.ts hello
560
- ```
561
-
562
- ### Abstract Package Management Examples
563
-
564
- The abstract package management system automatically detects your platform's package manager and provides a unified interface:
565
-
566
- ```bash
567
- # Auto-detect and update package sources
568
- hostctl run core.pkg.abstractUpdate
569
-
570
- # Search for packages (auto-detects package manager)
571
- hostctl run core.pkg.abstractSearch query:nginx
572
-
573
- # Install packages (auto-detects package manager)
574
- hostctl run core.pkg.abstractInstall package:curl
575
-
576
- # Install multiple packages
577
- hostctl run core.pkg.abstractInstall package:curl,wget,git
578
-
579
- # Force use of specific package manager
580
- hostctl run core.pkg.abstractInstall package:nginx packageManager:apt
581
-
582
- # List installed packages
583
- hostctl run core.pkg.abstractList
584
-
585
- # Upgrade all packages
586
- hostctl run core.pkg.abstractUpgrade
587
-
588
- # Clean package cache
589
- hostctl run core.pkg.abstractClean
590
- ```
591
-
592
- **Supported Platforms**: Ubuntu/Debian (apt), Alpine (apk), CentOS/RHEL (yum/dnf), Arch Linux (pacman), openSUSE (zypper), Solus (eopkg), Windows (winget/choco), macOS (brew)
593
-
594
- ### Package Structure
595
-
596
- A `hostctl` package should follow this structure:
597
-
598
- ```
599
- my-hostctl-package/
600
- ├── package.json # Package metadata
601
- ├── index.ts # Default task (optional)
602
- ├── task1.ts # Individual task
603
- ├── task2.ts # Individual task
604
- ├── tasks/ # Task subdirectory (optional)
605
- │ ├── task3.ts
606
- │ └── task4.ts
607
- └── src/ # Source subdirectory (optional)
608
- ├── task5.ts
609
- └── task6.ts
610
- ```
611
-
612
- ### Package Dependencies
613
-
614
- Each installed package maintains its own `node_modules` directory, ensuring that:
615
-
616
- - Dependencies are isolated per package
617
- - The `hostctl` runtime is available to each package
618
- - Packages can have their own version requirements
619
-
620
- ### Package Sources
621
-
622
- The package management system supports multiple source types:
623
-
624
- - **Local Paths**: `./my-package` or `/path/to/package`
625
- - **Git Repositories**: `https://github.com/user/repo.git` or `git@github.com:user/repo.git`
626
- - **GitHub Shorthand**: `github.com/user/repo`
627
- - **NPM Packages**: `@scope/package-name` or `package-name`
628
-
629
- ### Task Execution Context
630
-
631
- When running tasks from installed packages:
632
-
633
- - Tasks have access to the full `hostctl` runtime API
634
- - Package dependencies are available in the task's `node_modules`
635
- - Tasks can use all core tasks and utilities
636
- - Remote execution works seamlessly with installed package tasks
637
-
638
- #### `inventory` (alias: `i`)
639
-
640
- Manages and displays information about the host inventory.
641
-
642
- **Usage:**
643
-
644
- ```bash
645
- hostctl inventory [subcommand]
646
- ```
647
-
648
- Running `hostctl inventory` without a subcommand prints a report of the current inventory.
649
-
650
- **Subcommands:**
651
-
652
- - **`encrypt`** (alias: `e`)
653
- - Encrypts the inventory file (determined by the global `--config` option).
654
- - **Usage:** `hostctl inventory encrypt`
655
- - **`decrypt`** (alias: `d`)
656
- - Decrypts the inventory file.
657
-
658
- #### `runtime` (alias: `rt`)
659
-
660
- Manages the `hostctl` runtime environment, particularly for Node.js scripts.
661
-
662
- **Usage:**
663
-
664
- ```bash
665
- hostctl runtime [subcommand]
666
- ```
667
-
668
- Running `hostctl runtime` without a subcommand prints a report about the detected/configured runtime environment.
669
-
670
- **Subcommands:**
671
-
672
- - **`install`** (alias: `i`)
673
- - Installs or verifies a temporary Node.js runtime environment on the local host if `hostctl` determines one is needed (e.g., if a global Node.js is not found or a specific version is required).
674
- - **Usage:** `hostctl runtime install`
675
-
676
- ## Inventory File Structure
677
-
678
- `hostctl` uses a YAML file (defaulting to `hostctl.yaml`) to define its inventory of hosts, manage secrets, and specify encryption recipients. The file can contain three top-level keys: `hosts`, `secrets`, and `ids`.
679
-
680
- ### `hosts` Section
681
-
682
- This section defines the machines `hostctl` can interact with.
683
-
684
- ```yaml
685
- hosts:
686
- server1.example.com:
687
- user: deploy_user
688
- ssh-key: !secret server1_ssh_key # References a secret
689
- tags: [webserver, production, dc1]
690
-
691
- 192.168.1.100:
692
- alias: db_server_primary
693
- user: admin
694
- password: !secret db_admin_password
695
- tags: [database, primary, dc1]
696
-
697
- localhost:
698
- user: mylocaluser
699
- tags: [local, dev]
700
-
701
- another.server.com:
702
- # Minimal configuration, will use global defaults or prompt if needed
703
- tags: [staging]
704
- ```
705
-
706
- - Each key under `hosts` (e.g., `server1.example.com`, `192.168.1.100`) is the hostname or IP address.
707
- - **Properties for each host:**
708
- - `alias` (optional): A friendly name for the host.
709
- - `user` (optional): The username for SSH connections.
710
- - `password` (optional): The password for SSH. Can be a plain string or a `!secret` reference (e.g., `!secret my_password_name`).
711
- - `ssh-key` (optional): Path to an SSH private key file or the private key content itself. Can also be a `!secret` reference.
712
- - `tags` (optional): A list of strings for categorizing and selecting hosts.
713
-
714
- ### `secrets` Section
715
-
716
- This section stores sensitive data, typically encrypted using AGE encryption.
717
-
718
- ```yaml
719
- secrets:
720
- server1_ssh_key:
721
- ids: [age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, admin_keys_group] # AGE public key or id group ref
722
- value: |
723
- -----BEGIN AGE ENCRYPTED FILE-----
724
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
725
- -----END AGE ENCRYPTED FILE-----
726
-
727
- db_admin_password:
728
- ids: db_admins_group # Reference to an 'ids' group
729
- value: 'plaintextpasswordbeforeencryption' # This will be encrypted by 'hostct inventory encrypt'
730
- ```
731
-
732
- - Each key under `secrets` (e.g., `server1_ssh_key`) is a unique name for the secret.
733
- - **Properties for each secret:**
734
- - `ids`: A single string or a list of strings specifying the AGE public keys or named recipient groups (from the `ids` section) that can decrypt this secret.
735
- - `value`: The secret content. If `hostctl inventory encrypt` has been run, this will be an AGE encrypted block.
736
-
737
- ### `ids` Section
738
-
739
- This section defines named groups of AGE public keys, simplifying secret management.
740
-
741
- ```yaml
742
- ids:
743
- admin_keys_group:
744
- - age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Alice's public key
745
- - age1yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy # Bob's public key
746
-
747
- db_admins_group:
748
- - admin_keys_group # Nested group reference
749
- - age1zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz # CI server's public key
750
-
751
- single_key_user: age1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
752
- ```
753
-
754
- - Each key under `ids` (e.g., `admin_keys_group`) is a unique name for a group of recipients.
755
- - The value can be a single AGE public key string, a list of AGE public key strings, or a list containing references to other `ids` group names.
756
-
757
- This structure allows for a flexible and secure way to manage host information and sensitive credentials. The `hostct inventory encrypt` and `hostct inventory decrypt` commands interact with the `secrets` and `ids` sections to manage encryption.
758
-
759
- ### Inventory Decryption Behavior
760
-
761
- When working with the inventory decryption functionality, it's important to understand these key behaviors:
762
-
763
- 1. **Hostname Storage**: In the YAML configuration, hostnames are stored as the keys in the `hosts` map, not as properties inside each host object. When a host is serialized to YAML via `Host.toYAML()`, the `hostname` field is not included because it's represented by the map key.
764
-
765
- 2. **Verbosity and Logging**: The inventory decryption process follows the general verbosity rules of hostctl:
766
- - By default, only ERROR level messages are shown
767
- - INFO level messages (like "No encrypted secrets found to decrypt. Inventory is already decrypted.") require the `-v` flag to be visible
768
- - For idempotent operations like decrypting an already-decrypted inventory, you need to use the `-v` flag to see confirmation messages
769
-
770
- 3. **Idempotent Operations**: The `inventory decrypt` command is designed to be idempotent - running it on an already decrypted inventory will not cause errors, but will only show a message if the `-v` flag is used.
771
-
772
- ## Writing Tasks
773
-
774
- A `hostctl` task is a TypeScript file that exports a default function created by the `task()` factory. This function receives a `context` object, which provides APIs for interacting with the system, hosts, and the user.
775
-
776
- ### Task Structure
777
-
778
- Here is a basic example of a task that echoes a message:
779
-
780
- ```typescript
781
- // src/core/echo.ts
782
- import { task, type TaskContext, Verbosity } from '../runtime';
783
-
784
- export interface EchoParams {
785
- message: string;
786
- }
787
-
788
- export interface EchoResult {
789
- echo: string;
790
- }
791
-
792
- async function run(context: TaskContext<EchoParams>): Promise<EchoResult> {
793
- const { params, log } = context;
794
- const { message } = params;
795
- log(Verbosity.INFO, `Echoing message: ${message}`);
796
- return { echo: message };
797
- }
798
-
799
- export default task(run, {
800
- description: 'Echoes a message back to the caller.',
801
- });
802
- ```
803
-
804
- ### The `TaskContext` API
805
-
806
- The `context` object passed to your task's `run` function contains several helpful properties and methods for task execution:
807
-
808
- - `params`: An object containing the parameters passed to the task.
809
- - `id`: A unique identifier for the current task invocation.
810
- - `host`: The `Host` object the task is currently running against. This is only available in a remote context (e.g., inside `context.ssh`).
811
- - `config`: The resolved application configuration (`AppConfig`).
812
- - `log(level, ...message)`: A function to log messages with a specific verbosity level. Levels are available from `Verbosity` (`ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE`).
813
- - `info(...)`, `debug(...)`, `warn(...)`, `error(...)`: Shortcuts for `log()` with the corresponding verbosity level.
814
- - `exec(command, options)`: Executes a shell command on the target host. The `command` can be a string or an array of strings. It returns a `Promise<CommandResult>`, which contains `stdout`, `stderr`, `exitCode`, and boolean `success`/`failure` properties.
815
- - `run(taskFn)`: Executes another `hostctl` task as a sub-task. This is the preferred way to compose tasks and reuse logic. For example: `await context.run(someOtherTask({ param: 'value' }))`.
816
- - `ssh(tags, remoteTaskFn)`: Executes a function on a set of remote hosts matching the given tags, enabling parallel remote execution.
817
- - `file`: An object providing a file system API (`read`, `write`, `exists`, `mkdir`, `rm`) that is contextualized to the target host (local or remote).
818
- - `inventory(tags)`: Returns a list of `Host` objects from the inventory that match the given tags.
819
- - `getPassword()`: Prompts the user for a password interactively.
820
- - `getSecret(name)`: Retrieves a decrypted secret value from the configuration by its name.
821
- - `exit(exitCode, message)`: Immediately terminates the `hostctl` application with a given exit code and optional message.
822
-
823
- This API provides the core building blocks for creating powerful and flexible automation tasks with `hostctl`.
824
-
825
- ## Writing hostctl Scripts
826
-
827
- `hostctl` scripts are powerful tools for automating tasks on local and remote machines. They are written in TypeScript or JavaScript and follow a simple, consistent structure. This guide covers everything you need to know to write your own `hostctl` scripts.
828
-
829
- ### The Basic Structure
830
-
831
- A `hostctl` script is a module that exports a default `task` object. This object is created using the `task()` factory function, which takes your main logic function and an optional description.
832
-
833
- ```typescript
834
- // src/tasks/my-task.ts
835
- import { task, type TaskContext, Verbosity } from 'hostctl';
836
-
837
- // Define the parameters your task accepts
838
- export interface MyTaskParams {
839
- targetSystem: string;
840
- enableFeature: boolean;
841
- }
842
-
843
- // Define the structure of the value your task returns
844
- export interface MyTaskResult {
845
- status: string;
846
- rebootRequired: boolean;
847
- }
848
-
849
- // The core logic of your task
850
- async function run(context: TaskContext<MyTaskParams>): Promise<MyTaskResult> {
851
- const { params, log, exec } = context;
852
-
853
- log(Verbosity.INFO, `Configuring ${params.targetSystem}...`);
854
-
855
- if (params.enableFeature) {
856
- await exec('echo "Feature enabled" >> /etc/config.conf');
857
- }
858
-
859
- const result = await exec('check-reboot-status');
860
-
861
- return {
862
- status: 'Configuration applied successfully.',
863
- rebootRequired: result.stdout.includes('reboot required'),
864
- };
865
- }
866
-
867
- // Export the task with a description that can use Handlebars templating
868
- export default task(run, 'Configures {{targetSystem}} with feature: {{enableFeature}}');
869
- ```
870
-
871
- ### The `task()` Factory
872
-
873
- The `task()` function is the entry point for defining your script.
874
-
875
- `task(runFunction, description?)`
876
-
877
- - `runFunction`: An `async` function that contains the core logic of your task. It receives a `TaskContext` object.
878
- - `description` (optional): A string describing the task. It supports [Handlebars](https://handlebarsjs.com/) templating to dynamically insert parameter values, e.g., `"Deploying version {{version}} to {{environment}}"`.
879
-
880
- ### The `TaskContext` Object
881
-
882
- The `TaskContext` is the heart of the scripting API. It's passed to your `run` function and provides all the necessary tools to interact with the system.
883
-
884
- - `params: TParams`: A strongly-typed object containing the parameters passed to your script from the command line or a params file.
885
- - `log(level: LogLevel, ...message)`: A logging function for structured output.
886
- - **Levels**: Use the `Verbosity` enum for log levels: `Verbosity.DEBUG`, `Verbosity.INFO`, `Verbosity.WARN`, `Verbosity.ERROR`, `Verbosity.TRACE`.
887
- - **Usage**: `context.log(Verbosity.INFO as LogLevel, "This is an info message");`
888
- - `info(...message)`: A convenience function that calls `log(Verbosity.INFO, ...message)`
889
- - `error(...message)`: A convenience function that calls `log(Verbosity.ERROR, ...message)`
890
- - `warn(...message)`: A convenience function that calls `log(Verbosity.WARN, ...message)`
891
- - `debug(...message)`: A convenience function that calls `log(Verbosity.DEBUG, ...message)`
892
- - `trace(...message)`: A convenience function that calls `log(Verbosity.TRACE, ...message)`
893
- - `exec(command: string, options?: ExecOptions): Promise<CommandResult>`: Executes a shell command on the target host.
894
- - **`options`**:
895
- - `cwd`: The working directory for the command.
896
- - `sudo`: If `true`, runs the command with `sudo`.
897
- - `stdin`: A string or Buffer to pass to the command's standard input when it starts.
898
- - `input`: An object where keys are regular expression strings (to match command output) and values are the corresponding strings to send to the command's standard input. Used for automating interactive prompts.
899
- - `pty`: If `true`, allocates a pseudo-terminal, useful for interactive commands.
900
- - `env`: An object of environment variables.
901
- - **`CommandResult`**: The promise resolves to an object containing `stdout`, `stderr`, `exitCode`, and `signal`.
902
- - `ssh(hostSelector, taskDefinition, params?): Promise<TRemoteReturn[] | Error>`: Executes another `hostctl` task on one or more remote hosts.
903
- - **`hostSelector`**: A string, an array of strings, a `Host` object, or an array of `Host` objects to specify the target hosts from your inventory.
904
- - **`taskDefinition`**: The task to execute on the remote host(s).
905
- - `params`: The parameters to pass to the remote task.
906
- - `file: FileOperations`: An object for performing file operations on the target host.
907
- - `read(path)`: Reads the content of a file.
908
- - `write(path, content)`: Writes content to a file.
909
- - `exists(path)`: Checks if a file or directory exists.
910
- - `mkdir(path)`: Creates a directory.
911
- - `rm(path)`: Removes a file or directory.
912
- - `cwd: Path`: The current working directory of the task.
913
- - `app: App`: A reference to the main `App` instance, giving you access to global configuration, the full host inventory, and secret management (`context.app.getSecretValue()`).
914
- - `host?: Host`: If the task is running on a specific host (e.g., via `ssh`), this object contains details about that host.
915
-
916
- ### Handling Interactive Commands
917
-
918
- For commands that require user input (e.g., password prompts, confirmation dialogs), `hostctl` provides a straightforward way to automate these interactions through the `input` option in `context.exec()`.
919
-
920
- You don't need to create an `InteractionHandler` instance directly in your task script. Instead, you provide an object to the `input` option where:
921
-
922
- - Each **key** is a string representing a regular expression. `hostctl` will monitor the command's standard output for lines matching this regex.
923
- - Each **value** is the string that `hostctl` will send to the command's standard input when the corresponding key's regex is matched.
924
-
925
- ```typescript
926
- import { task, type TaskContext } from 'hostctl';
927
-
928
- async function run(context: TaskContext): Promise<void> {
929
- const { exec, log } = context;
930
-
931
- log(Verbosity.INFO as LogLevel, 'Attempting an operation that requires interactive confirmation...');
932
-
933
- const result = await exec('your-interactive-command --with-prompt', {
934
- pty: true, // A PTY is usually required for interactive commands
935
- input: {
936
- // When the command outputs "Enter your password:", respond with "mysecretpassword"
937
- 'Enter your password:': 'mysecretpassword',
938
- // When the command outputs something like "Are you sure you want to continue (yes/no)?", respond with "yes"
939
- 'Are you sure you want to continue \\(yes\\/no\\)\\?': 'yes',
940
- },
941
- });
942
-
943
- if (result.exitCode === 0) {
944
- log(Verbosity.INFO as LogLevel, 'Interactive command completed successfully.');
945
- } else {
946
- log(
947
- Verbosity.ERROR as LogLevel,
948
- `Interactive command failed with exit code ${result.exitCode}. Stderr: ${result.stderr}`,
949
- );
950
- }
951
- }
952
-
953
- export default task(run, 'Demonstrate handling of an interactive command');
954
- ```
955
-
956
- **Important Considerations:**
957
-
958
- - **PTY Allocation**: Interactive commands usually require a pseudo-terminal (PTY) to behave correctly. Always set `pty: true` in the `ExecOptions` when using the `input` map for interactions.
959
- - **Regex Specificity**: Make your regular expressions specific enough to avoid unintended matches. Test them thoroughly.
960
- - **Order of Interactions**: If a command has multiple prompts, `hostctl` will respond to them as they appear in the output and match the keys in your `input` object.
961
-
962
- #### Using the `withSudo` Helper
963
-
964
- Since running commands with `sudo` is a very common pattern, `hostctl` provides a convenient helper function, `withSudo`, to simplify this process. It automatically creates the correct input mapping for the `sudo` password prompt.
965
-
966
- ```typescript
967
- import { task, type TaskContext, withSudo } from 'hostctl';
968
-
969
- async function run(context: TaskContext): Promise<void> {
970
- const { exec, log, getPassword } = context;
971
-
972
- const sudoPassword = await getPassword(); // Prompts user if not already provided
973
- if (!sudoPassword) {
974
- throw new Error('Sudo password is required but was not provided.');
975
- }
976
-
977
- // Use the helper to build the input map
978
- const input = withSudo(sudoPassword);
979
-
980
- log(Verbosity.INFO as LogLevel, 'Running a command with sudo...');
981
-
982
- // Pass the generated input map to the exec options
983
- const result = await exec('sudo apt-get update', { pty: true, input });
984
-
985
- if (result.exitCode === 0) {
986
- log(Verbosity.INFO as LogLevel, "'apt-get update' completed successfully.");
987
- } else {
988
- log(Verbosity.ERROR as LogLevel, "'apt-get update' failed.");
989
- }
990
- }
991
-
992
- export default task(run, 'Demonstrate using the withSudo helper');
993
- ```
994
-
995
- The `withSudo` helper can also be combined with other interactions:
996
-
997
- `const input = withSudo(password, { "Do you want to continue?": "yes" });`
998
-
999
- ### A Complete Example: Installing a Package
1000
-
1001
- Here is a realistic example of a task that installs a package on a Debian-based system.
1002
-
1003
- ```typescript
1004
- // src/tasks/install-nginx.ts
1005
- import { task, type TaskContext, Verbosity, type LogLevel } from 'hostctl';
1006
-
1007
- export interface InstallNginxParams {
1008
- updateCache?: boolean;
1009
- }
1010
-
1011
- async function run(context: TaskContext<InstallNginxParams>): Promise<void> {
1012
- const { params, log, exec } = context;
1013
-
1014
- log(Verbosity.INFO as LogLevel, 'Starting Nginx installation...');
1015
-
1016
- if (params.updateCache) {
1017
- log(Verbosity.DEBUG as LogLevel, 'Updating package cache...');
1018
- await exec('sudo apt-get update');
1019
- }
1020
-
1021
- log(Verbosity.INFO as LogLevel, 'Installing nginx package...');
1022
- const installResult = await exec('sudo apt-get install -y nginx');
1023
-
1024
- if (installResult.exitCode !== 0) {
1025
- log(Verbosity.ERROR as LogLevel, 'Failed to install Nginx.');
1026
- throw new Error(`apt-get failed with exit code ${installResult.exitCode}`);
1027
- }
1028
-
1029
- log(Verbosity.INFO as LogLevel, 'Nginx installed successfully.');
1030
- }
1031
-
1032
- export default task(run, 'Install Nginx on a Debian-based system');
1033
- ```
1034
-
1035
- ## TODO
1036
-
1037
- - [ ] Further refine the script execution interface.
1038
- - [ ] Expand documentation for script development.
1039
- - [ ] Add more examples for common use cases.
3
+ `hostctl` is a modern task runner for managing fleets of hosts. It makes it easy to execute TypeScript or JavaScript automations locally or over SSH while keeping secrets, inventories, and host metadata in one place.
4
+
5
+ ## Why hostctl
6
+ - Treat remote automation like regular code: tasks are modules with strong typing and rich logging.
7
+ - First-class inventory and secrets management built around [AGE](https://age-encryption.org/).
8
+ - Layered runtime isolates CLI parsing, orchestration, and local/remote execution for easy extension.
9
+
10
+ ## Quick Start
11
+ 1. **Install dependencies**
12
+ - Node.js 24+ (the CLI runs through `tsx` to execute TypeScript directly)
13
+ - `age`: `brew install age` (macOS) or follow <https://age-encryption.org>.
14
+ 2. **Clone and bootstrap**
15
+ ```bash
16
+ git clone https://github.com/monopod/hostctl.git
17
+ cd hostctl
18
+ npm install
19
+ ```
20
+ 3. **Generate AGE identities**
21
+ ```bash
22
+ mkdir -p ~/.hostctl/age
23
+ age-keygen -o ~/.hostctl/age/jane.priv
24
+ age-keygen -y ~/.hostctl/age/jane.priv > ~/.hostctl/age/jane.pub
25
+ ```
26
+ 4. **Author an inventory**
27
+ Create `~/.hostctl/hostctl.yaml`:
28
+ ```yaml
29
+ hosts:
30
+ ubuntu-vm:
31
+ hostname: 192.168.56.15
32
+ user: vagrant
33
+ password: !secret vagrant-password
34
+ tags: [ubuntu, testvm]
35
+ secrets:
36
+ vagrant-password:
37
+ ids: jane
38
+ value: vagrant
39
+ ids:
40
+ jane: age1...
41
+ ```
42
+ See `docs/system_management.md` for more host examples and encryption workflows.
43
+ 5. **Run an example task**
44
+ ```bash
45
+ npm run build
46
+ ./dist/bin/hostctl.js run example/echo.ts args:hello
47
+ ```
48
+ Use `AGE_IDS="~/.hostctl/age/*.priv"` to point at custom identities.
49
+
50
+ 6. **Target remote hosts by tag**
51
+ ```bash
52
+ ./dist/bin/hostctl.js run -r -t xcpng-lab --json core.net.interfaces
53
+ ```
54
+ Flags:
55
+ - `-r` executes the task against matching remote hosts from your inventory.
56
+ - `-t <tag>` narrows the target set; multiple tags may be comma-separated.
57
+ - `--json` emits machine-readable results per host.
58
+
59
+ ## Install via npm
60
+ - Global CLI:
61
+ ```bash
62
+ npm install -g hostctl
63
+ hostctl run example/echo.ts args:hello
64
+ ```
65
+ - One-off execution:
66
+ ```bash
67
+ npx hostctl run core.echo message:hello
68
+ ```
69
+ The published binary keeps its `tsx`-based runtime so TypeScript scripts can execute without pre-compilation; ensure your runtime Node version is 24 or newer.
70
+
71
+ ## Documentation Map
72
+ - **Architecture & Design**: [ARCHITECTURE.md](ARCHITECTURE.md) captures module boundaries, runtime flows, and design principles.
73
+ - **Operational Guides**: User-facing how‑tos live in `docs/` (e.g., `docs/process_management.md`, `docs/package_management.md`, `docs/system_management.md`).
74
+ - **Detailed Onboarding**: [docs/user-onboarding.md](docs/user-onboarding.md) preserves the full setup, CLI, and task catalog reference.
75
+ - **XCP-ng Provisioning**: [docs/xcp-ng-operations.md](docs/xcp-ng-operations.md) covers the emerging hypervisor-native testing workflow.
76
+ - **LLM Contributor Guide**: See [AGENTS.md](AGENTS.md) for automated contributors.
77
+ - **Community Standards**: [CONTRIBUTING.md](CONTRIBUTING.md) outlines coding standards, issue triage, and release process.
78
+
79
+ ## Developer Workflow
80
+ - Format & typing: `npm run format` (Prettier) and `npm run lint` (TypeScript no-emit).
81
+ - Build artifacts: `npm run build` (tsup) produces `dist/` bundles consumed by the CLI and published package.
82
+ - Tests:
83
+ - `npm run test` → unit + functional suites.
84
+ - `npm run test:unit`, `npm run test:functional`, `npm run test:e2e` for focused runs.
85
+ - VM-backed runs currently lean on legacy Vagrant boxes. Those helpers (`npm run vm:*`) remain for continuity but are **deprecated** while we shift to provisioning test VMs directly on XCP-ng hypervisors. See [docs/xcp-ng-operations.md](docs/xcp-ng-operations.md) for the in-progress workflow and contribute new test setups there.
86
+ - Release scripts rely on `npm run release` and accompanying tooling described in `NPM_MIGRATION_PLAN.md`.
87
+
88
+ ## Next Steps
89
+ - Explore bundled examples under `example/` to learn the task API.
90
+ - Review [ARCHITECTURE.md](ARCHITECTURE.md) for runtime internals and extension guidelines.
91
+ - Familiarise yourself with the new XCP-ng task suite to help migrate integration tests off the Vagrant stack.
92
+ - Join discussions or open issues at <https://github.com/monopod/hostctl/issues>.