@runfile/cli 0.25.0 → 0.26.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.
@@ -17,7 +17,7 @@ $ run dev --port=4000
17
17
  "dev": {
18
18
  "description": "Starts the dev server",
19
19
  "commands": "vite",
20
- "env": { "PORT": "{{ ARGS.port ? '3000' }}" }
20
+ "env": { "PORT": "{{ ARG.port ? '3000' }}" }
21
21
  },
22
22
  "build": {
23
23
  "description": "Type-checks and builds in parallel",
@@ -25,11 +25,11 @@ $ run dev --port=4000
25
25
  "@type-check",
26
26
  "vite build",
27
27
  {
28
- "if": "{{ RUN.os == 'windows' && FLAGS.wsl }}",
28
+ "if": "{{ RUN.os == 'windows' && FLAG.wsl }}",
29
29
  "then": "wsl --shell-type login -- vite build"
30
30
  }
31
31
  ],
32
- "envFiles": [".env", ".env.{{ ARGS.env ? 'development' }}"],
32
+ "envFiles": [".env", ".env.{{ ARG.env ? 'development' }}"],
33
33
  "parallel": true
34
34
  },
35
35
  "type-check": {
@@ -190,21 +190,24 @@ Positional, named, flags, env vars, and runtime context (`{{ RUN.os }}` / `{{ RU
190
190
  `{{ RUN.cwd }}` / `{{ RUN.file }}` / `{{ RUN.parent }}`) — with chained fallbacks and required values. The
191
191
  `{{ ... }}` syntax avoids collisions with shell `$(...)` command substitution.
192
192
 
193
+ The source prefixes are `{{ ARG.<name> }}` (named arg), `{{ ENV.<name> }}`, `{{ FLAG.<name> }}`,
194
+ `{{ VAR.<name> }}`, and `{{ RUN.<key> }}`. Bare `{{ ARGS }}` expands to **all** positional arguments.
195
+
193
196
  ```jsonc
194
- "PORT": "{{ ARGS.port ? ENV.PORT ? '3000' }}",
195
- "OPTS": "{{ FLAGS.release ? '--release' : }} {{ FLAGS.verbose ? '-v' : }}",
197
+ "PORT": "{{ ARG.port ? ENV.PORT ? '3000' }}",
198
+ "OPTS": "{{ FLAG.release ? '--release' : }} {{ FLAG.verbose ? '-v' : }}",
196
199
  "CARGO_TARGET_DIR": "target-{{ RUN.os }}",
197
200
  // Inline OS/shell branches — the boolean DSL goes inside a single `{{ ... }}` block:
198
201
  { "if": "{{ RUN.os == 'windows' }}", "then": ["del /S /Q build"], "else": ["rm -rf build"] },
199
- // User-supplied flags branch through `{{ FLAGS.x }}` (resolves to "true" / "false"):
200
- { "if": "{{ FLAGS.debug }}", "then": ["./tool --verbose"], "else": ["./tool"] },
202
+ // User-supplied flags branch through `{{ FLAG.x }}` (resolves to "true" / "false"):
203
+ { "if": "{{ FLAG.debug }}", "then": ["./tool --verbose"], "else": ["./tool"] },
201
204
  // workingDirectory is a free-form path (defaults to {{ RUN.parent }}):
202
- "workingDirectory": "{{ ARGS.workdir ? RUN.cwd }}"
205
+ "workingDirectory": "{{ ARG.workdir ? RUN.cwd }}"
203
206
  ```
204
207
 
205
208
  Strict format: exactly one space after `{{` and before `}}`, exactly one space around `?` and `:` operators.
206
209
  **String literals must be wrapped in single quotes** — `'production'`, not `production`. Source references
207
- (`ARGS.x`, `VARS.x`, etc.) and function calls remain bare. Use `\{{` / `\}}` to emit a literal `{{` / `}}` in
210
+ (`ARG.x`, `VAR.x`, etc.) and function calls remain bare. Use `\{{` / `\}}` to emit a literal `{{` / `}}` in
208
211
  your output.
209
212
 
210
213
  #### Boolean conditions inside substitutions
@@ -216,41 +219,41 @@ but you can use it anywhere:
216
219
  ```jsonc
217
220
  "commands": [
218
221
  // Branch on a CLI flag — `if` checks if the substitution resolves to the literal "true":
219
- { "if": "{{ ARGS.env == 'production' }}",
222
+ { "if": "{{ ARG.env == 'production' }}",
220
223
  "then": ["./deploy-prod.sh"],
221
224
  "else": ["./deploy-staging.sh"] },
222
225
 
223
226
  // Compose AND/OR/NOT freely:
224
- { "if": "{{ ARGS.env != 'development' && ARGS.env != 'production' }}",
227
+ { "if": "{{ ARG.env != 'development' && ARG.env != 'production' }}",
225
228
  "then": ["./deploy-staging.sh"] },
226
229
 
227
230
  // Invert any boolean-returning value with unary `!` — including the
228
231
  // comparison helpers (`less_than`, `is_number`, …) and `contains` etc.:
229
- { "if": "{{ !is_number(ARGS.port) }}",
232
+ { "if": "{{ !is_number(ARG.port) }}",
230
233
  "then": ["echo 'port must be a number' && exit 1"] },
231
234
 
232
- // FLAGS.x works as a bare boolean inside DSL — no `== 'true'` needed:
233
- { "if": "{{ RUN.os == 'windows' && FLAGS.wsl }}",
235
+ // FLAG.x works as a bare boolean inside DSL — no `== 'true'` needed:
236
+ { "if": "{{ RUN.os == 'windows' && FLAG.wsl }}",
234
237
  "then": "wsl --shell-type login -- vite build" },
235
238
 
236
239
  // Inline in a command — useful for passing booleans to other tools:
237
- "my-command --resolve {{ ARGS.env == 'production' }}"
240
+ "my-command --resolve {{ ARG.env == 'production' }}"
238
241
  ]
239
242
  ```
240
243
 
241
- **Strict boolean rule.** Any value used as a bare boolean — both inside the DSL Truthy check (e.g. `&& FLAGS.x`,
242
- `!ARGS.y`) and as the entire `if` condition — must resolve to exactly one of:
244
+ **Strict boolean rule.** Any value used as a bare boolean — both inside the DSL Truthy check (e.g. `&& FLAG.x`,
245
+ `!ARG.y`) and as the entire `if` condition — must resolve to exactly one of:
243
246
 
244
247
  - `"true"` → truthy (the `then` branch runs / left arm of `&&` continues)
245
248
  - `"false"` → falsy
246
249
  - `""` (empty) → falsy
247
250
 
248
251
  Anything else (`"True"`, `"1"`, `"yes"`, `"hello"`, etc.) errors out with a clear message pointing you toward
249
- the explicit comparison form. So `if: "{{ ARGS.x }}"` and `{{ ARGS.x && ... }}` only work when `ARGS.x` is
250
- exactly `"true"` / `"false"` / empty — for any other check, use a comparison: `{{ ARGS.x == 'yes' }}`.
252
+ the explicit comparison form. So `if: "{{ ARG.x }}"` and `{{ ARG.x && ... }}` only work when `ARG.x` is
253
+ exactly `"true"` / `"false"` / empty — for any other check, use a comparison: `{{ ARG.x == 'yes' }}`.
251
254
 
252
255
  Comparisons (`==` / `!=`) operate on raw strings — *those* don't have the boolean restriction, so
253
- `{{ ARGS.env == 'staging' }}` works for any value of `ARGS.env`.
256
+ `{{ ARG.env == 'staging' }}` works for any value of `ARG.env`.
254
257
 
255
258
  #### Functions: transform values inline
256
259
 
@@ -262,30 +265,32 @@ and work as full substitution bodies *or* as chain segments:
262
265
  // Built-ins:
263
266
  // case : to_upper, to_lower, capitalize
264
267
  // trim : trim, trim_start, trim_end
265
- // inspect : length, starts_with, ends_with, contains
268
+ // inspect : length, starts_with, ends_with, contains, substring
266
269
  // transform : escape, repeat, replace_all, remove_all
267
270
  // regex : regex_replace, regex_remove, regex_matches, regex_capture, regex_capture_all
268
271
  // build : concat, join
269
272
  // split : nth, first, last, count_parts
270
- // math : add, subtract, multiply, divide
273
+ // path : basename, dirname, extname, stem, join_path
274
+ // math : add, subtract, multiply, divide, modulo, power, min, max, abs, round, floor, ceil
271
275
  // compare : less_than, less_than_or_equal, greater_than, greater_than_or_equal, is_number
272
276
  // validate : one_of
273
- // encoding : base64_encode, base64_decode
277
+ // encoding : base64_encode, base64_decode, url_encode, url_decode
274
278
  // hashing : sha256, md5
279
+ // ids/time : uuid, now
275
280
  // files : read_file, write_file, file_exists
276
281
  // json : json_get, json_set
277
- // error : try
282
+ // control : try, error
278
283
  // shell : shell_quote, capture
279
284
  // variables : define
280
285
  // cwd : set_cwd
281
- "echo deploying-{{ to_upper(ARGS.env) }}",
286
+ "echo deploying-{{ to_upper(ARG.env) }}",
282
287
  "curl -H \"X-Auth: {{ base64_encode(ENV.TOKEN) }}\" ...",
283
- "echo {{ concat('hello-', ARGS.name, '-2026') }}",
284
- "echo {{ join(' AND ', 'flag-1', 'flag-2', ARGS.extra) }}",
288
+ "echo {{ concat('hello-', ARG.name, '-2026') }}",
289
+ "echo {{ join(' AND ', 'flag-1', 'flag-2', ARG.extra) }}",
285
290
  // Tokenise-and-rejoin-style transforms via replace_all:
286
- "go test {{ replace_all(ARGS.flags, ' ', ' -tag=') }}",
291
+ "go test {{ replace_all(ARG.flags, ' ', ' -tag=') }}",
287
292
  // Strip every match of a regex (here: collapse whitespace runs to a single space):
288
- "echo {{ regex_replace(ARGS.text, '\\s+', ' ') }}",
293
+ "echo {{ regex_replace(ARG.text, '\\s+', ' ') }}",
289
294
  // Pull a capture group out of the first regex match — no `(?s)^.*X(...)X.*$`
290
295
  // greedy-replace trick needed. Group 0 is the whole match; group N is the
291
296
  // N-th `(...)`. Out-of-bounds returns "" (same convention as `nth`).
@@ -295,14 +300,14 @@ and work as full substitution bodies *or* as chain segments:
295
300
  "echo deps={{ regex_capture_all(read_file('deps.txt'), 'name = \"([^\"]+)\"', '1', ',') }}",
296
301
  // Split-by-separator scalar accessors — string in, string out, no list type:
297
302
  // basename idiom (last segment after `/`); empty string when input ends in `/`.
298
- "echo basename={{ last(ARGS.path, '/') }}",
303
+ "echo basename={{ last(ARG.path, '/') }}",
299
304
  // Pull out the N-th comma-separated field; out-of-bounds returns "".
300
- "echo target={{ nth(ARGS.csv, ',', '1') }}",
305
+ "echo target={{ nth(ARG.csv, ',', '1') }}",
301
306
  // Pair `count_parts` with `nth` to bound-check before indexing:
302
- // "if": "{{ count_parts(ARGS.csv, ',') == '3' }}"
307
+ // "if": "{{ count_parts(ARG.csv, ',') == '3' }}"
303
308
  // Boolean-returning helpers are valid DSL `Truthy` values — use them in `if`:
304
- // "if": "{{ starts_with(ARGS.path, '/usr') }}"
305
- // "if": "{{ regex_matches(ARGS.tag, '^v[0-9]+\\.[0-9]+$') }}"
309
+ // "if": "{{ starts_with(ARG.path, '/usr') }}"
310
+ // "if": "{{ regex_matches(ARG.tag, '^v[0-9]+\\.[0-9]+$') }}"
306
311
 
307
312
  // Safely inline arbitrary content (newlines, quotes, JSON) as a CLI arg —
308
313
  // `shell_quote` picks the right quoting for the active shell:
@@ -320,17 +325,36 @@ and work as full substitution bodies *or* as chain segments:
320
325
  // non-numeric input. The result is formatted as an integer when whole and
321
326
  // as a decimal otherwise (so `add('5', '3')` → "8", `add('5', '1.1')` →
322
327
  // "6.1"). Divide-by-zero errors out.
323
- "echo next-build={{ add(VARS.versionCode, '1') }}",
324
- "echo half={{ divide(VARS.total, '2') }}",
328
+ "echo next-build={{ add(VAR.versionCode, '1') }}",
329
+ "echo half={{ divide(VAR.total, '2') }}",
330
+ // …plus modulo, power, min, max, abs, round, floor, ceil:
331
+ "echo shard={{ modulo(ARG.id, '8') }} clamped={{ max('0', min('100', ARG.pct)) }}",
332
+
333
+ // Path helpers (std::path semantics). basename/dirname/extname/stem/join_path:
334
+ "echo name={{ basename(ARG.file) }} ext={{ extname(ARG.file) }}",
335
+ "cp {{ ARG.file }} {{ join_path(ARG.outdir, basename(ARG.file)) }}",
336
+
337
+ // Substring (char-indexed; 3rd arg = length, optional → to end). Short SHA:
338
+ "echo short={{ substring(capture('git rev-parse HEAD'), '0', '7') }}",
339
+
340
+ // Current UTC time. Formats: unix-timestamp, unix-millis, iso, iso-date,
341
+ // iso-time, rfc3339, year/month/day/hour/minute/second.
342
+ "echo built-at={{ now('iso') }} tag=build-{{ now('unix-timestamp') }}",
343
+
344
+ // UUID (v4-shaped) for unique temp / cache names. `--dry-run` → `<uuid>`.
345
+ "echo tmp=/tmp/job-{{ uuid() }}",
346
+
347
+ // URL percent-encode / decode (space → %20; symmetric round-trip):
348
+ "curl 'https://api/search?q={{ url_encode(ARG.query) }}'",
325
349
 
326
350
  // Numeric comparisons — coerce both args to numbers (same rules as the
327
351
  // arithmetic family, so non-numeric input errors) and return "true"/"false",
328
352
  // so they work as DSL `Truthy` values in `if` conditions:
329
- // "if": "{{ greater_than(VARS.count, '50') }}"
330
- // "if": "{{ less_than_or_equal(ARGS.retries, '3') }}"
353
+ // "if": "{{ greater_than(VAR.count, '50') }}"
354
+ // "if": "{{ less_than_or_equal(ARG.retries, '3') }}"
331
355
  // `is_number` tests whether a value parses as a (finite) number — it returns
332
356
  // "false" instead of erroring on non-numeric input:
333
- // "if": "{{ is_number(ARGS.port) }}"
357
+ // "if": "{{ is_number(ARG.port) }}"
334
358
 
335
359
  // Validate a value against a fixed allow-list. Returns the value on
336
360
  // match; lists every valid option in the error on mismatch. Collapses
@@ -366,29 +390,35 @@ and work as full substitution bodies *or* as chain segments:
366
390
 
367
391
  // `try(expr)` swallows inner errors. Standalone returns "" on failure;
368
392
  // chained, the next segment runs as a fallback.
369
- "echo {{ try(base64_decode(ARGS.maybe_b64)) ? ARGS.maybe_b64 }}",
393
+ "echo {{ try(base64_decode(ARG.maybe_b64)) ? ARG.maybe_b64 }}",
394
+
395
+ // `error('message')` fails the current command on purpose: prints the
396
+ // message to stderr and marks the step failed — but the failure flows
397
+ // through the normal walker, so `when: failure` / `when: always` steps
398
+ // still run and `ignoreErrors` still suppresses it. Great for guard clauses:
399
+ // { "if": "{{ ARG.env == 'prod' }}", "then": ["{{ error('refusing to deploy to prod from a laptop') }}"] }
370
400
 
371
401
  // Nested:
372
- "echo {{ to_upper(to_lower(ARGS.x)) }}",
402
+ "echo {{ to_upper(to_lower(ARG.x)) }}",
373
403
 
374
404
  // As a chain fallback:
375
- "echo host={{ ARGS.host ? to_lower(ENV.HOST) }}",
405
+ "echo host={{ ARG.host ? to_lower(ENV.HOST) }}",
376
406
 
377
- // Capture a value with `define(...)` and read it later via `VARS.<name>`:
407
+ // Capture a value with `define(...)` and read it later via `VAR.<name>`:
378
408
  "{{ define(sdk, ENV.SDK ? '/opt/sdk') }}",
379
- "{{ VARS.sdk }}/bin/build",
380
- "echo using sdk at {{ VARS.sdk }}",
409
+ "{{ VAR.sdk }}/bin/build",
410
+ "echo using sdk at {{ VAR.sdk }}",
381
411
 
382
412
  // `set_cwd(path)` switches the cwd subsequent commands spawn in — like
383
413
  // shell `cd`, but works on every shell / OS without forking. Relative
384
414
  // paths chain (matching `cd a; cd b` → `a/b`); absolute paths replace.
385
- "{{ set_cwd(ARGS.subproject ? 'packages/api') }}",
415
+ "{{ set_cwd(ARG.subproject ? 'packages/api') }}",
386
416
  "npm install",
387
417
  "npm run build",
388
418
 
389
419
  // Single-quoted strings interpolate nested {{ }} substitutions:
390
- "{{ define(cmd, 'docker compose -f {{ VARS.compose }} pull') }}",
391
- "{{ VARS.cmd }}"
420
+ "{{ define(cmd, 'docker compose -f {{ VAR.compose }} pull') }}",
421
+ "{{ VAR.cmd }}"
392
422
  ]
393
423
  ```
394
424
 
@@ -397,6 +427,38 @@ to only whitespace (i.e. one consisting solely of a `{{ define(...) }}` call) is
397
427
  being dispatched to the shell. `define`s in a parent target are visible to `@target` children. The `name` MUST
398
428
  be a bareword identifier matching `[A-Za-z_][A-Za-z0-9_-]*` — quotes are NOT allowed on the name.
399
429
 
430
+ #### Declaring variables up front (`vars`)
431
+
432
+ Instead of (or alongside) `define(...)`, you can declare variables directly on a target — or on `globals` to
433
+ apply them to every target — and read them as `{{ VAR.<key> }}`. This mirrors `env`: each value is a `{{ ... }}`
434
+ template resolved **after** the env is built (so it can reference `{{ ENV.* }}`), plus `{{ ARG.* }}`,
435
+ `{{ FLAG.* }}`, `{{ RUN.* }}`, and earlier vars. If a reference has no default and isn't supplied, it errors —
436
+ exactly like everywhere else.
437
+
438
+ ```jsonc
439
+ {
440
+ "globals": {
441
+ "vars": { "appName": "skiley" } // available to every target
442
+ },
443
+ "targets": {
444
+ "deploy": {
445
+ "env": { "REGION": "us-east-1" },
446
+ "vars": {
447
+ "region": "{{ ENV.REGION }}", // vars resolve after env
448
+ "retries": "3", // numbers/bools are stringified
449
+ "tag": "{{ ARG.tag ? 'latest' }}" // chain fallbacks work
450
+ },
451
+ "commands": ["echo deploying {{ VAR.appName }} to {{ VAR.region }} as {{ VAR.tag }}"]
452
+ }
453
+ }
454
+ }
455
+ ```
456
+
457
+ Global `vars` are merged into each target's `vars` at parse time (target keys win on conflict). Declared vars are
458
+ **scoped per-target like `env`**: a parent's vars are visible inside an `@target` dependency, but a dependency's
459
+ own declared vars don't leak back to the parent. A runtime `define(...)` of the same name overrides the declared
460
+ value for the rest of the target. Var keys must match `[A-Za-z_][A-Za-z0-9_-]*`.
461
+
400
462
  `set_cwd(path)` is the cwd analog of `define`: returns an empty string and changes the cwd that subsequent shell
401
463
  commands in the *current target* spawn in. Behaves like shell `cd`, but works uniformly across every shell / OS
402
464
  (no forked process, no `cd` binary needed). Resolution rules:
@@ -419,12 +481,12 @@ Function args are separated by `, ` (comma + exactly one space).
419
481
 
420
482
  - **Single quotes (`'...'`) — interpolated string**: surrounding quotes stripped, with any nested `{{ ... }}`
421
483
  resolved through the regular substitution machinery. Use these for almost every literal — they handle the
422
- full range of values including embedded substitutions: `concat('a, b', ARGS.x)`,
423
- `'/var/log/{{ RUN.os }}.log'`, `define(cmd, 'docker -f {{ VARS.compose }} pull')`.
484
+ full range of values including embedded substitutions: `concat('a, b', ARG.x)`,
485
+ `'/var/log/{{ RUN.os }}.log'`, `define(cmd, 'docker -f {{ VAR.compose }} pull')`.
424
486
  - **Double quotes (`"..."`) — fully literal value**: the quote characters are part of the value (so `"test"` is
425
487
  the 6-character string `"test"`). No interpolation. Useful when the literal you want to emit really should
426
488
  contain `"` chars.
427
- - **Plain barewords are rejected** — `{{ ARGS.env ? development }}` is a parse-time error. Wrap in quotes.
489
+ - **Plain barewords are rejected** — `{{ ARG.env ? development }}` is a parse-time error. Wrap in quotes.
428
490
 
429
491
  #### Conditionals and loops, no shell required
430
492
 
@@ -433,11 +495,11 @@ evaluated by Runfile itself, so the logic works the same on every shell and plat
433
495
 
434
496
  ```jsonc
435
497
  "commands": [
436
- { "if": "{{ ARGS.env == 'production' }}",
498
+ { "if": "{{ ARG.env == 'production' }}",
437
499
  "then": ["./deploy-prod.sh"],
438
500
  "else": ["./deploy-staging.sh"] },
439
501
 
440
- { "match": "{{ ARGS.tier ? '1' }}", // chain default; case "1" runs when --tier missing
502
+ { "match": "{{ ARG.tier ? '1' }}", // chain default; case "1" runs when --tier missing
441
503
  "cases": {
442
504
  "1": "flutter emulators --launch Tier_1_Android_9",
443
505
  "2": "flutter emulators --launch Tier_2_Android_11",
@@ -451,12 +513,12 @@ evaluated by Runfile itself, so the logic works the same on every shell and plat
451
513
  { "for": "service",
452
514
  "in": ["api", "web", "worker"],
453
515
  "parallel": true,
454
- // VARS.<name>_index exposes the 0-based iteration counter.
455
- "do": ["echo [{{ VARS.service_index }}] docker build -t {{ VARS.service }} services/{{ VARS.service }}"] },
516
+ // VAR.<name>_index exposes the 0-based iteration counter.
517
+ "do": ["echo [{{ VAR.service_index }}] docker build -t {{ VAR.service }} services/{{ VAR.service }}"] },
456
518
 
457
519
  { "for": "file",
458
520
  "shell": "git diff --name-only HEAD~1",
459
- "do": ["clang-format -i {{ VARS.file }}"] }
521
+ "do": ["clang-format -i {{ VAR.file }}"] }
460
522
  ]
461
523
  ```
462
524
 
@@ -476,7 +538,7 @@ namespace.
476
538
  "@test --coverage",
477
539
  "@build {{ ARGS }}",
478
540
  { "if": "{{ RUN.os == 'windows' }}", "then": "@deploy-win", "else": "@deploy-unix" },
479
- { "for": "ns", "in": "namespaces", "do": "@?{{ VARS.ns }}:adb-forward" } // skip namespaces without the target
541
+ { "for": "ns", "in": "namespaces", "do": "@?{{ VAR.ns }}:adb-forward" } // skip namespaces without the target
480
542
  ]
481
543
  ```
482
544
 
@@ -489,7 +551,7 @@ lines instead of corrupting each other's output. The label shows the full resolv
489
551
  (`[@dev --port 5000]`) for target-call branches, or the raw command truncated to 12 characters (`[docker compo]`)
490
552
  for shell branches; each label gets one of six cycling colors so adjacent branches stay distinct.
491
553
  SGR colors flow through unchanged. The prefix propagates through `@target` invocations too,
492
- so monorepo fan-outs like `{ "for": "ns", "in": "namespaces", "do": "@{{ VARS.ns }}:dev" }` tag every nested shell
554
+ so monorepo fan-outs like `{ "for": "ns", "in": "namespaces", "do": "@{{ VAR.ns }}:dev" }` tag every nested shell
493
555
  with its branch identity. Set `RUNFILE_NO_LINE_PREFIX=1` to opt out and inherit raw stdio.
494
556
 
495
557
  ```text
package/bin/linux-x64/run CHANGED
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runfile/cli",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "description": "Runfile CLI — an easy-to-use, batteries-included task runner",
5
5
  "repository": "https://github.com/Skiley/runfile",
6
6
  "homepage": "https://github.com/Skiley/runfile",