@rip-lang/stamp 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,9 +4,9 @@
4
4
 
5
5
  **Declarative host provisioning — no state file, no agent, no YAML.**
6
6
 
7
- Stamp reads a Stampfile, resolves each directive to a handler, and reconciles
8
- the declared state against reality using three operations: **check**, **apply**,
9
- and **verify**.
7
+ Stamp reads a Stampfile, resolves each directive to a handler, and
8
+ reconciles the declared state against reality. No state file to lose.
9
+ No agent to install. No YAML to wrestle. Just a blueprint and an engine.
10
10
 
11
11
  ```
12
12
  stamp apply Hostfile
@@ -14,49 +14,64 @@ stamp apply Hostfile
14
14
 
15
15
  ---
16
16
 
17
+ ## Why Stamp?
18
+
19
+ - **No state file.** Every handler queries the live system. The Stampfile
20
+ IS the source of truth. Running `stamp apply` twice is always safe.
21
+ - **Tiny directives.** Each handler is 30–50 lines of Rip. Three functions:
22
+ `check`, `apply`, `verify`. No imports, no boilerplate.
23
+ - **Injection-safe by default.** Shell commands use `$"..."` tagged templates —
24
+ interpolated values can never become shell code.
25
+ - **Pluggable.** Drop a `.rip` file in `directives/` and it just works.
26
+ Community handlers are npm packages with three exported functions.
27
+ - **Cross-platform.** Works on macOS (Homebrew, Multipass) and Linux
28
+ (apt-get, ZFS, Incus, systemd).
29
+
30
+ ---
31
+
17
32
  ## Quick Start
18
33
 
19
34
  ```bash
20
- # Preview changes (read-only)
21
- stamp plan Hostfile
35
+ stamp plan Hostfile # preview what would change
36
+ stamp apply Hostfile # make it so
37
+ stamp verify Hostfile # audit current state
38
+ ```
22
39
 
23
- # Apply the declared state
24
- stamp apply Hostfile
40
+ All three commands are read-safe. `plan` and `verify` never modify
41
+ anything. `apply` only changes what doesn't match.
25
42
 
26
- # Audit current state
27
- stamp verify Hostfile
28
- ```
43
+ ---
29
44
 
30
45
  ## Stampfile Syntax
31
46
 
32
47
  A Stampfile declares the desired state of a system. Each top-level entry
33
48
  names a resource using a **directive**. Indented lines below it describe
34
- properties.
49
+ properties. The file is read top to bottom — order IS the dependency order.
50
+
51
+ ### Inline — one line, done
35
52
 
36
53
  ```
37
54
  packages curl git jq zfsutils-linux
55
+ ```
38
56
 
39
- pool tank /dev/sdb
40
- compression zstd
41
- atime off
57
+ ### Block — directive + properties
42
58
 
59
+ ```
43
60
  container web ubuntu/24.04
44
61
  profile trusted
45
62
  disk data /tank/web -> /data
46
63
  start
47
-
48
- firewall
49
- default deny incoming
50
- default allow outgoing
51
- allow ssh
52
- allow 443/tcp
53
64
  ```
54
65
 
55
- ### Three levels of expression
66
+ ### Expanded property with its own sub-block
56
67
 
57
- - **Inline** — single line: `packages curl git jq`
58
- - **Block** — directive + indented properties
59
- - **Expanded** — property with its own nested sub-block
68
+ ```
69
+ container web ubuntu/24.04
70
+ disk data
71
+ source /tank/web
72
+ path /data
73
+ readonly true
74
+ ```
60
75
 
61
76
  ### Variables
62
77
 
@@ -65,11 +80,17 @@ set POOL tank
65
80
  set DEVICE /dev/sdb
66
81
 
67
82
  pool $POOL $DEVICE
83
+ compression zstd
84
+ atime off
85
+ mountpoint /tank
68
86
  ```
69
87
 
88
+ Variables are expanded before parsing. `$NAME` and `${NAME}` both work.
89
+ Undefined variables expand to the empty string.
90
+
70
91
  ### The `->` operator
71
92
 
72
- Source-to-destination mapping:
93
+ Source-to-destination mapping for disks, mounts, and similar:
73
94
 
74
95
  ```
75
96
  disk data /tank/web -> /data
@@ -88,78 +109,207 @@ datasets
88
109
  mode 2775
