hostctl 0.1.40 → 0.1.42
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 +170 -1012
- package/dist/bin/hostctl.js +2456 -2031
- package/dist/bin/hostctl.js.map +1 -1
- package/dist/index.d.ts +4035 -236
- package/dist/index.js +21360 -3584
- package/dist/index.js.map +1 -1
- package/package.json +36 -8
package/README.md
CHANGED
|
@@ -1,1039 +1,197 @@
|
|
|
1
1
|
# hostctl
|
|
2
2
|
|
|
3
|
-
`hostctl` is a modern
|
|
3
|
+
`hostctl` is a modern task runner for managing fleets of hosts. It executes TypeScript or JavaScript automations locally or over SSH while keeping inventories, tags, and secrets in one place.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
## Why hostctl
|
|
6
|
+
- Treat remote automation like regular code: tasks are modules with strong typing and structured logging.
|
|
7
|
+
- First-class inventory and secrets management built around [AGE](https://age-encryption.org/).
|
|
8
|
+
- Layered runtime cleanly separates CLI parsing, orchestration, and execution for easy extension.
|
|
7
9
|
|
|
8
|
-
##
|
|
10
|
+
## System Requirements
|
|
11
|
+
- Node.js **24+** (CLI runs through `tsx`).
|
|
12
|
+
- [`age`](https://age-encryption.org) for encrypting secrets.
|
|
13
|
+
- Git (recommended) for cloning packages and publishing tasks.
|
|
9
14
|
|
|
10
|
-
|
|
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:**
|
|
15
|
+
Check your toolchain:
|
|
369
16
|
|
|
370
17
|
```bash
|
|
371
|
-
|
|
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
|
|
18
|
+
node -v
|
|
19
|
+
npm -v
|
|
20
|
+
age --version
|
|
405
21
|
```
|
|
406
22
|
|
|
407
|
-
|
|
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:**
|
|
23
|
+
## Install hostctl
|
|
419
24
|
|
|
25
|
+
### Clone & Develop
|
|
420
26
|
```bash
|
|
421
|
-
|
|
27
|
+
git clone https://github.com/monopod/hostctl.git
|
|
28
|
+
cd hostctl
|
|
29
|
+
npm install
|
|
422
30
|
```
|
|
31
|
+
Use `./hostctl` during development or `npm run build && ./dist/bin/hostctl.js ...` to test the bundled CLI.
|
|
423
32
|
|
|
424
|
-
|
|
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
|
-
|
|
33
|
+
### Global CLI
|
|
442
34
|
```bash
|
|
443
|
-
|
|
35
|
+
npm install -g hostctl
|
|
36
|
+
hostctl --help
|
|
444
37
|
```
|
|
445
38
|
|
|
446
|
-
-
|
|
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
|
-
|
|
39
|
+
### One-off Execution
|
|
40
|
+
Run straight from npm without installing globally:
|
|
464
41
|
```bash
|
|
465
|
-
hostctl
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
- **
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
3. **Relative Paths**: `hostctl run ./script.ts`
|
|
539
|
-
|
|
540
|
-
### Package Resolution Examples
|
|
541
|
-
|
|
42
|
+
npx hostctl run core.echo message:hello
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Bootstrap identities, inventory, and secrets
|
|
46
|
+
1. **Generate AGE identities**
|
|
47
|
+
```bash
|
|
48
|
+
mkdir -p ~/.hostctl/age
|
|
49
|
+
age-keygen -o ~/.hostctl/age/you.priv
|
|
50
|
+
age-keygen -y ~/.hostctl/age/you.priv > ~/.hostctl/age/you.pub
|
|
51
|
+
```
|
|
52
|
+
2. **Author an inventory** at `~/.hostctl/hostctl.yaml`:
|
|
53
|
+
```yaml
|
|
54
|
+
hosts:
|
|
55
|
+
ubuntu-vm:
|
|
56
|
+
hostname: 192.168.56.15
|
|
57
|
+
user: vagrant
|
|
58
|
+
password: !secret vagrant-password
|
|
59
|
+
tags: [ubuntu, testvm]
|
|
60
|
+
secrets:
|
|
61
|
+
vagrant-password:
|
|
62
|
+
ids: you
|
|
63
|
+
value: vagrant
|
|
64
|
+
ids:
|
|
65
|
+
you: age1...
|
|
66
|
+
```
|
|
67
|
+
3. **Manage the file**
|
|
68
|
+
```bash
|
|
69
|
+
hostctl inventory encrypt # wrap with AGE recipients
|
|
70
|
+
hostctl inventory decrypt # view/edit locally
|
|
71
|
+
hostctl inventory list # inspect hosts & tags
|
|
72
|
+
```
|
|
73
|
+
Set `AGE_IDS="~/.hostctl/age/*.priv"` before running commands if the file is encrypted.
|
|
74
|
+
|
|
75
|
+
## Running tasks
|
|
76
|
+
- **Local script**
|
|
77
|
+
```bash
|
|
78
|
+
npm run build
|
|
79
|
+
./dist/bin/hostctl.js run example/echo.ts args:hello
|
|
80
|
+
```
|
|
81
|
+
- **Local package**
|
|
82
|
+
```bash
|
|
83
|
+
hostctl run ./my-package hello name:Sam
|
|
84
|
+
```
|
|
85
|
+
- **Remote orchestration**
|
|
86
|
+
```bash
|
|
87
|
+
hostctl run -r -t ubuntu core.net.interfaces --json
|
|
88
|
+
```
|
|
89
|
+
- `-r/--remote` targets hosts selected by tags via SSH.
|
|
90
|
+
- `-t/--tag` is greedy; use `--` before positional args when needed.
|
|
91
|
+
- **From npm or git**
|
|
92
|
+
```bash
|
|
93
|
+
hostctl run hostctl-hello greet name:Phil
|
|
94
|
+
hostctl run https://github.com/monopod/hostctl-example echo args:hello,world
|
|
95
|
+
```
|
|
96
|
+
- **Install Docker anywhere**
|
|
97
|
+
```bash
|
|
98
|
+
hostctl run -r -t ubuntu core.docker.install users:hostctl
|
|
99
|
+
```
|
|
100
|
+
The task follows Docker’s official installation guides for Ubuntu/Debian and Fedora/RHEL/Rocky (matching the current `xcpng-e2e` templates), so the same invocation works across your lab images.
|
|
101
|
+
- **Run containers**
|
|
102
|
+
```bash
|
|
103
|
+
hostctl run core.docker.run-container image:alpine command:'["/bin/sh","-c","echo from container"]'
|
|
104
|
+
hostctl run core.docker.run-container-detached image:redis name:ci-cache
|
|
105
|
+
```
|
|
106
|
+
The first task streams container output back to the CLI; the detached variant returns the created container ID/name so you can manage it later.
|
|
107
|
+
|
|
108
|
+
**Passing parameters**
|
|
109
|
+
- `key:value` pairs after the task name.
|
|
110
|
+
- `--params '{"key":"value"}'` for JSON blobs.
|
|
111
|
+
- `--file params.json` to load structured arguments.
|
|
112
|
+
|
|
113
|
+
## Managing task packages
|
|
114
|
+
`hostctl pkg` commands wrap the npm-only package manager:
|
|
542
115
|
```bash
|
|
543
|
-
|
|
544
|
-
hostctl pkg install
|
|
545
|
-
|
|
546
|
-
# List installed packages and their tasks
|
|
116
|
+
hostctl pkg create my-task --lang typescript
|
|
117
|
+
hostctl pkg install hostctl-hello
|
|
547
118
|
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
|
|
119
|
+
hostctl pkg remove hostctl-hello
|
|
560
120
|
```
|
|
121
|
+
Installed packages live under `~/.hostctl/packages` and can be run offline once cached.
|
|
561
122
|
|
|
562
|
-
|
|
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
|
|
123
|
+
## Designing tasks
|
|
124
|
+
Define tasks with the `task` helper and a typed `TaskContext`:
|
|
584
125
|
|
|
585
|
-
|
|
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
|
|
126
|
+
```ts
|
|
926
127
|
import { task, type TaskContext } from 'hostctl';
|
|
927
128
|
|
|
928
|
-
|
|
929
|
-
|
|
129
|
+
interface EchoParams { message: string }
|
|
130
|
+
interface EchoResult { repeated: string }
|
|
930
131
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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.');
|
|
132
|
+
async function run(context: TaskContext<EchoParams>): Promise<EchoResult> {
|
|
133
|
+
const { params, info } = context;
|
|
134
|
+
info(`Echo: ${params.message}`);
|
|
135
|
+
return { repeated: params.message };
|
|
1030
136
|
}
|
|
1031
137
|
|
|
1032
|
-
export default task(run, '
|
|
1033
|
-
```
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
-
|
|
1038
|
-
-
|
|
1039
|
-
-
|
|
138
|
+
export default task(run, { name: 'example.echo', description: 'Prints a message' });
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Key `TaskContext` capabilities (see [`docs/task-api.md`](docs/task-api.md) for details):
|
|
142
|
+
- Structured logging via `info`, `warn`, `error`, `debug`, or `log(level, ...)`.
|
|
143
|
+
- Command execution with `exec(command, { sudo, env, cwd, stdin, pty })`.
|
|
144
|
+
- Remote fan-out using `ssh(tags, remoteFn)`.
|
|
145
|
+
- Subtask orchestration with `run(otherTask(params))`.
|
|
146
|
+
- Secrets & credentials via `getSecret(name)` and `getPassword()`.
|
|
147
|
+
- Inventory helpers: `inventory(tags)` and `selectedInventory(tags?)`.
|
|
148
|
+
- File helpers: `file.read`, `file.write`, `file.exists`, `file.mkdir`, `file.rm` that respect local vs. remote runtime.
|
|
149
|
+
|
|
150
|
+
## Building & testing task packages
|
|
151
|
+
1. Scaffold: `hostctl pkg create awesome-firewall --lang typescript`.
|
|
152
|
+
2. Install deps: `cd awesome-firewall && npm install`.
|
|
153
|
+
3. Build: `npm run build` (defaults to `tsc`).
|
|
154
|
+
4. Test locally without publishing: `npx hostctl run . args:foo`.
|
|
155
|
+
5. Add unit tests with Vitest if needed, or invoke tasks directly in scripts.
|
|
156
|
+
|
|
157
|
+
## Publishing task packages
|
|
158
|
+
1. Update `package.json` metadata (`name`, `version`, `description`, `files`).
|
|
159
|
+
2. Build artifacts (`npm run build` or rely on `prepublishOnly`).
|
|
160
|
+
3. Authenticate with npm (`npm login` or `NPM_TOKEN`).
|
|
161
|
+
4. Publish:
|
|
162
|
+
```bash
|
|
163
|
+
npm version patch # or minor/major
|
|
164
|
+
npm publish --access public
|
|
165
|
+
```
|
|
166
|
+
5. Tag releases in git or automate with tools like `release-it` (this repo ships `release.sh` as an example flow).
|
|
167
|
+
6. Verify: `npm info your-package version`.
|
|
168
|
+
|
|
169
|
+
## Consuming published packages
|
|
170
|
+
- One-off: `npx hostctl run your-package taskName param:value`.
|
|
171
|
+
- Cache locally: `hostctl pkg install your-package@1.2.3` then run offline.
|
|
172
|
+
- Compose in other codebases by adding the package to `dependencies` and importing its exported tasks.
|
|
173
|
+
|
|
174
|
+
## Troubleshooting
|
|
175
|
+
- **Task not found**: confirm the package exports the task name and that `exports` exposes it.
|
|
176
|
+
- **Version mismatch**: keep `package.json`, `src/version.ts`, and `jsr.json` in sync before releasing.
|
|
177
|
+
- **SSH failures**: verify inventory entries (hostname, user, auth) and only pass `-r` when you intend remote execution.
|
|
178
|
+
- **Secrets**: when inventories are encrypted, ensure `AGE_IDS` includes the private keys so `hostctl` can decrypt secrets.
|
|
179
|
+
- **Environment drift**: run `hostctl runtime` to inspect prerequisites or `hostctl runtime install` to bootstrap them.
|
|
180
|
+
|
|
181
|
+
## Developer workflow
|
|
182
|
+
- Format & typing: `npm run format` (Prettier) and `npm run lint` (`tsc --noEmit`).
|
|
183
|
+
- Build artifacts: `npm run build` (tsup) produces `dist/` bundles for the CLI and published package.
|
|
184
|
+
- Tests:
|
|
185
|
+
- `npm run test` → unit + functional suites.
|
|
186
|
+
- `npm run test:unit`, `npm run test:functional`, `npm run test:e2e` for focused runs.
|
|
187
|
+
- VM helpers (`npm run vm:*`) are legacy while XCP-ng provisioning matures; see `docs/xcp-ng-operations.md`.
|
|
188
|
+
- Releases rely on `npm run release` + `release-it`; see `NPM_MIGRATION_PLAN.md` for npm-only context.
|
|
189
|
+
|
|
190
|
+
## Documentation map
|
|
191
|
+
- **Task API reference**: [`docs/task-api.md`](docs/task-api.md).
|
|
192
|
+
- **Operational guides**: `docs/*.md` (process, package, system management, etc.).
|
|
193
|
+
- **Architecture & design**: [`ARCHITECTURE.md`](ARCHITECTURE.md) and supporting design docs.
|
|
194
|
+
- **Contributor guide**: [`CONTRIBUTING.md`](CONTRIBUTING.md) and [`AGENTS.md`](AGENTS.md).
|
|
195
|
+
- **Issue tracking**: <https://github.com/monopod/hostctl/issues>.
|
|
196
|
+
|
|
197
|
+
Explore the examples under `example/`, get familiar with the task API, and share automations with the community via npm-compatible packages.
|