@runfile/cli 0.12.0 → 0.13.0
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/bin/darwin-arm64/README.md +97 -8
- package/bin/darwin-arm64/run +0 -0
- package/bin/darwin-x64/README.md +97 -8
- package/bin/darwin-x64/run +0 -0
- package/bin/linux-arm64/README.md +97 -8
- package/bin/linux-arm64/run +0 -0
- package/bin/linux-x64/README.md +97 -8
- package/bin/linux-x64/run +0 -0
- package/bin/win32-arm64/run.exe +0 -0
- package/bin/win32-x64/run.exe +0 -0
- package/package.json +1 -1
|
@@ -111,16 +111,16 @@ Sorted from where Runfile is most differentiated to where it lags behind.
|
|
|
111
111
|
| Argument substitution with chained fallbacks | ✅ | ❌ | ✅ | ❌ |
|
|
112
112
|
| JSON Schema autocomplete in editors | ✅ | ❌ | ❌ | ✅ |
|
|
113
113
|
| Watch mode, built-in | ✅ | ❌ | ❌ | ✅ |
|
|
114
|
+
| Output grouping / prefixing in parallel mode | ✅ | ❌ | ❌ | ✅ |
|
|
114
115
|
| Native Windows (no WSL / msys2 needed) | ✅ | ❌ | ✅ | ✅ |
|
|
115
|
-
| Parallel execution | ✅ | ✅ | ❌ | ✅ |
|
|
116
116
|
| Shell completions | ✅ | ❌ | ✅ | ✅ |
|
|
117
117
|
| Private / internal targets (`_name`) | ✅ | ❌ | ✅ | ✅ |
|
|
118
|
+
| Built-in string functions (upper, replace, trim, regex, base64, …) | ✅ | ❌ | ✅ | ✅ |
|
|
119
|
+
| Parallel execution | ✅ | ✅ | ❌ | ✅ |
|
|
118
120
|
| Single static binary | ✅ | ✅ | ✅ | ✅ |
|
|
119
121
|
| Pattern rules (`%.o: %.c`) | ❌ | ✅ | ❌ | ❌ |
|
|
120
|
-
| Output grouping / prefixing in parallel mode | ❌ | ❌ | ❌ | ✅ |
|
|
121
122
|
| Preconditions / status checks | ❌ | ❌ | ❌ | ✅ |
|
|
122
123
|
| Incremental builds (sources / timestamps / checksums) | ❌ | ✅ | ❌ | ✅ |
|
|
123
|
-
| Built-in string functions (upper, replace, trim, …) | ❌ | ❌ | ✅ | ✅ |
|
|
124
124
|
|
|
125
125
|
---
|
|
126
126
|
|
|
@@ -167,9 +167,9 @@ $ run :env inject -f .env.production -- ./deploy.sh # encrypted values aut
|
|
|
167
167
|
|
|
168
168
|
#### Powerful argument substitution
|
|
169
169
|
|
|
170
|
-
Positional, named, flags, env vars, and runtime context (`{{ RUN.os }}` / `{{ RUN.
|
|
171
|
-
`{{ RUN.file }}` / `{{ RUN.parent }}`) — with chained fallbacks and required values. The
|
|
172
|
-
avoids collisions with shell `$(...)` command substitution.
|
|
170
|
+
Positional, named, flags, env vars, and runtime context (`{{ RUN.os }}` / `{{ RUN.arch }}` / `{{ RUN.shell }}` /
|
|
171
|
+
`{{ RUN.cwd }}` / `{{ RUN.file }}` / `{{ RUN.parent }}`) — with chained fallbacks and required values. The
|
|
172
|
+
`{{ ... }}` syntax avoids collisions with shell `$(...)` command substitution.
|
|
173
173
|
|
|
174
174
|
```jsonc
|
|
175
175
|
"PORT": "{{ ARGS.port ? ENV.PORT ? '3000' }}",
|
|
@@ -242,9 +242,15 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
242
242
|
// transform : escape, repeat, replace_all, remove_all
|
|
243
243
|
// regex : regex_replace, regex_remove, regex_matches
|
|
244
244
|
// build : concat, join
|
|
245
|
+
// split : nth, first, last, count_parts
|
|
245
246
|
// encoding : base64_encode, base64_decode
|
|
247
|
+
// hashing : sha256, md5
|
|
248
|
+
// files : read_file, file_exists
|
|
249
|
+
// json : json_get, json_set
|
|
250
|
+
// error : try
|
|
246
251
|
// shell : shell_quote
|
|
247
252
|
// variables : define
|
|
253
|
+
// cwd : set_cwd
|
|
248
254
|
"echo deploying-{{ to_upper(ARGS.env) }}",
|
|
249
255
|
"curl -H \"X-Auth: {{ base64_encode(ENV.TOKEN) }}\" ...",
|
|
250
256
|
"echo {{ concat('hello-', ARGS.name, '-2026') }}",
|
|
@@ -253,6 +259,13 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
253
259
|
"go test {{ replace_all(ARGS.flags, ' ', ' -tag=') }}",
|
|
254
260
|
// Strip every match of a regex (here: collapse whitespace runs to a single space):
|
|
255
261
|
"echo {{ regex_replace(ARGS.text, '\\s+', ' ') }}",
|
|
262
|
+
// Split-by-separator scalar accessors — string in, string out, no list type:
|
|
263
|
+
// basename idiom (last segment after `/`); empty string when input ends in `/`.
|
|
264
|
+
"echo basename={{ last(ARGS.path, '/') }}",
|
|
265
|
+
// Pull out the N-th comma-separated field; out-of-bounds returns "".
|
|
266
|
+
"echo target={{ nth(ARGS.csv, ',', '1') }}",
|
|
267
|
+
// Pair `count_parts` with `nth` to bound-check before indexing:
|
|
268
|
+
// "if": "{{ count_parts(ARGS.csv, ',') == '3' }}"
|
|
256
269
|
// Boolean-returning helpers are valid DSL `Truthy` values — use them in `if`:
|
|
257
270
|
// "if": "{{ starts_with(ARGS.path, '/usr') }}"
|
|
258
271
|
// "if": "{{ regex_matches(ARGS.tag, '^v[0-9]+\\.[0-9]+$') }}"
|
|
@@ -261,6 +274,28 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
261
274
|
// `shell_quote` picks the right quoting for the active shell:
|
|
262
275
|
"some-tool --json {{ shell_quote(base64_decode(ENV.SECRET_BASE64)) }}",
|
|
263
276
|
|
|
277
|
+
// Cache keys / content fingerprints. `sha256` for security-sensitive use,
|
|
278
|
+
// `md5` for cheap fingerprinting (NOT cryptographically secure).
|
|
279
|
+
"echo cache-key={{ sha256(read_file('package-lock.json')) }}",
|
|
280
|
+
|
|
281
|
+
// Branch on file presence without forking a shell. `file_exists` returns
|
|
282
|
+
// the literal "true" / "false", so it doubles as a DSL truthy value.
|
|
283
|
+
// "if": "{{ file_exists('.env.local') }}"
|
|
284
|
+
|
|
285
|
+
// Read a file inline — relative paths anchor to the Runfile directory.
|
|
286
|
+
// Use `try(...)` to recover from missing files.
|
|
287
|
+
"echo version={{ try(read_file('VERSION')) ? '0.0.0-dev' }}",
|
|
288
|
+
|
|
289
|
+
// Pull values out of arbitrary JSON without `jq` (works on every shell):
|
|
290
|
+
"echo db_host={{ json_get(read_file('config.json'), 'database.host') }}",
|
|
291
|
+
|
|
292
|
+
// Modify a JSON document in place (returns the new compact JSON):
|
|
293
|
+
"echo {{ json_set('{\"port\":3000}', 'env', 'production') }}",
|
|
294
|
+
|
|
295
|
+
// `try(expr)` swallows inner errors. Standalone returns "" on failure;
|
|
296
|
+
// chained, the next segment runs as a fallback.
|
|
297
|
+
"echo {{ try(base64_decode(ARGS.maybe_b64)) ? ARGS.maybe_b64 }}",
|
|
298
|
+
|
|
264
299
|
// Nested:
|
|
265
300
|
"echo {{ to_upper(to_lower(ARGS.x)) }}",
|
|
266
301
|
|
|
@@ -272,6 +307,13 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
272
307
|
"{{ VARS.sdk }}/bin/build",
|
|
273
308
|
"echo using sdk at {{ VARS.sdk }}",
|
|
274
309
|
|
|
310
|
+
// `set_cwd(path)` switches the cwd subsequent commands spawn in — like
|
|
311
|
+
// shell `cd`, but works on every shell / OS without forking. Relative
|
|
312
|
+
// paths chain (matching `cd a; cd b` → `a/b`); absolute paths replace.
|
|
313
|
+
"{{ set_cwd(ARGS.subproject ? 'packages/api') }}",
|
|
314
|
+
"npm install",
|
|
315
|
+
"npm run build",
|
|
316
|
+
|
|
275
317
|
// Single-quoted strings interpolate nested {{ }} substitutions:
|
|
276
318
|
"{{ define(cmd, 'docker compose -f {{ VARS.compose }} pull') }}",
|
|
277
319
|
"{{ VARS.cmd }}"
|
|
@@ -283,6 +325,22 @@ to only whitespace (i.e. one consisting solely of a `{{ define(...) }}` call) is
|
|
|
283
325
|
being dispatched to the shell. `define`s in a parent target are visible to `@target` children. The `name` MUST
|
|
284
326
|
be a bareword identifier matching `[A-Za-z_][A-Za-z0-9_-]*` — quotes are NOT allowed on the name.
|
|
285
327
|
|
|
328
|
+
`set_cwd(path)` is the cwd analog of `define`: returns an empty string and changes the cwd that subsequent shell
|
|
329
|
+
commands in the *current target* spawn in. Behaves like shell `cd`, but works uniformly across every shell / OS
|
|
330
|
+
(no forked process, no `cd` binary needed). Resolution rules:
|
|
331
|
+
|
|
332
|
+
- **Absolute path** → fully replaces the current override (matches `cd /abs`).
|
|
333
|
+
- **Relative path** → joins onto the existing override, or onto the target's `workingDirectory` if no override
|
|
334
|
+
has been set yet. So `set_cwd('a'); set_cwd('b')` lands subsequent commands in `<workingDirectory>/a/b`,
|
|
335
|
+
matching how `cd a; cd b` chains in a shell.
|
|
336
|
+
|
|
337
|
+
`set_cwd` is **per-target**: each `@target` invocation starts with a clean override (the dispatched target sees
|
|
338
|
+
its own `workingDirectory`, not the caller's `set_cwd` state). Inside `parallel: true` targets each leaf
|
|
339
|
+
captures the override at the moment of its substitution, so siblings don't race on the spawn cwd. Like
|
|
340
|
+
`define`, the side effect is skipped on the redacted-logging pass so log lines don't double-apply it. With
|
|
341
|
+
`sameShell: true`, only the *final* `set_cwd` value applies (everything joins into one shell invocation) — use
|
|
342
|
+
shell `cd` directly between leaves there if you need intermediate cwd changes.
|
|
343
|
+
|
|
286
344
|
Function args are separated by `, ` (comma + exactly one space).
|
|
287
345
|
|
|
288
346
|
**Quote semantics inside `{{ ... }}`:**
|
|
@@ -311,13 +369,18 @@ evaluated by Runfile itself, so the logic works the same on every shell and plat
|
|
|
311
369
|
"cases": {
|
|
312
370
|
"1": "flutter emulators --launch Tier_1_Android_9",
|
|
313
371
|
"2": "flutter emulators --launch Tier_2_Android_11",
|
|
314
|
-
"3": "flutter emulators --launch Tier_3_Android_14"
|
|
372
|
+
"3": "flutter emulators --launch Tier_3_Android_14",
|
|
373
|
+
// Case keys wrapped in `/.../` are treated as regex patterns.
|
|
374
|
+
// Literal cases always win over a regex that would also match.
|
|
375
|
+
"/^v\\d+$/": "echo version-tag",
|
|
376
|
+
"/^pr-\\d+$/": "echo preview-build"
|
|
315
377
|
} }, // unknown values error out, listing the valid cases
|
|
316
378
|
|
|
317
379
|
{ "for": "service",
|
|
318
380
|
"in": ["api", "web", "worker"],
|
|
319
381
|
"parallel": true,
|
|
320
|
-
|
|
382
|
+
// VARS.<name>_index exposes the 0-based iteration counter.
|
|
383
|
+
"do": ["echo [{{ VARS.service_index }}] docker build -t {{ VARS.service }} services/{{ VARS.service }}"] },
|
|
321
384
|
|
|
322
385
|
{ "for": "file",
|
|
323
386
|
"shell": "git diff --name-only HEAD~1",
|
|
@@ -379,6 +442,27 @@ Wrap any commands in a `when:` block to run them only after a failure, or always
|
|
|
379
442
|
]
|
|
380
443
|
```
|
|
381
444
|
|
|
445
|
+
#### Single-shell mode with `sameShell`
|
|
446
|
+
|
|
447
|
+
By default each step runs in its own shell process, so `cd`, exported variables, and other shell state don't carry
|
|
448
|
+
over between steps. Set `"sameShell": true` to join every step into a single shell invocation — state changes
|
|
449
|
+
persist for free.
|
|
450
|
+
|
|
451
|
+
```jsonc
|
|
452
|
+
{
|
|
453
|
+
"deploy": {
|
|
454
|
+
"sameShell": true,
|
|
455
|
+
"commands": ["cd ci-scripts/", "./ci-deploy.sh"]
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Steps are joined with `&&` so the run stops at the first failure (or `;` / `&` when `ignoreErrors: true`). `if`,
|
|
461
|
+
`for`, and `match` blocks are evaluated by Runfile and their chosen branches flow into the same joined invocation.
|
|
462
|
+
`@target` calls inside the body are rejected — they have their own shell context and can't share state with the
|
|
463
|
+
parent. `sameShell` also composes with `detach: true` (joined command spawns as one detached process) and is
|
|
464
|
+
available on `globals` for project-wide defaults.
|
|
465
|
+
|
|
382
466
|
#### Watch mode, built in
|
|
383
467
|
|
|
384
468
|
Add a `watch` array to any target. No flags, no extra tooling.
|
|
@@ -397,6 +481,11 @@ $ run :generate zed-tasks
|
|
|
397
481
|
$ run :generate jetbrains-run-configurations
|
|
398
482
|
```
|
|
399
483
|
|
|
484
|
+
Skip a target from the generated configs by setting `metadata.excludeFromGenerateCommand: true`. The
|
|
485
|
+
`metadata` block on `globals` and on each target is **fully open** — any property of any JSON type
|
|
486
|
+
(strings, numbers, booleans, arrays, deeply-nested objects) is accepted and round-trips untouched, so
|
|
487
|
+
editor extensions and CI scripts can stash arbitrary tooling-specific fields here.
|
|
488
|
+
|
|
400
489
|
#### Migrate in seconds
|
|
401
490
|
|
|
402
491
|
```bash
|
package/bin/darwin-arm64/run
CHANGED
|
Binary file
|
package/bin/darwin-x64/README.md
CHANGED
|
@@ -111,16 +111,16 @@ Sorted from where Runfile is most differentiated to where it lags behind.
|
|
|
111
111
|
| Argument substitution with chained fallbacks | ✅ | ❌ | ✅ | ❌ |
|
|
112
112
|
| JSON Schema autocomplete in editors | ✅ | ❌ | ❌ | ✅ |
|
|
113
113
|
| Watch mode, built-in | ✅ | ❌ | ❌ | ✅ |
|
|
114
|
+
| Output grouping / prefixing in parallel mode | ✅ | ❌ | ❌ | ✅ |
|
|
114
115
|
| Native Windows (no WSL / msys2 needed) | ✅ | ❌ | ✅ | ✅ |
|
|
115
|
-
| Parallel execution | ✅ | ✅ | ❌ | ✅ |
|
|
116
116
|
| Shell completions | ✅ | ❌ | ✅ | ✅ |
|
|
117
117
|
| Private / internal targets (`_name`) | ✅ | ❌ | ✅ | ✅ |
|
|
118
|
+
| Built-in string functions (upper, replace, trim, regex, base64, …) | ✅ | ❌ | ✅ | ✅ |
|
|
119
|
+
| Parallel execution | ✅ | ✅ | ❌ | ✅ |
|
|
118
120
|
| Single static binary | ✅ | ✅ | ✅ | ✅ |
|
|
119
121
|
| Pattern rules (`%.o: %.c`) | ❌ | ✅ | ❌ | ❌ |
|
|
120
|
-
| Output grouping / prefixing in parallel mode | ❌ | ❌ | ❌ | ✅ |
|
|
121
122
|
| Preconditions / status checks | ❌ | ❌ | ❌ | ✅ |
|
|
122
123
|
| Incremental builds (sources / timestamps / checksums) | ❌ | ✅ | ❌ | ✅ |
|
|
123
|
-
| Built-in string functions (upper, replace, trim, …) | ❌ | ❌ | ✅ | ✅ |
|
|
124
124
|
|
|
125
125
|
---
|
|
126
126
|
|
|
@@ -167,9 +167,9 @@ $ run :env inject -f .env.production -- ./deploy.sh # encrypted values aut
|
|
|
167
167
|
|
|
168
168
|
#### Powerful argument substitution
|
|
169
169
|
|
|
170
|
-
Positional, named, flags, env vars, and runtime context (`{{ RUN.os }}` / `{{ RUN.
|
|
171
|
-
`{{ RUN.file }}` / `{{ RUN.parent }}`) — with chained fallbacks and required values. The
|
|
172
|
-
avoids collisions with shell `$(...)` command substitution.
|
|
170
|
+
Positional, named, flags, env vars, and runtime context (`{{ RUN.os }}` / `{{ RUN.arch }}` / `{{ RUN.shell }}` /
|
|
171
|
+
`{{ RUN.cwd }}` / `{{ RUN.file }}` / `{{ RUN.parent }}`) — with chained fallbacks and required values. The
|
|
172
|
+
`{{ ... }}` syntax avoids collisions with shell `$(...)` command substitution.
|
|
173
173
|
|
|
174
174
|
```jsonc
|
|
175
175
|
"PORT": "{{ ARGS.port ? ENV.PORT ? '3000' }}",
|
|
@@ -242,9 +242,15 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
242
242
|
// transform : escape, repeat, replace_all, remove_all
|
|
243
243
|
// regex : regex_replace, regex_remove, regex_matches
|
|
244
244
|
// build : concat, join
|
|
245
|
+
// split : nth, first, last, count_parts
|
|
245
246
|
// encoding : base64_encode, base64_decode
|
|
247
|
+
// hashing : sha256, md5
|
|
248
|
+
// files : read_file, file_exists
|
|
249
|
+
// json : json_get, json_set
|
|
250
|
+
// error : try
|
|
246
251
|
// shell : shell_quote
|
|
247
252
|
// variables : define
|
|
253
|
+
// cwd : set_cwd
|
|
248
254
|
"echo deploying-{{ to_upper(ARGS.env) }}",
|
|
249
255
|
"curl -H \"X-Auth: {{ base64_encode(ENV.TOKEN) }}\" ...",
|
|
250
256
|
"echo {{ concat('hello-', ARGS.name, '-2026') }}",
|
|
@@ -253,6 +259,13 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
253
259
|
"go test {{ replace_all(ARGS.flags, ' ', ' -tag=') }}",
|
|
254
260
|
// Strip every match of a regex (here: collapse whitespace runs to a single space):
|
|
255
261
|
"echo {{ regex_replace(ARGS.text, '\\s+', ' ') }}",
|
|
262
|
+
// Split-by-separator scalar accessors — string in, string out, no list type:
|
|
263
|
+
// basename idiom (last segment after `/`); empty string when input ends in `/`.
|
|
264
|
+
"echo basename={{ last(ARGS.path, '/') }}",
|
|
265
|
+
// Pull out the N-th comma-separated field; out-of-bounds returns "".
|
|
266
|
+
"echo target={{ nth(ARGS.csv, ',', '1') }}",
|
|
267
|
+
// Pair `count_parts` with `nth` to bound-check before indexing:
|
|
268
|
+
// "if": "{{ count_parts(ARGS.csv, ',') == '3' }}"
|
|
256
269
|
// Boolean-returning helpers are valid DSL `Truthy` values — use them in `if`:
|
|
257
270
|
// "if": "{{ starts_with(ARGS.path, '/usr') }}"
|
|
258
271
|
// "if": "{{ regex_matches(ARGS.tag, '^v[0-9]+\\.[0-9]+$') }}"
|
|
@@ -261,6 +274,28 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
261
274
|
// `shell_quote` picks the right quoting for the active shell:
|
|
262
275
|
"some-tool --json {{ shell_quote(base64_decode(ENV.SECRET_BASE64)) }}",
|
|
263
276
|
|
|
277
|
+
// Cache keys / content fingerprints. `sha256` for security-sensitive use,
|
|
278
|
+
// `md5` for cheap fingerprinting (NOT cryptographically secure).
|
|
279
|
+
"echo cache-key={{ sha256(read_file('package-lock.json')) }}",
|
|
280
|
+
|
|
281
|
+
// Branch on file presence without forking a shell. `file_exists` returns
|
|
282
|
+
// the literal "true" / "false", so it doubles as a DSL truthy value.
|
|
283
|
+
// "if": "{{ file_exists('.env.local') }}"
|
|
284
|
+
|
|
285
|
+
// Read a file inline — relative paths anchor to the Runfile directory.
|
|
286
|
+
// Use `try(...)` to recover from missing files.
|
|
287
|
+
"echo version={{ try(read_file('VERSION')) ? '0.0.0-dev' }}",
|
|
288
|
+
|
|
289
|
+
// Pull values out of arbitrary JSON without `jq` (works on every shell):
|
|
290
|
+
"echo db_host={{ json_get(read_file('config.json'), 'database.host') }}",
|
|
291
|
+
|
|
292
|
+
// Modify a JSON document in place (returns the new compact JSON):
|
|
293
|
+
"echo {{ json_set('{\"port\":3000}', 'env', 'production') }}",
|
|
294
|
+
|
|
295
|
+
// `try(expr)` swallows inner errors. Standalone returns "" on failure;
|
|
296
|
+
// chained, the next segment runs as a fallback.
|
|
297
|
+
"echo {{ try(base64_decode(ARGS.maybe_b64)) ? ARGS.maybe_b64 }}",
|
|
298
|
+
|
|
264
299
|
// Nested:
|
|
265
300
|
"echo {{ to_upper(to_lower(ARGS.x)) }}",
|
|
266
301
|
|
|
@@ -272,6 +307,13 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
272
307
|
"{{ VARS.sdk }}/bin/build",
|
|
273
308
|
"echo using sdk at {{ VARS.sdk }}",
|
|
274
309
|
|
|
310
|
+
// `set_cwd(path)` switches the cwd subsequent commands spawn in — like
|
|
311
|
+
// shell `cd`, but works on every shell / OS without forking. Relative
|
|
312
|
+
// paths chain (matching `cd a; cd b` → `a/b`); absolute paths replace.
|
|
313
|
+
"{{ set_cwd(ARGS.subproject ? 'packages/api') }}",
|
|
314
|
+
"npm install",
|
|
315
|
+
"npm run build",
|
|
316
|
+
|
|
275
317
|
// Single-quoted strings interpolate nested {{ }} substitutions:
|
|
276
318
|
"{{ define(cmd, 'docker compose -f {{ VARS.compose }} pull') }}",
|
|
277
319
|
"{{ VARS.cmd }}"
|
|
@@ -283,6 +325,22 @@ to only whitespace (i.e. one consisting solely of a `{{ define(...) }}` call) is
|
|
|
283
325
|
being dispatched to the shell. `define`s in a parent target are visible to `@target` children. The `name` MUST
|
|
284
326
|
be a bareword identifier matching `[A-Za-z_][A-Za-z0-9_-]*` — quotes are NOT allowed on the name.
|
|
285
327
|
|
|
328
|
+
`set_cwd(path)` is the cwd analog of `define`: returns an empty string and changes the cwd that subsequent shell
|
|
329
|
+
commands in the *current target* spawn in. Behaves like shell `cd`, but works uniformly across every shell / OS
|
|
330
|
+
(no forked process, no `cd` binary needed). Resolution rules:
|
|
331
|
+
|
|
332
|
+
- **Absolute path** → fully replaces the current override (matches `cd /abs`).
|
|
333
|
+
- **Relative path** → joins onto the existing override, or onto the target's `workingDirectory` if no override
|
|
334
|
+
has been set yet. So `set_cwd('a'); set_cwd('b')` lands subsequent commands in `<workingDirectory>/a/b`,
|
|
335
|
+
matching how `cd a; cd b` chains in a shell.
|
|
336
|
+
|
|
337
|
+
`set_cwd` is **per-target**: each `@target` invocation starts with a clean override (the dispatched target sees
|
|
338
|
+
its own `workingDirectory`, not the caller's `set_cwd` state). Inside `parallel: true` targets each leaf
|
|
339
|
+
captures the override at the moment of its substitution, so siblings don't race on the spawn cwd. Like
|
|
340
|
+
`define`, the side effect is skipped on the redacted-logging pass so log lines don't double-apply it. With
|
|
341
|
+
`sameShell: true`, only the *final* `set_cwd` value applies (everything joins into one shell invocation) — use
|
|
342
|
+
shell `cd` directly between leaves there if you need intermediate cwd changes.
|
|
343
|
+
|
|
286
344
|
Function args are separated by `, ` (comma + exactly one space).
|
|
287
345
|
|
|
288
346
|
**Quote semantics inside `{{ ... }}`:**
|
|
@@ -311,13 +369,18 @@ evaluated by Runfile itself, so the logic works the same on every shell and plat
|
|
|
311
369
|
"cases": {
|
|
312
370
|
"1": "flutter emulators --launch Tier_1_Android_9",
|
|
313
371
|
"2": "flutter emulators --launch Tier_2_Android_11",
|
|
314
|
-
"3": "flutter emulators --launch Tier_3_Android_14"
|
|
372
|
+
"3": "flutter emulators --launch Tier_3_Android_14",
|
|
373
|
+
// Case keys wrapped in `/.../` are treated as regex patterns.
|
|
374
|
+
// Literal cases always win over a regex that would also match.
|
|
375
|
+
"/^v\\d+$/": "echo version-tag",
|
|
376
|
+
"/^pr-\\d+$/": "echo preview-build"
|
|
315
377
|
} }, // unknown values error out, listing the valid cases
|
|
316
378
|
|
|
317
379
|
{ "for": "service",
|
|
318
380
|
"in": ["api", "web", "worker"],
|
|
319
381
|
"parallel": true,
|
|
320
|
-
|
|
382
|
+
// VARS.<name>_index exposes the 0-based iteration counter.
|
|
383
|
+
"do": ["echo [{{ VARS.service_index }}] docker build -t {{ VARS.service }} services/{{ VARS.service }}"] },
|
|
321
384
|
|
|
322
385
|
{ "for": "file",
|
|
323
386
|
"shell": "git diff --name-only HEAD~1",
|
|
@@ -379,6 +442,27 @@ Wrap any commands in a `when:` block to run them only after a failure, or always
|
|
|
379
442
|
]
|
|
380
443
|
```
|
|
381
444
|
|
|
445
|
+
#### Single-shell mode with `sameShell`
|
|
446
|
+
|
|
447
|
+
By default each step runs in its own shell process, so `cd`, exported variables, and other shell state don't carry
|
|
448
|
+
over between steps. Set `"sameShell": true` to join every step into a single shell invocation — state changes
|
|
449
|
+
persist for free.
|
|
450
|
+
|
|
451
|
+
```jsonc
|
|
452
|
+
{
|
|
453
|
+
"deploy": {
|
|
454
|
+
"sameShell": true,
|
|
455
|
+
"commands": ["cd ci-scripts/", "./ci-deploy.sh"]
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Steps are joined with `&&` so the run stops at the first failure (or `;` / `&` when `ignoreErrors: true`). `if`,
|
|
461
|
+
`for`, and `match` blocks are evaluated by Runfile and their chosen branches flow into the same joined invocation.
|
|
462
|
+
`@target` calls inside the body are rejected — they have their own shell context and can't share state with the
|
|
463
|
+
parent. `sameShell` also composes with `detach: true` (joined command spawns as one detached process) and is
|
|
464
|
+
available on `globals` for project-wide defaults.
|
|
465
|
+
|
|
382
466
|
#### Watch mode, built in
|
|
383
467
|
|
|
384
468
|
Add a `watch` array to any target. No flags, no extra tooling.
|
|
@@ -397,6 +481,11 @@ $ run :generate zed-tasks
|
|
|
397
481
|
$ run :generate jetbrains-run-configurations
|
|
398
482
|
```
|
|
399
483
|
|
|
484
|
+
Skip a target from the generated configs by setting `metadata.excludeFromGenerateCommand: true`. The
|
|
485
|
+
`metadata` block on `globals` and on each target is **fully open** — any property of any JSON type
|
|
486
|
+
(strings, numbers, booleans, arrays, deeply-nested objects) is accepted and round-trips untouched, so
|
|
487
|
+
editor extensions and CI scripts can stash arbitrary tooling-specific fields here.
|
|
488
|
+
|
|
400
489
|
#### Migrate in seconds
|
|
401
490
|
|
|
402
491
|
```bash
|
package/bin/darwin-x64/run
CHANGED
|
Binary file
|
|
@@ -111,16 +111,16 @@ Sorted from where Runfile is most differentiated to where it lags behind.
|
|
|
111
111
|
| Argument substitution with chained fallbacks | ✅ | ❌ | ✅ | ❌ |
|
|
112
112
|
| JSON Schema autocomplete in editors | ✅ | ❌ | ❌ | ✅ |
|
|
113
113
|
| Watch mode, built-in | ✅ | ❌ | ❌ | ✅ |
|
|
114
|
+
| Output grouping / prefixing in parallel mode | ✅ | ❌ | ❌ | ✅ |
|
|
114
115
|
| Native Windows (no WSL / msys2 needed) | ✅ | ❌ | ✅ | ✅ |
|
|
115
|
-
| Parallel execution | ✅ | ✅ | ❌ | ✅ |
|
|
116
116
|
| Shell completions | ✅ | ❌ | ✅ | ✅ |
|
|
117
117
|
| Private / internal targets (`_name`) | ✅ | ❌ | ✅ | ✅ |
|
|
118
|
+
| Built-in string functions (upper, replace, trim, regex, base64, …) | ✅ | ❌ | ✅ | ✅ |
|
|
119
|
+
| Parallel execution | ✅ | ✅ | ❌ | ✅ |
|
|
118
120
|
| Single static binary | ✅ | ✅ | ✅ | ✅ |
|
|
119
121
|
| Pattern rules (`%.o: %.c`) | ❌ | ✅ | ❌ | ❌ |
|
|
120
|
-
| Output grouping / prefixing in parallel mode | ❌ | ❌ | ❌ | ✅ |
|
|
121
122
|
| Preconditions / status checks | ❌ | ❌ | ❌ | ✅ |
|
|
122
123
|
| Incremental builds (sources / timestamps / checksums) | ❌ | ✅ | ❌ | ✅ |
|
|
123
|
-
| Built-in string functions (upper, replace, trim, …) | ❌ | ❌ | ✅ | ✅ |
|
|
124
124
|
|
|
125
125
|
---
|
|
126
126
|
|
|
@@ -167,9 +167,9 @@ $ run :env inject -f .env.production -- ./deploy.sh # encrypted values aut
|
|
|
167
167
|
|
|
168
168
|
#### Powerful argument substitution
|
|
169
169
|
|
|
170
|
-
Positional, named, flags, env vars, and runtime context (`{{ RUN.os }}` / `{{ RUN.
|
|
171
|
-
`{{ RUN.file }}` / `{{ RUN.parent }}`) — with chained fallbacks and required values. The
|
|
172
|
-
avoids collisions with shell `$(...)` command substitution.
|
|
170
|
+
Positional, named, flags, env vars, and runtime context (`{{ RUN.os }}` / `{{ RUN.arch }}` / `{{ RUN.shell }}` /
|
|
171
|
+
`{{ RUN.cwd }}` / `{{ RUN.file }}` / `{{ RUN.parent }}`) — with chained fallbacks and required values. The
|
|
172
|
+
`{{ ... }}` syntax avoids collisions with shell `$(...)` command substitution.
|
|
173
173
|
|
|
174
174
|
```jsonc
|
|
175
175
|
"PORT": "{{ ARGS.port ? ENV.PORT ? '3000' }}",
|
|
@@ -242,9 +242,15 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
242
242
|
// transform : escape, repeat, replace_all, remove_all
|
|
243
243
|
// regex : regex_replace, regex_remove, regex_matches
|
|
244
244
|
// build : concat, join
|
|
245
|
+
// split : nth, first, last, count_parts
|
|
245
246
|
// encoding : base64_encode, base64_decode
|
|
247
|
+
// hashing : sha256, md5
|
|
248
|
+
// files : read_file, file_exists
|
|
249
|
+
// json : json_get, json_set
|
|
250
|
+
// error : try
|
|
246
251
|
// shell : shell_quote
|
|
247
252
|
// variables : define
|
|
253
|
+
// cwd : set_cwd
|
|
248
254
|
"echo deploying-{{ to_upper(ARGS.env) }}",
|
|
249
255
|
"curl -H \"X-Auth: {{ base64_encode(ENV.TOKEN) }}\" ...",
|
|
250
256
|
"echo {{ concat('hello-', ARGS.name, '-2026') }}",
|
|
@@ -253,6 +259,13 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
253
259
|
"go test {{ replace_all(ARGS.flags, ' ', ' -tag=') }}",
|
|
254
260
|
// Strip every match of a regex (here: collapse whitespace runs to a single space):
|
|
255
261
|
"echo {{ regex_replace(ARGS.text, '\\s+', ' ') }}",
|
|
262
|
+
// Split-by-separator scalar accessors — string in, string out, no list type:
|
|
263
|
+
// basename idiom (last segment after `/`); empty string when input ends in `/`.
|
|
264
|
+
"echo basename={{ last(ARGS.path, '/') }}",
|
|
265
|
+
// Pull out the N-th comma-separated field; out-of-bounds returns "".
|
|
266
|
+
"echo target={{ nth(ARGS.csv, ',', '1') }}",
|
|
267
|
+
// Pair `count_parts` with `nth` to bound-check before indexing:
|
|
268
|
+
// "if": "{{ count_parts(ARGS.csv, ',') == '3' }}"
|
|
256
269
|
// Boolean-returning helpers are valid DSL `Truthy` values — use them in `if`:
|
|
257
270
|
// "if": "{{ starts_with(ARGS.path, '/usr') }}"
|
|
258
271
|
// "if": "{{ regex_matches(ARGS.tag, '^v[0-9]+\\.[0-9]+$') }}"
|
|
@@ -261,6 +274,28 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
261
274
|
// `shell_quote` picks the right quoting for the active shell:
|
|
262
275
|
"some-tool --json {{ shell_quote(base64_decode(ENV.SECRET_BASE64)) }}",
|
|
263
276
|
|
|
277
|
+
// Cache keys / content fingerprints. `sha256` for security-sensitive use,
|
|
278
|
+
// `md5` for cheap fingerprinting (NOT cryptographically secure).
|
|
279
|
+
"echo cache-key={{ sha256(read_file('package-lock.json')) }}",
|
|
280
|
+
|
|
281
|
+
// Branch on file presence without forking a shell. `file_exists` returns
|
|
282
|
+
// the literal "true" / "false", so it doubles as a DSL truthy value.
|
|
283
|
+
// "if": "{{ file_exists('.env.local') }}"
|
|
284
|
+
|
|
285
|
+
// Read a file inline — relative paths anchor to the Runfile directory.
|
|
286
|
+
// Use `try(...)` to recover from missing files.
|
|
287
|
+
"echo version={{ try(read_file('VERSION')) ? '0.0.0-dev' }}",
|
|
288
|
+
|
|
289
|
+
// Pull values out of arbitrary JSON without `jq` (works on every shell):
|
|
290
|
+
"echo db_host={{ json_get(read_file('config.json'), 'database.host') }}",
|
|
291
|
+
|
|
292
|
+
// Modify a JSON document in place (returns the new compact JSON):
|
|
293
|
+
"echo {{ json_set('{\"port\":3000}', 'env', 'production') }}",
|
|
294
|
+
|
|
295
|
+
// `try(expr)` swallows inner errors. Standalone returns "" on failure;
|
|
296
|
+
// chained, the next segment runs as a fallback.
|
|
297
|
+
"echo {{ try(base64_decode(ARGS.maybe_b64)) ? ARGS.maybe_b64 }}",
|
|
298
|
+
|
|
264
299
|
// Nested:
|
|
265
300
|
"echo {{ to_upper(to_lower(ARGS.x)) }}",
|
|
266
301
|
|
|
@@ -272,6 +307,13 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
272
307
|
"{{ VARS.sdk }}/bin/build",
|
|
273
308
|
"echo using sdk at {{ VARS.sdk }}",
|
|
274
309
|
|
|
310
|
+
// `set_cwd(path)` switches the cwd subsequent commands spawn in — like
|
|
311
|
+
// shell `cd`, but works on every shell / OS without forking. Relative
|
|
312
|
+
// paths chain (matching `cd a; cd b` → `a/b`); absolute paths replace.
|
|
313
|
+
"{{ set_cwd(ARGS.subproject ? 'packages/api') }}",
|
|
314
|
+
"npm install",
|
|
315
|
+
"npm run build",
|
|
316
|
+
|
|
275
317
|
// Single-quoted strings interpolate nested {{ }} substitutions:
|
|
276
318
|
"{{ define(cmd, 'docker compose -f {{ VARS.compose }} pull') }}",
|
|
277
319
|
"{{ VARS.cmd }}"
|
|
@@ -283,6 +325,22 @@ to only whitespace (i.e. one consisting solely of a `{{ define(...) }}` call) is
|
|
|
283
325
|
being dispatched to the shell. `define`s in a parent target are visible to `@target` children. The `name` MUST
|
|
284
326
|
be a bareword identifier matching `[A-Za-z_][A-Za-z0-9_-]*` — quotes are NOT allowed on the name.
|
|
285
327
|
|
|
328
|
+
`set_cwd(path)` is the cwd analog of `define`: returns an empty string and changes the cwd that subsequent shell
|
|
329
|
+
commands in the *current target* spawn in. Behaves like shell `cd`, but works uniformly across every shell / OS
|
|
330
|
+
(no forked process, no `cd` binary needed). Resolution rules:
|
|
331
|
+
|
|
332
|
+
- **Absolute path** → fully replaces the current override (matches `cd /abs`).
|
|
333
|
+
- **Relative path** → joins onto the existing override, or onto the target's `workingDirectory` if no override
|
|
334
|
+
has been set yet. So `set_cwd('a'); set_cwd('b')` lands subsequent commands in `<workingDirectory>/a/b`,
|
|
335
|
+
matching how `cd a; cd b` chains in a shell.
|
|
336
|
+
|
|
337
|
+
`set_cwd` is **per-target**: each `@target` invocation starts with a clean override (the dispatched target sees
|
|
338
|
+
its own `workingDirectory`, not the caller's `set_cwd` state). Inside `parallel: true` targets each leaf
|
|
339
|
+
captures the override at the moment of its substitution, so siblings don't race on the spawn cwd. Like
|
|
340
|
+
`define`, the side effect is skipped on the redacted-logging pass so log lines don't double-apply it. With
|
|
341
|
+
`sameShell: true`, only the *final* `set_cwd` value applies (everything joins into one shell invocation) — use
|
|
342
|
+
shell `cd` directly between leaves there if you need intermediate cwd changes.
|
|
343
|
+
|
|
286
344
|
Function args are separated by `, ` (comma + exactly one space).
|
|
287
345
|
|
|
288
346
|
**Quote semantics inside `{{ ... }}`:**
|
|
@@ -311,13 +369,18 @@ evaluated by Runfile itself, so the logic works the same on every shell and plat
|
|
|
311
369
|
"cases": {
|
|
312
370
|
"1": "flutter emulators --launch Tier_1_Android_9",
|
|
313
371
|
"2": "flutter emulators --launch Tier_2_Android_11",
|
|
314
|
-
"3": "flutter emulators --launch Tier_3_Android_14"
|
|
372
|
+
"3": "flutter emulators --launch Tier_3_Android_14",
|
|
373
|
+
// Case keys wrapped in `/.../` are treated as regex patterns.
|
|
374
|
+
// Literal cases always win over a regex that would also match.
|
|
375
|
+
"/^v\\d+$/": "echo version-tag",
|
|
376
|
+
"/^pr-\\d+$/": "echo preview-build"
|
|
315
377
|
} }, // unknown values error out, listing the valid cases
|
|
316
378
|
|
|
317
379
|
{ "for": "service",
|
|
318
380
|
"in": ["api", "web", "worker"],
|
|
319
381
|
"parallel": true,
|
|
320
|
-
|
|
382
|
+
// VARS.<name>_index exposes the 0-based iteration counter.
|
|
383
|
+
"do": ["echo [{{ VARS.service_index }}] docker build -t {{ VARS.service }} services/{{ VARS.service }}"] },
|
|
321
384
|
|
|
322
385
|
{ "for": "file",
|
|
323
386
|
"shell": "git diff --name-only HEAD~1",
|
|
@@ -379,6 +442,27 @@ Wrap any commands in a `when:` block to run them only after a failure, or always
|
|
|
379
442
|
]
|
|
380
443
|
```
|
|
381
444
|
|
|
445
|
+
#### Single-shell mode with `sameShell`
|
|
446
|
+
|
|
447
|
+
By default each step runs in its own shell process, so `cd`, exported variables, and other shell state don't carry
|
|
448
|
+
over between steps. Set `"sameShell": true` to join every step into a single shell invocation — state changes
|
|
449
|
+
persist for free.
|
|
450
|
+
|
|
451
|
+
```jsonc
|
|
452
|
+
{
|
|
453
|
+
"deploy": {
|
|
454
|
+
"sameShell": true,
|
|
455
|
+
"commands": ["cd ci-scripts/", "./ci-deploy.sh"]
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Steps are joined with `&&` so the run stops at the first failure (or `;` / `&` when `ignoreErrors: true`). `if`,
|
|
461
|
+
`for`, and `match` blocks are evaluated by Runfile and their chosen branches flow into the same joined invocation.
|
|
462
|
+
`@target` calls inside the body are rejected — they have their own shell context and can't share state with the
|
|
463
|
+
parent. `sameShell` also composes with `detach: true` (joined command spawns as one detached process) and is
|
|
464
|
+
available on `globals` for project-wide defaults.
|
|
465
|
+
|
|
382
466
|
#### Watch mode, built in
|
|
383
467
|
|
|
384
468
|
Add a `watch` array to any target. No flags, no extra tooling.
|
|
@@ -397,6 +481,11 @@ $ run :generate zed-tasks
|
|
|
397
481
|
$ run :generate jetbrains-run-configurations
|
|
398
482
|
```
|
|
399
483
|
|
|
484
|
+
Skip a target from the generated configs by setting `metadata.excludeFromGenerateCommand: true`. The
|
|
485
|
+
`metadata` block on `globals` and on each target is **fully open** — any property of any JSON type
|
|
486
|
+
(strings, numbers, booleans, arrays, deeply-nested objects) is accepted and round-trips untouched, so
|
|
487
|
+
editor extensions and CI scripts can stash arbitrary tooling-specific fields here.
|
|
488
|
+
|
|
400
489
|
#### Migrate in seconds
|
|
401
490
|
|
|
402
491
|
```bash
|
package/bin/linux-arm64/run
CHANGED
|
Binary file
|
package/bin/linux-x64/README.md
CHANGED
|
@@ -111,16 +111,16 @@ Sorted from where Runfile is most differentiated to where it lags behind.
|
|
|
111
111
|
| Argument substitution with chained fallbacks | ✅ | ❌ | ✅ | ❌ |
|
|
112
112
|
| JSON Schema autocomplete in editors | ✅ | ❌ | ❌ | ✅ |
|
|
113
113
|
| Watch mode, built-in | ✅ | ❌ | ❌ | ✅ |
|
|
114
|
+
| Output grouping / prefixing in parallel mode | ✅ | ❌ | ❌ | ✅ |
|
|
114
115
|
| Native Windows (no WSL / msys2 needed) | ✅ | ❌ | ✅ | ✅ |
|
|
115
|
-
| Parallel execution | ✅ | ✅ | ❌ | ✅ |
|
|
116
116
|
| Shell completions | ✅ | ❌ | ✅ | ✅ |
|
|
117
117
|
| Private / internal targets (`_name`) | ✅ | ❌ | ✅ | ✅ |
|
|
118
|
+
| Built-in string functions (upper, replace, trim, regex, base64, …) | ✅ | ❌ | ✅ | ✅ |
|
|
119
|
+
| Parallel execution | ✅ | ✅ | ❌ | ✅ |
|
|
118
120
|
| Single static binary | ✅ | ✅ | ✅ | ✅ |
|
|
119
121
|
| Pattern rules (`%.o: %.c`) | ❌ | ✅ | ❌ | ❌ |
|
|
120
|
-
| Output grouping / prefixing in parallel mode | ❌ | ❌ | ❌ | ✅ |
|
|
121
122
|
| Preconditions / status checks | ❌ | ❌ | ❌ | ✅ |
|
|
122
123
|
| Incremental builds (sources / timestamps / checksums) | ❌ | ✅ | ❌ | ✅ |
|
|
123
|
-
| Built-in string functions (upper, replace, trim, …) | ❌ | ❌ | ✅ | ✅ |
|
|
124
124
|
|
|
125
125
|
---
|
|
126
126
|
|
|
@@ -167,9 +167,9 @@ $ run :env inject -f .env.production -- ./deploy.sh # encrypted values aut
|
|
|
167
167
|
|
|
168
168
|
#### Powerful argument substitution
|
|
169
169
|
|
|
170
|
-
Positional, named, flags, env vars, and runtime context (`{{ RUN.os }}` / `{{ RUN.
|
|
171
|
-
`{{ RUN.file }}` / `{{ RUN.parent }}`) — with chained fallbacks and required values. The
|
|
172
|
-
avoids collisions with shell `$(...)` command substitution.
|
|
170
|
+
Positional, named, flags, env vars, and runtime context (`{{ RUN.os }}` / `{{ RUN.arch }}` / `{{ RUN.shell }}` /
|
|
171
|
+
`{{ RUN.cwd }}` / `{{ RUN.file }}` / `{{ RUN.parent }}`) — with chained fallbacks and required values. The
|
|
172
|
+
`{{ ... }}` syntax avoids collisions with shell `$(...)` command substitution.
|
|
173
173
|
|
|
174
174
|
```jsonc
|
|
175
175
|
"PORT": "{{ ARGS.port ? ENV.PORT ? '3000' }}",
|
|
@@ -242,9 +242,15 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
242
242
|
// transform : escape, repeat, replace_all, remove_all
|
|
243
243
|
// regex : regex_replace, regex_remove, regex_matches
|
|
244
244
|
// build : concat, join
|
|
245
|
+
// split : nth, first, last, count_parts
|
|
245
246
|
// encoding : base64_encode, base64_decode
|
|
247
|
+
// hashing : sha256, md5
|
|
248
|
+
// files : read_file, file_exists
|
|
249
|
+
// json : json_get, json_set
|
|
250
|
+
// error : try
|
|
246
251
|
// shell : shell_quote
|
|
247
252
|
// variables : define
|
|
253
|
+
// cwd : set_cwd
|
|
248
254
|
"echo deploying-{{ to_upper(ARGS.env) }}",
|
|
249
255
|
"curl -H \"X-Auth: {{ base64_encode(ENV.TOKEN) }}\" ...",
|
|
250
256
|
"echo {{ concat('hello-', ARGS.name, '-2026') }}",
|
|
@@ -253,6 +259,13 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
253
259
|
"go test {{ replace_all(ARGS.flags, ' ', ' -tag=') }}",
|
|
254
260
|
// Strip every match of a regex (here: collapse whitespace runs to a single space):
|
|
255
261
|
"echo {{ regex_replace(ARGS.text, '\\s+', ' ') }}",
|
|
262
|
+
// Split-by-separator scalar accessors — string in, string out, no list type:
|
|
263
|
+
// basename idiom (last segment after `/`); empty string when input ends in `/`.
|
|
264
|
+
"echo basename={{ last(ARGS.path, '/') }}",
|
|
265
|
+
// Pull out the N-th comma-separated field; out-of-bounds returns "".
|
|
266
|
+
"echo target={{ nth(ARGS.csv, ',', '1') }}",
|
|
267
|
+
// Pair `count_parts` with `nth` to bound-check before indexing:
|
|
268
|
+
// "if": "{{ count_parts(ARGS.csv, ',') == '3' }}"
|
|
256
269
|
// Boolean-returning helpers are valid DSL `Truthy` values — use them in `if`:
|
|
257
270
|
// "if": "{{ starts_with(ARGS.path, '/usr') }}"
|
|
258
271
|
// "if": "{{ regex_matches(ARGS.tag, '^v[0-9]+\\.[0-9]+$') }}"
|
|
@@ -261,6 +274,28 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
261
274
|
// `shell_quote` picks the right quoting for the active shell:
|
|
262
275
|
"some-tool --json {{ shell_quote(base64_decode(ENV.SECRET_BASE64)) }}",
|
|
263
276
|
|
|
277
|
+
// Cache keys / content fingerprints. `sha256` for security-sensitive use,
|
|
278
|
+
// `md5` for cheap fingerprinting (NOT cryptographically secure).
|
|
279
|
+
"echo cache-key={{ sha256(read_file('package-lock.json')) }}",
|
|
280
|
+
|
|
281
|
+
// Branch on file presence without forking a shell. `file_exists` returns
|
|
282
|
+
// the literal "true" / "false", so it doubles as a DSL truthy value.
|
|
283
|
+
// "if": "{{ file_exists('.env.local') }}"
|
|
284
|
+
|
|
285
|
+
// Read a file inline — relative paths anchor to the Runfile directory.
|
|
286
|
+
// Use `try(...)` to recover from missing files.
|
|
287
|
+
"echo version={{ try(read_file('VERSION')) ? '0.0.0-dev' }}",
|
|
288
|
+
|
|
289
|
+
// Pull values out of arbitrary JSON without `jq` (works on every shell):
|
|
290
|
+
"echo db_host={{ json_get(read_file('config.json'), 'database.host') }}",
|
|
291
|
+
|
|
292
|
+
// Modify a JSON document in place (returns the new compact JSON):
|
|
293
|
+
"echo {{ json_set('{\"port\":3000}', 'env', 'production') }}",
|
|
294
|
+
|
|
295
|
+
// `try(expr)` swallows inner errors. Standalone returns "" on failure;
|
|
296
|
+
// chained, the next segment runs as a fallback.
|
|
297
|
+
"echo {{ try(base64_decode(ARGS.maybe_b64)) ? ARGS.maybe_b64 }}",
|
|
298
|
+
|
|
264
299
|
// Nested:
|
|
265
300
|
"echo {{ to_upper(to_lower(ARGS.x)) }}",
|
|
266
301
|
|
|
@@ -272,6 +307,13 @@ and work as full substitution bodies *or* as chain segments:
|
|
|
272
307
|
"{{ VARS.sdk }}/bin/build",
|
|
273
308
|
"echo using sdk at {{ VARS.sdk }}",
|
|
274
309
|
|
|
310
|
+
// `set_cwd(path)` switches the cwd subsequent commands spawn in — like
|
|
311
|
+
// shell `cd`, but works on every shell / OS without forking. Relative
|
|
312
|
+
// paths chain (matching `cd a; cd b` → `a/b`); absolute paths replace.
|
|
313
|
+
"{{ set_cwd(ARGS.subproject ? 'packages/api') }}",
|
|
314
|
+
"npm install",
|
|
315
|
+
"npm run build",
|
|
316
|
+
|
|
275
317
|
// Single-quoted strings interpolate nested {{ }} substitutions:
|
|
276
318
|
"{{ define(cmd, 'docker compose -f {{ VARS.compose }} pull') }}",
|
|
277
319
|
"{{ VARS.cmd }}"
|
|
@@ -283,6 +325,22 @@ to only whitespace (i.e. one consisting solely of a `{{ define(...) }}` call) is
|
|
|
283
325
|
being dispatched to the shell. `define`s in a parent target are visible to `@target` children. The `name` MUST
|
|
284
326
|
be a bareword identifier matching `[A-Za-z_][A-Za-z0-9_-]*` — quotes are NOT allowed on the name.
|
|
285
327
|
|
|
328
|
+
`set_cwd(path)` is the cwd analog of `define`: returns an empty string and changes the cwd that subsequent shell
|
|
329
|
+
commands in the *current target* spawn in. Behaves like shell `cd`, but works uniformly across every shell / OS
|
|
330
|
+
(no forked process, no `cd` binary needed). Resolution rules:
|
|
331
|
+
|
|
332
|
+
- **Absolute path** → fully replaces the current override (matches `cd /abs`).
|
|
333
|
+
- **Relative path** → joins onto the existing override, or onto the target's `workingDirectory` if no override
|
|
334
|
+
has been set yet. So `set_cwd('a'); set_cwd('b')` lands subsequent commands in `<workingDirectory>/a/b`,
|
|
335
|
+
matching how `cd a; cd b` chains in a shell.
|
|
336
|
+
|
|
337
|
+
`set_cwd` is **per-target**: each `@target` invocation starts with a clean override (the dispatched target sees
|
|
338
|
+
its own `workingDirectory`, not the caller's `set_cwd` state). Inside `parallel: true` targets each leaf
|
|
339
|
+
captures the override at the moment of its substitution, so siblings don't race on the spawn cwd. Like
|
|
340
|
+
`define`, the side effect is skipped on the redacted-logging pass so log lines don't double-apply it. With
|
|
341
|
+
`sameShell: true`, only the *final* `set_cwd` value applies (everything joins into one shell invocation) — use
|
|
342
|
+
shell `cd` directly between leaves there if you need intermediate cwd changes.
|
|
343
|
+
|
|
286
344
|
Function args are separated by `, ` (comma + exactly one space).
|
|
287
345
|
|
|
288
346
|
**Quote semantics inside `{{ ... }}`:**
|
|
@@ -311,13 +369,18 @@ evaluated by Runfile itself, so the logic works the same on every shell and plat
|
|
|
311
369
|
"cases": {
|
|
312
370
|
"1": "flutter emulators --launch Tier_1_Android_9",
|
|
313
371
|
"2": "flutter emulators --launch Tier_2_Android_11",
|
|
314
|
-
"3": "flutter emulators --launch Tier_3_Android_14"
|
|
372
|
+
"3": "flutter emulators --launch Tier_3_Android_14",
|
|
373
|
+
// Case keys wrapped in `/.../` are treated as regex patterns.
|
|
374
|
+
// Literal cases always win over a regex that would also match.
|
|
375
|
+
"/^v\\d+$/": "echo version-tag",
|
|
376
|
+
"/^pr-\\d+$/": "echo preview-build"
|
|
315
377
|
} }, // unknown values error out, listing the valid cases
|
|
316
378
|
|
|
317
379
|
{ "for": "service",
|
|
318
380
|
"in": ["api", "web", "worker"],
|
|
319
381
|
"parallel": true,
|
|
320
|
-
|
|
382
|
+
// VARS.<name>_index exposes the 0-based iteration counter.
|
|
383
|
+
"do": ["echo [{{ VARS.service_index }}] docker build -t {{ VARS.service }} services/{{ VARS.service }}"] },
|
|
321
384
|
|
|
322
385
|
{ "for": "file",
|
|
323
386
|
"shell": "git diff --name-only HEAD~1",
|
|
@@ -379,6 +442,27 @@ Wrap any commands in a `when:` block to run them only after a failure, or always
|
|
|
379
442
|
]
|
|
380
443
|
```
|
|
381
444
|
|
|
445
|
+
#### Single-shell mode with `sameShell`
|
|
446
|
+
|
|
447
|
+
By default each step runs in its own shell process, so `cd`, exported variables, and other shell state don't carry
|
|
448
|
+
over between steps. Set `"sameShell": true` to join every step into a single shell invocation — state changes
|
|
449
|
+
persist for free.
|
|
450
|
+
|
|
451
|
+
```jsonc
|
|
452
|
+
{
|
|
453
|
+
"deploy": {
|
|
454
|
+
"sameShell": true,
|
|
455
|
+
"commands": ["cd ci-scripts/", "./ci-deploy.sh"]
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Steps are joined with `&&` so the run stops at the first failure (or `;` / `&` when `ignoreErrors: true`). `if`,
|
|
461
|
+
`for`, and `match` blocks are evaluated by Runfile and their chosen branches flow into the same joined invocation.
|
|
462
|
+
`@target` calls inside the body are rejected — they have their own shell context and can't share state with the
|
|
463
|
+
parent. `sameShell` also composes with `detach: true` (joined command spawns as one detached process) and is
|
|
464
|
+
available on `globals` for project-wide defaults.
|
|
465
|
+
|
|
382
466
|
#### Watch mode, built in
|
|
383
467
|
|
|
384
468
|
Add a `watch` array to any target. No flags, no extra tooling.
|
|
@@ -397,6 +481,11 @@ $ run :generate zed-tasks
|
|
|
397
481
|
$ run :generate jetbrains-run-configurations
|
|
398
482
|
```
|
|
399
483
|
|
|
484
|
+
Skip a target from the generated configs by setting `metadata.excludeFromGenerateCommand: true`. The
|
|
485
|
+
`metadata` block on `globals` and on each target is **fully open** — any property of any JSON type
|
|
486
|
+
(strings, numbers, booleans, arrays, deeply-nested objects) is accepted and round-trips untouched, so
|
|
487
|
+
editor extensions and CI scripts can stash arbitrary tooling-specific fields here.
|
|
488
|
+
|
|
400
489
|
#### Migrate in seconds
|
|
401
490
|
|
|
402
491
|
```bash
|
package/bin/linux-x64/run
CHANGED
|
Binary file
|
package/bin/win32-arm64/run.exe
CHANGED
|
Binary file
|
package/bin/win32-x64/run.exe
CHANGED
|
Binary file
|
package/package.json
CHANGED