89
110
  ```
90
111
 
91
- ## Built-in Directives
112
+ This is syntactic sugar — `datasets` decomposes to individual `dataset`
113
+ calls before dispatching.
92
114
 
93
- | Directive | Purpose |
94
- |-----------|---------|
95
- | `brew` | Homebrew packages (macOS) |
96
- | `packages` | System packages (apt-get) |
97
- | `pool` | ZFS pool creation |
98
- | `dataset` | ZFS dataset with ownership/permissions |
99
- | `profile` | Incus profile configuration |
100
- | `container` | Incus container management |
101
- | `incus` | Incus daemon initialization |
102
- | `multipass` | Multipass virtual machines |
103
- | `user` | System user management |
104
- | `group` | System group management |
105
- | `firewall` | ufw firewall rules |
106
- | `ssh` | SSH daemon configuration |
107
- | `service` | systemd service management |
115
+ ---
108
116
 
109
- ## Handler Contract
117
+ ## Examples
110
118
 
111
- Every directive handler exports three async functions:
119
+ ### macOS: Create an Ubuntu VM with Multipass
112
120
 
113
- - **check(name, props)** — returns `"ok"`, `"drift"`, or `"missing"`
114
- - **apply(name, props)** — make reality match the declaration
115
- - **verify(name, props)** — return `[{ status, message }]` results
121
+ ```
122
+ brew multipass
116
123
 
117
- ## Plugin System
124
+ multipass stamp 24.04
125
+ cpus 2
126
+ memory 4G
127
+ disk 20G
128
+ start
118
129
 
119
- Handlers resolve in this order:
130
+ ensure unzip
131
+ check multipass exec stamp -- which unzip
132
+ apply multipass exec stamp -- sudo apt-get update -qq
133
+ apply multipass exec stamp -- sudo apt-get install -y -qq unzip
120
134
 
121
- 1. Built-in (`directives/`)
122
- 2. Local (`./directives/` beside Stampfile)
123
- 3. Installed (`~/.stamp/directives/`)
124
- 4. npm (`@stamp/<name>` or `stamp-<name>`)
125
- 5. Remote (via `use` directive)
135
+ ensure bun
136
+ check multipass exec stamp -- test -x /home/ubuntu/.bun/bin/bun
137
+ apply multipass exec stamp -- bash -lc "curl -fsSL https://bun.sh/install | bash"
126
138
 
127
- ### Writing a directive
139
+ ensure rip
140
+ check multipass exec stamp -- test -x /home/ubuntu/.bun/bin/rip
141
+ apply multipass exec stamp -- /home/ubuntu/.bun/bin/bun add -g rip-lang
142
+ ```
128
143
 
129
- ```coffee
130
- import { sh, ok, run } from "@rip-lang/stamp/helpers"
144
+ One file installs Multipass, creates a VM, and bootstraps Bun + Rip
145
+ inside it. Run it again everything shows `ok`, nothing changes.
146
+
147
+ ### Linux: Provision an Incus + ZFS host
148
+
149
+ ```
150
+ set POOL tank
151
+ set DEVICE /dev/disk/by-id/scsi-0Linode_Volume_tank
152
+ set MOUNT /tank
153
+
154
+ packages
155
+ zfsutils-linux
156
+ incus
157
+ openssh-server
158
+ fail2ban
159
+ ufw
160
+
161
+ pool $POOL $DEVICE
162
+ compression zstd
163
+ atime off
164
+ mountpoint $MOUNT
165
+
166
+ dataset $POOL/home
167
+ dataset $POOL/home/web
168
+ owner 1000:1000
131
169
 
170
+ incus
171
+ storage default zfs $POOL/incus/default
172
+ network incusbr0
173
+
174
+ profile trusted
175
+ security.privileged true
176
+ limits.memory 2GB
177
+ limits.cpu 2
178
+
179
+ container web ubuntu/24.04
180
+ profile trusted
181
+ disk home $MOUNT/home/web -> /home
182
+ user shreeve 1000:1000
183
+ start
184
+
185
+ firewall
186
+ default deny incoming
187
+ default allow outgoing
188
+ allow ssh
189
+
190
+ service fail2ban
191
+
192
+ ssh
193
+ password-auth no
194
+ permit-root-login prohibit-password
195
+ pubkey-auth yes
196
+ ```
197
+
198
+ This replaces ~600 lines of bash scripts, preseed templates, and
199
+ numbered setup files with a single declarative file.
200
+
201
+ ---
202
+
203
+ ## Built-in Directives
204
+
205
+ | Directive | Purpose | Platform |
206
+ |-------------|----------------------------------------|----------|
207
+ | `brew` | Homebrew packages | macOS |
208
+ | `packages` | System packages (apt-get) | Linux |
209
+ | `ensure` | Guarded imperative commands | any |
210
+ | `pool` | ZFS pool creation | Linux |
211
+ | `dataset` | ZFS dataset with ownership/permissions | Linux |
212
+ | `profile` | Incus profile configuration | Linux |
213
+ | `container` | Incus container management | Linux |
214
+ | `incus` | Incus daemon initialization | Linux |
215
+ | `multipass` | Multipass virtual machines | macOS |
216
+ | `user` | System user management | Linux |
217
+ | `group` | System group management | Linux |
218
+ | `firewall` | ufw firewall rules | Linux |
219
+ | `ssh` | SSH daemon configuration | Linux |
220
+ | `service` | systemd service management | Linux |
221
+
222
+ ---
223
+
224
+ ## Writing a Directive
225
+
226
+ A directive is a `.rip` file that exports three functions. That's it.
227
+ No imports needed — `sh`, `ok`, and `run` are available globally.
228
+
229
+ ```coffee
132
230
  export name = "mydirective"
133
231
  export description = "What it does"
134
232
 
135
233
  export check = (name, props) ->
136
- return 'missing' unless ok "some-check"
234
+ return 'missing' unless ok $"some-check #{name}"
137
235
  'ok'
138
236
 
139
237
  export apply = (name, props) ->
140
- sh "some-command"
238
+ sh $"some-command #{name}"
141
239
 
142
240
  export verify = (name, props) ->
143
- [{ status: 'pass', message: 'looks good' }]
241
+ results = []
242
+ if ok $"some-check #{name}"
243
+ results.push { status: 'pass', message: "#{name} is good" }
244
+ else
245
+ results.push { status: 'fail', message: "#{name} is missing" }
246
+ results
144
247
  ```
