paratix 0.0.1
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 +21 -0
- package/README.md +346 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Software GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# Paratix
|
|
2
|
+
|
|
3
|
+
Idempotent VPS configuration in TypeScript.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/paratix)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Paratix configures Linux servers over SSH using TypeScript playbooks. Each playbook declares the desired state of a server -- packages, files, services, firewall rules -- and Paratix enforces that state idempotently. Every module follows a check/apply pattern: `check` determines whether work is needed, `apply` makes it so.
|
|
12
|
+
|
|
13
|
+
If you have used Ansible, the idea is the same. The difference is that you write TypeScript instead of YAML, with full type safety and your existing toolchain.
|
|
14
|
+
|
|
15
|
+
## Getting started
|
|
16
|
+
|
|
17
|
+
Scaffold a new project:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm create paratix my-server
|
|
21
|
+
# or: pnpm create paratix my-server
|
|
22
|
+
# or: yarn create paratix my-server
|
|
23
|
+
# or: bun create paratix my-server
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This creates a project with the following structure:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
my-server/
|
|
30
|
+
server.ts # Your playbook
|
|
31
|
+
files/ # Templates and config files
|
|
32
|
+
package.json
|
|
33
|
+
tsconfig.json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Open `server.ts` and define your server:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { server } from "paratix"
|
|
40
|
+
import { hostname, package as pkg } from "paratix/modules"
|
|
41
|
+
|
|
42
|
+
export default server({
|
|
43
|
+
name: "web-01",
|
|
44
|
+
host: "10.0.0.1",
|
|
45
|
+
ssh: {
|
|
46
|
+
user: "root",
|
|
47
|
+
ports: [22],
|
|
48
|
+
privateKey: "~/.ssh/id_ed25519",
|
|
49
|
+
},
|
|
50
|
+
run: [hostname.set("web-01"), pkg.update("2025-03-01"), pkg.installed("nginx", "curl", "git")],
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Apply the playbook:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx paratix apply server.ts
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Preview changes without applying them:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx paratix apply server.ts --dry-run
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Core concepts
|
|
67
|
+
|
|
68
|
+
### Playbook
|
|
69
|
+
|
|
70
|
+
A playbook is a TypeScript file that default-exports a `server()` call. It declares the target host, SSH credentials, environment variables, and an ordered list of modules to run.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { server } from "paratix"
|
|
74
|
+
|
|
75
|
+
export default server({
|
|
76
|
+
name: "web-01",
|
|
77
|
+
host: "10.0.0.1",
|
|
78
|
+
ssh: { user: "root", ports: [22], privateKey: "~/.ssh/id_ed25519" },
|
|
79
|
+
env: {
|
|
80
|
+
DOMAIN: "example.com",
|
|
81
|
+
APP_PORT: 3000,
|
|
82
|
+
},
|
|
83
|
+
run: [
|
|
84
|
+
// modules go here
|
|
85
|
+
],
|
|
86
|
+
signals: [
|
|
87
|
+
// fire after run if anything changed
|
|
88
|
+
],
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Module
|
|
93
|
+
|
|
94
|
+
A module is the smallest unit of configuration. Each module has a `check` function (is the desired state already in place?) and an `apply` function (enforce the desired state). Modules are idempotent: running them twice produces the same result as running them once.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { package as pkg, service } from "paratix/modules"
|
|
98
|
+
|
|
99
|
+
pkg.installed("nginx") // installs nginx if missing, does nothing if present
|
|
100
|
+
service.running("nginx") // starts nginx if stopped, does nothing if running
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Recipe
|
|
104
|
+
|
|
105
|
+
A recipe groups related modules into a reusable unit with its own signals. Signals on a recipe fire only when a module inside that recipe reported a change.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { recipe } from "paratix"
|
|
109
|
+
import { package as pkg, file, service } from "paratix/modules"
|
|
110
|
+
|
|
111
|
+
const nginx = recipe(
|
|
112
|
+
"nginx",
|
|
113
|
+
[
|
|
114
|
+
pkg.installed("nginx"),
|
|
115
|
+
file.template("/etc/nginx/nginx.conf", "./files/nginx.conf.tmpl"),
|
|
116
|
+
service.enabled("nginx"),
|
|
117
|
+
service.running("nginx"),
|
|
118
|
+
],
|
|
119
|
+
{
|
|
120
|
+
signals: [service.reload("nginx")],
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
If the config file changes, `service.reload("nginx")` fires. If nothing changed, the reload is skipped.
|
|
126
|
+
|
|
127
|
+
### Signals
|
|
128
|
+
|
|
129
|
+
Signals are modules that run only when something changed. Use them for actions like reloading a service after a config file update.
|
|
130
|
+
|
|
131
|
+
At the server level, `signals` fire when any module in `run` reported a change. Inside a recipe, signals fire only when that recipe's modules changed.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
export default server({
|
|
135
|
+
// ...
|
|
136
|
+
run: [file.template("/etc/myapp/config.yml", "./files/config.yml.tmpl")],
|
|
137
|
+
signals: [service.restart("myapp")],
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Note:** `service.restart()` and `service.reload()` always apply when called. Place them in `signals`, not directly in `run`.
|
|
142
|
+
|
|
143
|
+
### Environment
|
|
144
|
+
|
|
145
|
+
The `env` object makes values available to templates and conditional logic. Values can be strings, numbers, or async functions (resolved lazily on first access).
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
export default server({
|
|
149
|
+
// ...
|
|
150
|
+
env: {
|
|
151
|
+
DOMAIN: "example.com",
|
|
152
|
+
APP_PORT: 3000,
|
|
153
|
+
DB_PASSWORD: async () => fetchFromVault("db-password"),
|
|
154
|
+
},
|
|
155
|
+
run: [
|
|
156
|
+
/* ... */
|
|
157
|
+
],
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Modules can return `meta` in their result, which merges into the environment for subsequent modules.
|
|
162
|
+
|
|
163
|
+
### Templates
|
|
164
|
+
|
|
165
|
+
`file.template()` deploys a file with `{{KEY}}` placeholders resolved from the environment.
|
|
166
|
+
|
|
167
|
+
Template file (`files/nginx.conf.tmpl`):
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
server {
|
|
171
|
+
listen 80;
|
|
172
|
+
server_name {{DOMAIN}};
|
|
173
|
+
proxy_pass http://127.0.0.1:{{APP_PORT}};
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
file.template("/etc/nginx/sites-available/default", "./files/nginx.conf.tmpl")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Use `\{{` to produce a literal `{{` in the output. Unknown keys throw an error at runtime.
|
|
182
|
+
|
|
183
|
+
## CLI reference
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
paratix apply <file>
|
|
187
|
+
|
|
188
|
+
Options:
|
|
189
|
+
--dry-run Check only, don't apply
|
|
190
|
+
--env <key=value> Set environment variable (repeatable)
|
|
191
|
+
--env-file <path> Load .env file
|
|
192
|
+
--reconnect-timeout <s> SSH reconnect timeout in seconds (default: 300)
|
|
193
|
+
--verbose Show full stack traces on error
|
|
194
|
+
--version Show version number
|
|
195
|
+
--help Show help
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Module reference
|
|
199
|
+
|
|
200
|
+
All modules are imported from `"paratix/modules"`.
|
|
201
|
+
|
|
202
|
+
**Note:** `package` is a reserved word in JavaScript. Import it as: `import { package as pkg } from "paratix/modules"`.
|
|
203
|
+
|
|
204
|
+
### System
|
|
205
|
+
|
|
206
|
+
| Namespace | Methods |
|
|
207
|
+
| ---------- | --------------------------- |
|
|
208
|
+
| `hostname` | `set` |
|
|
209
|
+
| `system` | `facts`, `reboot`, `uptime` |
|
|
210
|
+
| `sysctl` | `set` |
|
|
211
|
+
| `mount` | `present`, `absent` |
|
|
212
|
+
|
|
213
|
+
### Packages
|
|
214
|
+
|
|
215
|
+
| Namespace | Methods |
|
|
216
|
+
| ---------------- | --------------------------------------------- |
|
|
217
|
+
| `package` | `installed`, `absent`, `update`, `upgrade` |
|
|
218
|
+
| `apt` | `debconf`, `distUpgrade`, `key`, `repository` |
|
|
219
|
+
| `releaseUpgrade` | `upgrade` |
|
|
220
|
+
|
|
221
|
+
### Files
|
|
222
|
+
|
|
223
|
+
| Namespace | Methods |
|
|
224
|
+
| ---------- | ------------------------------------------------------------------------------------------------------- |
|
|
225
|
+
| `file` | `absent`, `assemble`, `block`, `copy`, `directory`, `line`, `properties`, `replace`, `stat`, `template` |
|
|
226
|
+
| `archive` | `extract` |
|
|
227
|
+
| `download` | `url`, `github`, `large` |
|
|
228
|
+
| `git` | `clone` |
|
|
229
|
+
| `rsync` | `sync` |
|
|
230
|
+
|
|
231
|
+
### Services
|
|
232
|
+
|
|
233
|
+
| Namespace | Methods |
|
|
234
|
+
| --------- | ------------------------------------------------------------------------- |
|
|
235
|
+
| `service` | `running`, `stopped`, `enabled`, `disabled`, `restart`, `reload`, `facts` |
|
|
236
|
+
| `systemd` | `unit`, `daemonReload`, `masked`, `unmasked` |
|
|
237
|
+
|
|
238
|
+
### Users and groups
|
|
239
|
+
|
|
240
|
+
| Namespace | Methods |
|
|
241
|
+
| --------- | ------------------- |
|
|
242
|
+
| `user` | `present`, `absent` |
|
|
243
|
+
| `group` | `present`, `absent` |
|
|
244
|
+
|
|
245
|
+
### Network and security
|
|
246
|
+
|
|
247
|
+
| Namespace | Methods |
|
|
248
|
+
| --------- | ------------------------------ |
|
|
249
|
+
| `ufw` | `enabled`, `rule` |
|
|
250
|
+
| `ssh` | `authorizedKeys`, `knownHosts` |
|
|
251
|
+
| `sshd` | `config`, `port` |
|
|
252
|
+
|
|
253
|
+
### Scheduling
|
|
254
|
+
|
|
255
|
+
| Namespace | Methods |
|
|
256
|
+
| --------- | ------- |
|
|
257
|
+
| `cron` | `job` |
|
|
258
|
+
|
|
259
|
+
### Commands
|
|
260
|
+
|
|
261
|
+
| Namespace | Methods |
|
|
262
|
+
| --------- | ------- |
|
|
263
|
+
| `command` | `shell` |
|
|
264
|
+
|
|
265
|
+
### Secrets
|
|
266
|
+
|
|
267
|
+
| Namespace | Methods |
|
|
268
|
+
| --------- | --------- |
|
|
269
|
+
| `op` | `resolve` |
|
|
270
|
+
|
|
271
|
+
## Custom modules
|
|
272
|
+
|
|
273
|
+
A custom module is an object with `name`, `check`, and `apply`:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import type { Module, ModuleResult, SshConnection, Environment } from "paratix"
|
|
277
|
+
import { NEEDS_APPLY } from "paratix"
|
|
278
|
+
|
|
279
|
+
function ensureFile(path: string, content: string): Module {
|
|
280
|
+
return {
|
|
281
|
+
name: `ensure-file: ${path}`,
|
|
282
|
+
|
|
283
|
+
async check(ssh: SshConnection | null, env: Environment): Promise<"needs-apply" | "ok"> {
|
|
284
|
+
if (!ssh) return NEEDS_APPLY
|
|
285
|
+
const current = await ssh.readFile(path).catch(() => null)
|
|
286
|
+
return current === content ? "ok" : NEEDS_APPLY
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
async apply(ssh: SshConnection | null, env: Environment): Promise<ModuleResult> {
|
|
290
|
+
if (!ssh) return { status: "failed" }
|
|
291
|
+
await ssh.writeFile(path, content)
|
|
292
|
+
return { status: "changed" }
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Rules for custom modules:
|
|
299
|
+
|
|
300
|
+
- `check` returns `"ok"` or `NEEDS_APPLY` (use the exported constant, not the string `"needs-apply"`).
|
|
301
|
+
- `apply` returns `{ status }` where status is `"changed"`, `"failed"`, `"ok"`, or `"skipped"`.
|
|
302
|
+
- Always handle the case where `ssh` is `null` (happens for local-only modules).
|
|
303
|
+
- Return `meta` from `apply` to pass data to subsequent modules via the environment.
|
|
304
|
+
- Set `local: true` on the module object if it runs on the local machine instead of over SSH.
|
|
305
|
+
|
|
306
|
+
## SshConnection API
|
|
307
|
+
|
|
308
|
+
Methods available on the `ssh` parameter in custom modules:
|
|
309
|
+
|
|
310
|
+
| Method | Returns | Description |
|
|
311
|
+
| ----------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
312
|
+
| `exec(cmd, options?)` | `Promise<ExecResult>` | Run a command. Returns `{ code, stdout, stderr }`. Throws on non-zero exit unless `ignoreExitCode: true`. |
|
|
313
|
+
| `test(cmd)` | `Promise<boolean>` | Run a command, return `true` if exit code is 0. |
|
|
314
|
+
| `output(cmd)` | `Promise<string>` | Run a command, return trimmed stdout. |
|
|
315
|
+
| `lines(cmd)` | `Promise<string[]>` | Run a command, return stdout split into lines. |
|
|
316
|
+
| `exists(path)` | `Promise<boolean>` | Check if a remote path exists. |
|
|
317
|
+
| `readFile(path)` | `Promise<string>` | Read a remote file. |
|
|
318
|
+
| `writeFile(path, content)` | `Promise<void>` | Write content to a remote file. |
|
|
319
|
+
| `uploadFile(local, remote)` | `Promise<void>` | Upload a local file via SFTP. |
|
|
320
|
+
| `downloadFile(remote, local)` | `Promise<void>` | Download a remote file. |
|
|
321
|
+
| `sha256(path)` | `Promise<string \| null>` | Get SHA-256 hex digest, or `null` if the file does not exist. |
|
|
322
|
+
|
|
323
|
+
Additional methods (`addPort`, `disconnect`, `getConnectionInfo`, `probeSudo`, `updateHost`) are available for advanced use cases. See the type definitions for details.
|
|
324
|
+
|
|
325
|
+
## Built-in helpers
|
|
326
|
+
|
|
327
|
+
Import these from `"paratix"`:
|
|
328
|
+
|
|
329
|
+
| Helper | Description |
|
|
330
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------- |
|
|
331
|
+
| `recipe(name, modules, opts?)` | Group modules into a reusable unit with optional signals. See [Recipes](#recipe) above. |
|
|
332
|
+
| `assert(condition, message)` | Abort the run if `condition` returns false. The condition receives the current environment. |
|
|
333
|
+
| `when(condition, ...modules)` | Run modules only if `condition` returns true. Skipped modules report `"skipped"`, not `"failed"`. |
|
|
334
|
+
| `debug(message)` | Print a debug message during the run. |
|
|
335
|
+
| `fail(message)` | Abort the run unconditionally with a failure. |
|
|
336
|
+
| `pause(message?)` | Wait for the operator to press Enter before continuing. |
|
|
337
|
+
| `shellQuote(value)` | Safely quote a string for shell interpolation. |
|
|
338
|
+
| `NEEDS_APPLY` | Constant to return from `check` when work is needed. |
|
|
339
|
+
|
|
340
|
+
## LLM Guide
|
|
341
|
+
|
|
342
|
+
This package includes an `llm-guide.md` file that provides detailed information for writing Paratix modules and playbooks. It covers the complete API reference, code patterns, and common mistakes to avoid. When using an LLM to generate Paratix code, point it at this file for best results.
|
|
343
|
+
|
|
344
|
+
## License
|
|
345
|
+
|
|
346
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "paratix",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Idempotent VPS setup tool in TypeScript",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"paratix",
|
|
8
|
+
"vps",
|
|
9
|
+
"server",
|
|
10
|
+
"ssh",
|
|
11
|
+
"deployment",
|
|
12
|
+
"idempotent",
|
|
13
|
+
"typescript",
|
|
14
|
+
"devops",
|
|
15
|
+
"infrastructure",
|
|
16
|
+
"automation"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://paratix.oss.sebastian-software.com",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/sebastian-software/paratix.git",
|
|
22
|
+
"directory": "packages/paratix"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/sebastian-software/paratix/issues"
|
|
26
|
+
},
|
|
27
|
+
"author": "Sebastian Fastner <s.fastner@sebastian-software.de>",
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|