hostctl 0.1.39 → 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/LICENSE +14 -0
- package/README.md +90 -1037
- package/dist/bin/hostctl.js +2456 -2031
- package/dist/bin/hostctl.js.map +1 -1
- package/dist/index.d.ts +3962 -222
- package/dist/index.js +20559 -3578
- package/dist/index.js.map +1 -1
- package/package.json +36 -9
package/README.md
CHANGED
|
@@ -1,1039 +1,92 @@
|
|
|
1
1
|
# hostctl
|
|
2
2
|
|
|
3
|
-
`hostctl` is a modern
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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>.
|