145
248
 
249
+ ### Shell helpers
250
+
251
+ | Function | Returns | Use case |
252
+ |--------------|----------------------------------|------------------------|
253
+ | `sh $"cmd"` | stdout string, throws on failure | Do the thing |
254
+ | `ok $"cmd"` | boolean | Does this exist? |
255
+ | `run $"cmd"` | `{ ok, stdout, stderr, code }` | Need the full picture |
256
+ | `sh [array]` | stdout string, throws on failure | Dynamic argument lists |
257
+
258
+ The `$"..."` syntax prevents shell injection — interpolated values are
259
+ passed as separate arguments, never interpreted by a shell.
260
+
261
+ ### Handler contract
262
+
263
+ - **check** has no side effects. Returns `"ok"`, `"drift"`, or `"missing"`.
264
+ - **apply** is idempotent. Only called when check returns something other
265
+ than `"ok"`. The engine runs a post-apply re-check to confirm success.
266
+ - **verify** has no side effects. Returns `[{ status, message }]` results
267
+ where status is `"pass"`, `"warn"`, or `"fail"`.
268
+
269
+ ### Plugin resolution
270
+
271
+ Handlers resolve in this order:
272
+
273
+ 1. **Built-in** — `directives/` in the stamp package
274
+ 2. **Local** — `./directives/` beside the Stampfile
275
+ 3. **Installed** — `~/.stamp/directives/`
276
+ 4. **npm** — `@stamp/<name>` or `stamp-<name>`
277
+ 5. **Remote** — fetched via `use` directive in the Stampfile
278
+
279
+ The first match wins. Drop a file in `./directives/` beside your
280
+ Stampfile to override any built-in.
281
+
282
+ ---
283
+
146
284
  ## CLI
147
285
 
148
286
  ```
149
- stamp apply [file] Reconcile system to match Stampfile
150
- stamp verify [file] Check current state, report PASS/WARN/FAIL
151
- stamp plan [file] Dry-run: show what apply would do
152
- stamp list Show all available directives
153
- stamp info <directive> Show a directive's syntax and properties
154
- stamp version Print version
155
- stamp help Show help
287
+ stamp apply [file] Reconcile system to match Stampfile
288
+ stamp verify [file] Check current state, report PASS/WARN/FAIL
289
+ stamp plan [file] Dry-run: show what apply would do
290
+ stamp list Show all available directives
291
+ stamp info <directive> Show a directive's syntax and properties
292
+ stamp version Print version
293
+ stamp help Show help
156
294
  ```
157
295
 
158
296
  Default file search: `Stampfile`, `Hostfile`, `Containerfile`.
159
297
 
298
+ ### Exit codes
299
+
300
+ | Code | Meaning |
301
+ |------|-----------------------------------------------------------------|
302
+ | 0 | Success (apply completed, verify had no FAILs, plan found nothing) |
303
+ | 1 | Failure (apply error, verify had FAILs, plan found changes) |
304
+ | 2 | Usage error (bad arguments, file not found) |
305
+
306
+ ---
307
+
160
308
  ## Runtime
161
309
 
162
- Bun + Rip. Zero dependencies beyond `rip-lang`.
310
+ Bun + Rip. Zero dependencies beyond `rip-lang`. The entire engine —
311
+ parser, handler resolution, execution loop, and shell helpers — is
312
+ 524 lines of Rip.
163
313
 
164
314
  ## License
165
315
 
@@ -15,7 +15,7 @@ export check = (path, props) ->
15
15
  'ok'
16
16
 
17
17
  export apply = (path, props) ->
18
- sh $"zfs create #{path}" unless ok $"zfs list #{path}"
18
+ sh $"zfs create -p #{path}" unless ok $"zfs list #{path}"
19
19
  mountpoint = sh $"zfs get -H -o value mountpoint #{path}"
20
20
  sh $"chown #{props.owner[0].args[0]} #{mountpoint}" if props.owner
21
21
  sh $"chmod #{props.mode[0].args[0]} #{mountpoint}" if props.mode
@@ -0,0 +1,29 @@
1
+ export name = "ensure"
2
+ export description = "Ensures a condition is met, running commands if not"
3
+ export positional = ["name"]
4
+ export properties =
5
+ check: { description: "Command to test — exit 0 means ok" }
6
+ apply: { description: "Commands to run if check fails", multi: true }
7
+
8
+ getCheck = (props) ->
9
+ props.check?[0]?.args
10
+
11
+ export check = (name, props) ->
12
+ return 'ok' unless args = getCheck(props)
13
+ if ok args then 'ok' else 'missing'
14
+
15
+ export apply = (name, props) ->
16
+ if props.apply
17
+ for entry in props.apply
18
+ sh entry.args
19
+
20
+ export verify = (name, props) ->
21
+ results = []
22
+ unless args = getCheck props
23
+ results.push { status: 'warn', message: "#{name} has no check command" }
24
+ return results
25
+ if ok args
26
+ results.push { status: 'pass', message: "#{name} is present" }
27
+ else
28
+ results.push { status: 'fail', message: "#{name} is missing" }
29
+ results
@@ -18,13 +18,14 @@ export check = (name, props) ->
18
18
  status = sh $"ufw status verbose"
19
19
  for entry in props.default
20
20
  [action, direction] = entry.args
21
- return 'drift' unless status.includes action
21
+ return 'drift' unless status.includes "#{action} (#{direction})"
22
22
 
23
23
  if props.allow
24
- status = sh $"ufw status"
24
+ status ??= sh $"ufw status verbose"
25
25
  for entry in props.allow
26
26
  rule = entry.args.join ' '
27
- return 'drift' unless status.toUpperCase().includes rule.toUpperCase()
27
+ port = (run $"getent services #{rule}").stdout.split(/\s+/)?[1] or ''
28
+ return 'drift' unless status.toUpperCase().includes(rule.toUpperCase()) or status.includes(port)
28
29
 
29
30
  'ok'
30
31
 
@@ -65,14 +66,19 @@ export verify = (name, props) ->
65
66
  status = sh $"ufw status verbose"
66
67
  for entry in props.default
67
68
  [action, direction] = entry.args
68
- if status.includes action
69
+ if status.includes "#{action} (#{direction})"
69
70
  results.push { status: 'pass', message: "default #{action} #{direction}" }
70
71
  else
71
72
  results.push { status: 'fail', message: "default #{action} #{direction} not set" }
72
73
 
73
74
  if props.allow
75
+ status ??= sh $"ufw status verbose"
74
76
  for entry in props.allow
75
77
  rule = entry.args.join ' '
76
- results.push { status: 'pass', message: "allow #{rule}" }
78
+ port = (run $"getent services #{rule}").stdout.split(/\s+/)?[1] or ''
79
+ if status.toUpperCase().includes(rule.toUpperCase()) or status.includes(port)
80
+ results.push { status: 'pass', message: "allow #{rule}" }
81
+ else
82
+ results.push { status: 'fail', message: "allow #{rule} not found" }
77
83
 
78
84
  results
@@ -11,6 +11,7 @@ export check = (name, props) ->
11
11
  if props.storage
12
12
  poolName = props.storage[0].args[0]
13
13
  return 'drift' unless ok $"incus storage show #{poolName}"
14
+ return 'drift' unless ok $"incus profile device get default root pool"
14
15
 
15
16
  if props.network
16
17
  bridge = props.network[0].args[0]
@@ -76,14 +77,16 @@ export apply = (name, props) ->
76
77
  raise "incus admin init failed: #{out.stderr.toString().trim()}"
77
78
  else
78
79
  if props.storage
79
- poolName = props.storage[0].args[0]
80
+ [poolName, driver, source] = props.storage[0].args
80
81
  unless ok $"incus storage show #{poolName}"
81
- raise "Incus already initialized but storage pool '#{poolName}' does not exist"
82
+ sh $"incus storage create #{poolName} #{driver} source=#{source}"
83
+ unless ok $"incus profile device get default root pool"
84
+ sh $"incus profile device add default root disk path=/ pool=#{poolName}"
82
85
 
83
86
  if props.network
84
87
  bridge = props.network[0].args[0]
85
88
  unless ok $"incus network show #{bridge}"
86
- raise "Incus already initialized but network '#{bridge}' does not exist"
89
+ sh $"incus network create #{bridge}"
87
90
 
88
91
  export verify = (name, props) ->
89
92
  results = []
@@ -9,6 +9,12 @@ export properties =
9
9
 
10
10
  export check = (name, props) ->
11
11
  return 'missing' unless ok $"zpool list #{name}"
12
+ if want = props.compression?[0]?.args?[0]
13
+ return 'drift' if want isnt sh $"zfs get -H -o value compression #{name}"
14
+ if want = props.atime?[0]?.args?[0]
15
+ return 'drift' if want isnt sh $"zfs get -H -o value atime #{name}"
16
+ if want = props.mountpoint?[0]?.args?[0]
17
+ return 'drift' if want isnt sh $"zfs get -H -o value mountpoint #{name}"
12
18
  'ok'
13
19
 
14
20
  export apply = (name, props) ->
@@ -16,8 +22,8 @@ export apply = (name, props) ->
16
22
  raise "pool '#{name}' requires a device argument" unless device
17
23
 
18
24
  unless ok $"zpool list #{name}"
19
- raise "Block device not found: #{device}" unless ok $"test -b #{device}"
20
- raise "Device #{device} has existing data. Run 'wipefs -a #{device}' first." if ok $"blkid #{device}"
25
+ raise "Device not found: #{device}" unless ok $"test -b #{device}" or ok $"test -f #{device}"
26
+ raise "Device #{device} has existing data. Run 'wipefs -a #{device}' first." if ok $"test -b #{device}" and ok $"blkid #{device}"
21
27
 
22
28
  ashift = props.ashift?[0]?.args?[0] or '12'
23
29
  sh $"zpool create -f -o ashift=#{ashift} #{name} #{device}"
@@ -15,6 +15,12 @@ OPTION_MAP =
15
15
  'permit-root-login': 'PermitRootLogin'
16
16
  'pubkey-auth': 'PubkeyAuthentication'
17
17
 
18
+ SYNONYMS =
19
+ 'without-password': 'prohibit-password'
20
+
21
+ normalize = (val) ->
22
+ SYNONYMS[val] or val
23
+
18
24
  export check = (name, props) ->
19
25
  out = run $"sshd -T"
20
26
  return 'missing' unless out.ok
@@ -24,7 +30,7 @@ export check = (name, props) ->
24
30
  sshdKey = OPTION_MAP[key]?.toLowerCase()
25
31
  continue unless sshdKey
26
32
  match = out.stdout.match new RegExp "^#{sshdKey}\\s+(\\S+)", 'mi'
27
- return 'drift' if match?[1] != want
33
+ return 'drift' if normalize(match?[1]) != want
28
34
 
29
35
  'ok'
30
36
 
@@ -55,7 +61,7 @@ export verify = (name, props) ->
55
61
  sshdKey = OPTION_MAP[key]?.toLowerCase()
56
62
  continue unless sshdKey
57
63
  match = out.stdout.match new RegExp "^#{sshdKey}\\s+(\\S+)", 'mi'
58
- actual = match?[1]
64
+ actual = normalize match?[1]
59
65
  if actual == want
60
66
  results.push { status: 'pass', message: "#{key} is #{want}" }
61
67
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/stamp",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "author": "Steve Shreeve <steve.shreeve@gmail.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,6 +19,7 @@
19
19
  "bin/",
20
20
  "src/",
21
21
  "directives/",
22
+ "stamps/",
22
23
  "README.md"
23
24
  ],
24
25
  "homepage": "https://github.com/shreeve/rip-lang/tree/main/packages/stamp#readme",
@@ -40,6 +41,6 @@
40
41
  },
41
42
  "type": "module",
42
43
  "dependencies": {
43
- "rip-lang": ">=3.13.106"
44
+ "rip-lang": ">=3.13.108"
44
45
  }
45
46
  }
package/src/cli.rip CHANGED
@@ -3,7 +3,8 @@ import * as path from 'path'
3
3
  import { parse } from './parser.rip'
4
4
  import { execute } from './engine.rip'
5
5
 
6
- VERSION = '0.1.0'
6
+ pkg = JSON.parse fs.readFileSync path.join(import.meta.dir, '..', 'package.json'), 'utf-8'
7
+ VERSION = pkg.version
7
8
 
8
9
  # ── Default file resolution ──────────────────────────────────────────────────
9
10
 
@@ -37,7 +38,7 @@ showVersion = ->
37
38
  # ── Subcommands ──────────────────────────────────────────────────────────────
38
39
 
39
40
  runMode = (mode, file) ->
40
- stampfile = file or findStampfile!
41
+ stampfile = file or findStampfile()
41
42
 
42
43
  unless stampfile
43
44
  warn "No Stampfile found. Provide a file or create Stampfile, Hostfile, or Containerfile."
@@ -63,7 +64,7 @@ runMode = (mode, file) ->
63
64
  exit 1
64
65
 
65
66
  listDirectives = ->
66
- builtins = ['brew', 'packages', 'pool', 'dataset', 'profile', 'container', 'incus', 'multipass', 'user', 'group', 'firewall', 'ssh', 'service']
67
+ builtins = ['brew', 'packages', 'ensure', 'pool', 'dataset', 'profile', 'container', 'incus', 'multipass', 'user', 'group', 'firewall', 'ssh', 'service']
67
68
  p "\nBuilt-in directives:\n"
68
69
  for name in builtins
69
70
  p " #{name}"
package/src/engine.rip CHANGED
@@ -11,9 +11,11 @@ globalThis.run ??= run
11
11
 
12
12
  STAMP_HOME = process.env.STAMP_HOME or path.join(process.env.HOME or '', '.stamp')
13
13
 
14
+ BUILTIN_DIR = path.join import.meta.dir, '..', 'directives'
15
+
14
16
  resolveHandler = (type, stampDir) ->
15
17
  candidates = [
16
- path.join(stampDir, '..', 'directives', "#{type}.rip") # Built-in
18
+ path.join(BUILTIN_DIR, "#{type}.rip") # Built-in
17
19
  path.join(stampDir, 'directives', "#{type}.rip") # Local (beside Stampfile)
18
20
  path.join(stampDir, 'directives', "#{type}.js")
19
21
  path.join(STAMP_HOME, 'directives', "#{type}.rip") # Installed
@@ -80,18 +82,19 @@ export apply = (directives, handlers) ->
80
82
  for dir in directives
81
83
  handler = handlers[dir.type]
82
84
  props = propsFor dir
85
+ label = dir.name or dir.args?.join(' ') or null
83
86
  result = handler.check! dir.name, props
84
87
 
85
88
  if result == 'ok'
86
- log.ok dir.type, dir.name
89
+ log.ok dir.type, label
87
90
  else
88
- log.apply dir.type, dir.name
91
+ log.apply dir.type, label
89
92
  handler.apply! dir.name, props
90
93
 
91
94
  after = handler.check! dir.name, props
92
95
  if after != 'ok'
93
- log.fail dir.type, dir.name, "still not ok after apply"
94
- throw new Error "Directive '#{dir.type}#{if dir.name then ' ' + dir.name else ''}' failed post-apply check"
96
+ log.fail dir.type, label, "still not ok after apply"
97
+ raise "Directive '#{dir.type}#{if label then ' ' + label else ''}' failed post-apply check"
95
98
  applied++
96
99
 
97
100
  p "\nApplied #{applied} of #{total} directives."
@@ -120,12 +123,13 @@ export plan = (directives, handlers) ->
120
123
  for dir in directives
121
124
  handler = handlers[dir.type]
122
125
  props = propsFor dir
126
+ label = dir.name or dir.args?.join(' ') or null
123
127
  result = handler.check! dir.name, props
124
128
 
125
129
  switch result
126
- when 'ok' then log.ok dir.type, dir.name
127
- when 'drift' then log.update dir.type, dir.name; changes++
128
- when 'missing' then log.create dir.type, dir.name; changes++
130
+ when 'ok' then log.ok dir.type, label
131
+ when 'drift' then log.update dir.type, label; changes++
132
+ when 'missing' then log.create dir.type, label; changes++
129
133
 
130
134
  if changes > 0
131
135
  p "\n#{changes} change#{if changes > 1 then 's' else ''} would be applied."
package/src/parser.rip CHANGED
@@ -159,7 +159,7 @@ export parse = (source, env = {}) ->
159
159
  singular = PLURALS[dir.type]
160
160
  if singular
161
161
  for own pname, props of dir.properties
162
- sub = { type: singular, name: pname, args: [], properties: {}, line: dir.line }
162
+ sub = { type: singular, name: pname, args: props[0]?.args or [], properties: {}, line: dir.line }
163
163
  for prop in props when prop.args.length or prop.source? or prop.sub?
164
164
  for own k, v of (prop.sub ?? {})
165
165
  sub.properties[k] = v
package/stamps/basic ADDED
@@ -0,0 +1,17 @@
1
+ # Test Stampfile
2
+
3
+ set NAME testhost
4
+ set POOL tank
5
+
6
+ packages curl git jq
7
+
8
+ group trust 1101
9
+ group jail 1002
10
+
11
+ user shreeve 1100:1100
12
+ groups trust
13
+
14
+ firewall
15
+ default deny incoming
16
+ default allow outgoing
17
+ allow ssh
package/stamps/host ADDED
@@ -0,0 +1,102 @@
1
+ # Hostfile — test parsing only
2
+
3
+ set POOL tank
4
+ set DEVICE /dev/disk/by-id/scsi-0Linode_Volume_tank
5
+ set MOUNT /tank
6
+ set INCUS_DS incus/default
7
+ set SHARED shared/common
8
+
9
+ # ── system packages ──────────────────────────────────────────
10
+
11
+ packages
12
+ zfsutils-linux
13
+ incus
14
+ uidmap
15
+ acl
16
+ gettext-base
17
+ openssh-server
18
+ ripgrep
19
+ fail2ban
20
+ ufw
21
+ unattended-upgrades
22
+
23
+ # ── ZFS pool and datasets ───────────────────────────────────
24
+
25
+ pool $POOL $DEVICE
26
+ ashift 12
27
+ compression zstd
28
+ atime off
29
+ mountpoint $MOUNT
30
+
31
+ dataset $POOL/$INCUS_DS
32
+
33
+ dataset $POOL/home
34
+ dataset $POOL/home/foo
35
+ owner 1000:1000
36
+ dataset $POOL/home/bar
37
+ owner 1000:1000
38
+
39
+ dataset $POOL/$SHARED
40
+ owner 1001:1001
41
+ mode 2775
42
+
43
+ # ── host users and groups ───────────────────────────────────
44
+
45
+ user shreeve 1000:1000
46
+ groups trust
47
+
48
+ user trust 1001:1001
49
+
50
+ group jail 1002
51
+
52
+ # ── Incus initialization ────────────────────────────────────
53
+
54
+ incus
55
+ storage default zfs $POOL/$INCUS_DS
56
+ network incusbr0
57
+
58
+ # ── Incus profiles ──────────────────────────────────────────
59
+
60
+ profile trusted
61
+ security.privileged true
62
+ limits.memory 2GB
63
+ limits.cpu 2
64
+
65
+ profile hardened
66
+ security.idmap.isolated true
67
+ security.nesting false
68
+ limits.memory 1GB
69
+ limits.cpu 1
70
+ limits.processes 512
71
+
72
+ # ── containers ──────────────────────────────────────────────
73
+
74
+ container foo ubuntu/24.04
75
+ profile trusted
76
+ disk home $MOUNT/home/foo -> /home
77
+ disk shared $MOUNT/$SHARED -> /shared
78
+ user shreeve 1000:1000
79
+ user trust 1001:1001
80
+ start
81
+
82
+ container bar ubuntu/24.04
83
+ profile trusted
84
+ disk home $MOUNT/home/bar -> /home
85
+ disk shared $MOUNT/$SHARED -> /shared readonly
86
+ user shreeve 1000:1000
87
+ user trust 1001:1001
88
+ start
89
+
90
+ # ── security ────────────────────────────────────────────────
91
+
92
+ firewall
93
+ default deny incoming
94
+ default allow outgoing
95
+ allow ssh
96
+
97
+ service fail2ban
98
+
99
+ ssh
100
+ password-auth no
101
+ permit-root-login prohibit-password
102
+ pubkey-auth yes
@@ -0,0 +1,22 @@
1
+ # VMfile — Ubuntu 24.04 VM on macOS via Multipass
2
+
3
+ brew multipass
4
+
5
+ multipass stamp 24.04
6
+ cpus 2
7
+ memory 4G
8
+ disk 20G
9
+ start
10
+
11
+ ensure unzip
12
+ check multipass exec stamp -- which unzip
13
+ apply multipass exec stamp -- sudo apt-get update -qq
14
+ apply multipass exec stamp -- sudo apt-get install -y -qq unzip
15
+
16
+ ensure bun
17
+ check multipass exec stamp -- test -x /home/ubuntu/.bun/bin/bun
18
+ apply multipass exec stamp -- bash -lc "curl -fsSL https://bun.sh/install | bash"
19
+
20
+ ensure rip
21
+ check multipass exec stamp -- test -x /home/ubuntu/.bun/bin/rip
22
+ apply multipass exec stamp -- /home/ubuntu/.bun/bin/bun add -g rip-lang
package/stamps/mac-vm ADDED
@@ -0,0 +1,95 @@
1
+ # VMHostfile — Full Incus + ZFS host inside a Multipass VM
2
+ #
3
+ # Adapted from Hostfile for local VM testing:
4
+ # - File-backed ZFS pool (no block device)
5
+ # - UIDs offset to avoid conflict with ubuntu (1000)
6
+
7
+ set POOL tank
8
+ set MOUNT /tank
9
+ set INCUS_DS incus/default
10
+ set SHARED shared/common
11
+
12
+ # ── system packages ──────────────────────────────────────────
13
+
14
+ packages
15
+ zfsutils-linux
16
+ incus
17
+ uidmap
18
+ acl
19
+ gettext-base
20
+ openssh-server
21
+ ripgrep
22
+ fail2ban
23
+ ufw
24
+ unattended-upgrades
25
+
26
+ # ── ZFS pool (file-backed for VM testing) ────────────────────
27
+
28
+ ensure tank-image
29
+ check test -f /tmp/tank.img
30
+ apply truncate -s 2G /tmp/tank.img
31
+
32
+ pool $POOL /tmp/tank.img
33
+ compression zstd
34
+ atime off
35
+ mountpoint $MOUNT
36
+
37
+ # ── ZFS datasets ─────────────────────────────────────────────
38
+
39
+ dataset $POOL/$INCUS_DS
40
+
41
+ dataset $POOL/home
42
+ dataset $POOL/home/foo
43
+ owner 1100:1100
44
+ dataset $POOL/home/bar
45
+ owner 1100:1100
46
+
47
+ dataset $POOL/$SHARED
48
+ owner 1101:1101
49
+ mode 2775
50
+
51
+ # ── host users and groups ────────────────────────────────────
52
+
53
+ group trust 1101
54
+ group jail 1002
55
+
56
+ user shreeve 1100:1100
57
+ groups trust
58
+
59
+ user trust 1101:1101
60
+
61
+ # ── Incus initialization ─────────────────────────────────────
62
+
63
+ incus
64
+ storage default zfs $POOL/$INCUS_DS
65
+ network incusbr0
66
+
67
+ # ── Incus profiles ───────────────────────────────────────────
68
+
69
+ profile trusted
70
+ security.privileged true
71
+ limits.memory 1GB
72
+ limits.cpu 1
73
+
74
+ # ── containers ───────────────────────────────────────────────
75
+
76
+ container foo images:ubuntu/24.04
77
+ profile trusted
78
+ disk home $MOUNT/home/foo -> /home
79
+ disk shared $MOUNT/$SHARED -> /shared
80
+ user shreeve 1100:1100
81
+ start
82
+
83
+ # ── security ─────────────────────────────────────────────────
84
+
85
+ firewall
86
+ default deny incoming
87
+ default allow outgoing
88
+ allow ssh
89
+
90
+ service fail2ban
91
+
92
+ ssh
93
+ password-auth no
94
+ permit-root-login prohibit-password
95
+ pubkey-auth yes