@runfile/cli 1.1.6 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,8 @@ web/runfiles/build.run → run web:build
23
23
  ```
24
24
 
25
25
  A target is a file, its name is its path, and its first comment block is its description. There is no central
26
- file to merge, so two people adding a task never conflict.
26
+ file to merge, so two people adding a task never conflict. `run :list` shows the first line of each description
27
+ — one line per target, cut to the terminal's width — and `run <target> --help` shows the whole block.
27
28
 
28
29
  ```sh
29
30
  # runfiles/dev.run
@@ -44,8 +45,23 @@ $ run :list
44
45
 
45
46
  ## Quick start
46
47
 
48
+ Linux and macOS:
49
+
50
+ ```bash
51
+ curl -fsSL https://git.joaoverona.com/joaaoverona/runfile/releases/download/latest/install.sh | sh
52
+ ```
53
+
54
+ Windows, in PowerShell:
55
+
56
+ ```powershell
57
+ irm https://git.joaoverona.com/joaaoverona/runfile/releases/download/latest/install.ps1 | iex
58
+ ```
59
+
60
+ Either one installs a single binary: `~/.local/bin/run`, or `run.exe` under `%LOCALAPPDATA%\runfile\bin`, which
61
+ the PowerShell script adds to your PATH for you — open a new shell and `run` is there. `RUNFILE_VERSION` installs
62
+ a release other than the latest, and `RUNFILE_INSTALL_DIR` chooses where it goes.
63
+
47
64
  ```bash
48
- curl -fsSL https://github.com/JoaaoVerona/runfile/releases/latest/download/install.sh | sh
49
65
  run :init # creates runfiles/ with an example
50
66
  run :list # every target, with descriptions
51
67
  run <target> --help # what one target does, and what it reads
@@ -136,27 +152,27 @@ $ docker exec -i db psql -U app < seed.sql
136
152
 
137
153
  ### Doing several things at once
138
154
 
139
- `.parallel` on a `for` fans out its iterations, and labels every line so you can tell them apart.
140
-
141
- It is block-scoped, so **where it sits is what it covers**: inside the loop it parallelises the iterations and
142
- nothing else, and at the top of the file it would cover every command in the target.
155
+ `parallel for` runs every iteration at once and waits for the last. What is inside one iteration still runs in
156
+ order, so each service is built before it is pushed, and every line carries the name of the iteration it came
157
+ from.
143
158
 
144
159
  ```sh
145
- # runfiles/pull.run
146
- # Pull every service image
160
+ # runfiles/release.run
161
+ # Build and push every service image, then say so
147
162
 
148
- for compose in glob("**/docker-compose.yml")
149
- .parallel
150
- .ignore-errors
151
-
152
- $ docker compose -f {{ compose }} pull
163
+ parallel for svc in ["api", "web", "worker"]
164
+ $ docker build -t {{ svc }} services/{{ svc }}
165
+ $ docker push {{ svc }}
153
166
  end
167
+
168
+ print("all three pushed")
154
169
  ```
155
170
 
156
171
  ```
157
- [docker] api Pulling
158
- [docker] web Pulling
159
- [docker] postgres Pull complete
172
+ web | Successfully built 9a0c41e2b7d3
173
+ api | Successfully built 1f2e86c0a4b5
174
+ worker | The push refers to repository [docker.io/library/worker]
175
+ all three pushed
160
176
  ```
161
177
 
162
178
  ### JSON without the backslashes
@@ -190,7 +206,7 @@ end
190
206
  run _aws -- s3api put-bucket-policy --bucket {{ bucket }} --policy {{ policy }}
191
207
  ```
192
208
 
193
- `run :format` lays the block out, and a missing brace is underlined in your editor as you type — not reported
209
+ `run :lint` lays the block out, and a missing brace is underlined in your editor as you type — not reported
194
210
  by the far end an hour later.
195
211
 
196
212
  ### Reading JSON without jq
@@ -365,22 +381,28 @@ Line-oriented, with one rule: **the language is the default, the shell is marked
365
381
 
366
382
  | Line | Meaning |
367
383
  | --- | --- |
368
- | `# text` | A comment. The leading block is the target's description. |
384
+ | `# text` | A comment, to the end of the line. It may follow code. The leading block is the target's description. |
369
385
  | `.name = value` | A property. |
370
386
  | `$ echo hi` | Hand this line to a shell. |
371
387
  | `exec python3` … `end` | Run a command with the block as its stdin. |
388
+ | `detach $ npm run dev` | Start this one command and do not wait for it. Also `detach exec …`. |
372
389
  | `json` … `end` | A block of JSON, as one value. |
373
- | `let x = 1` | Bind a value. `x = 2` rebinds. `let a, b = pair` takes a list apart. |
390
+ | `let x = 1` | Bind a value. `x = 2` rebinds. `let a, b = pair` takes a list apart. A name is bound before it is read. |
374
391
  | `if` / `else if` / `else` / `end` | Branch. However many `else if`s, one `end`. |
375
392
  | `for x in list` / `end` | Loop over a list. `for k, v in pairs` unpacks each item. |
393
+ | `parallel do` / `parallel for x in list` … `end` | Run each statement inside, or each iteration, at once. |
376
394
  | `while c` / `until c` / `loop` / `end` | Loop on a condition, on its negation, or forever. |
377
395
  | `break` / `continue` | Leave the innermost loop, or start its next pass. |
378
396
  | `match` / `case` / `default` / `end` | Dispatch on a value. A label is a quoted string: `case "linux"`. |
379
397
  | `retry n [every s]` … `end` | Run the block again while it fails, up to `n` times. |
380
398
  | `do` … `end` | A block with no condition, so a property can cover a few commands. |
381
- | `run other-target` | Run another target, in this process. |
399
+ | `run other-target` | Run another target, in this process, starting from this one's environment. |
382
400
  | `print(…)` | Anything else is an expression, evaluated for its effect. |
383
401
 
402
+ A `#` opens a comment where it begins a word — the shell's own rule, so it reads the same on both sides of the
403
+ marker, and `a#b` is one word in either half. The text after `$ ` is the shell's, `#` and all, and so is an
404
+ `exec` block's body; everything else this language reads takes a comment at the end of the line.
405
+
384
406
  ### Values
385
407
 
386
408
  Strings, numbers, booleans and lists — **strict, with no coercion.** `"a" + 1` is an error telling you to use
@@ -390,7 +412,8 @@ Lists nest as deep as you like, which beats packing several fields into one stri
390
412
 
391
413
  ```sh
392
414
  for volume, owner in [
393
- ["prometheus-data", "65534:65534"],
415
+ ["prometheus-data", "65534:65534"], # nobody
416
+ # the rest run as themselves
394
417
  ["kvrocks-data", "999:999"],
395
418
  ]
396
419
  $ docker run --rm -v {{ volume }}:/v alpine chown -R {{ owner }} /v
@@ -448,6 +471,47 @@ end
448
471
  included. Under `--dry-run` a conditional loop walks its body once: a preview performs none of the effects the
449
472
  condition is waiting on, so how often is not a thing it can honestly answer.
450
473
 
474
+ ### Parallel blocks
475
+
476
+ `parallel do` runs every statement directly inside it at once — a `$` line, a `run`, a call, a whole `if` or
477
+ `for` — and waits for the last. `parallel for` does the same with every iteration. Inside a branch everything
478
+ runs in order, as it does anywhere else: only what is marked `parallel` fans out, so nesting one fans out again.
479
+
480
+ ```sh
481
+ # runfiles/check.run
482
+ # Every check at once: tests, lint, and each subproject's own
483
+
484
+ parallel do
485
+ $ cargo test
486
+ $ cargo clippy -- -D warnings
487
+
488
+ parallel for ns in RUN.namespaces
489
+ run {{ ns }}:check
490
+ end
491
+ end
492
+ ```
493
+
494
+ - Each branch works on **its own copy of the variables**. Nothing it binds reaches the other branches or the code
495
+ after the block, so assigning a name bound outside the branch is a parse error — that value would be lost. A
496
+ `let` directly inside `parallel do` binds nothing anyone can read, and is one too.
497
+ - Every branch **runs to completion before a failure is reported**, so a set of services is never left half
498
+ started. `.ignore-errors` on the block forgives each branch.
499
+ - Every line a branch prints carries its name, **in a colour of its own**: the target it runs, the iteration's
500
+ value, or as many words of its command as tell it from its siblings — `cargo test` and `cargo clippy`.
501
+ Siblings take consecutive colours, so the output you are reading side by side is the output guaranteed to
502
+ look different, and the names are padded to the widest of them so the `|` stands in one column:
503
+
504
+ ```
505
+ web-admin | ready on :3001
506
+ web-app | ready on :3000
507
+ web-blog | ready on :3002
508
+ ```
509
+
510
+ `NO_COLOR` turns the colour off, `FORCE_COLOR` turns it on through a pipe. A branch reads nothing from the
511
+ terminal, and `confirm()` refuses to ask inside one.
512
+ - Branches run at once, so neither `break` nor `continue` can reach a loop outside the block, and `break` cannot
513
+ stop a `parallel for`, whose iterations are already running; `continue` ends one of them.
514
+
451
515
  ### Where values come from
452
516
 
453
517
  | Source | From |
@@ -490,7 +554,9 @@ Environment
490
554
  ```
491
555
 
492
556
  Nothing is declared for that — a `?` chain is what makes a value optional, and the literal it ends in is the
493
- default. `run --stdin-args deploy` asks for the same list, in the same order, **before anything runs**.
557
+ default. `run --stdin-args deploy` asks for the same list, in the same order, **before anything runs**. A name
558
+ the target sets for itself with `.env.NAME` — above the read, or in a `_shared.run` — is not on the list at all,
559
+ since nothing the caller passes would reach it.
494
560
 
495
561
  That refusal has one exception, and it is the useful one: **a target that reads `ARGS`**. A wrapper can read
496
562
  any word, so it gets any word — a `--flag` it does not claim for itself joins the positionals where it was
@@ -622,28 +688,148 @@ skipped by `-y`, in CI, and under `--dry-run`, where there is nothing to approve
622
688
  A line that is only a value — `exit`, `abc`, `35` — is a parse error, since it computes something and throws
623
689
  it away. Most often it is a call with the parentheses left off, and the message says so.
624
690
 
691
+ ### Names are checked before anything runs
692
+
693
+ A call to a function the language does not have, a read of a name nothing has bound, or a `RUN` key that does not
694
+ exist stops the run **before its first line** — wherever it sits, a branch that would not have been taken included —
695
+ and every one in the file is reported at once, with what you probably meant:
696
+
697
+ ```bash
698
+ $ run backup
699
+ [runfile] error: /home/you/shop/runfiles/backup.run: line 12: unknown function `exists`; did you mean `directory_exists` or `file_exists`?
700
+ [runfile] error: /home/you/shop/runfiles/backup.run: line 15: `regoin` is not defined; did you mean `region`?
701
+ ```
702
+
703
+ A name counts once any path to the line can have bound it: a `let` inside an `if` still binds for what follows
704
+ its `end`, and a pass of a loop sees what an earlier pass bound. What does not reach past its block is a loop's
705
+ own name and anything a `parallel` branch binds; a `_shared.run` binds with its top-level `let`s. A read behind
706
+ a `?` is checked too, since a name nothing binds takes the fallback every time. And `x = 2` needs a `let x`
707
+ above it — otherwise a typo in the name being updated would quietly start a new one.
708
+
709
+ Your editor underlines the same names as you type them — it is the runner's own check, served by `run :lsp` —
710
+ and `run :lint` reports them for every runfile in the project at once.
711
+
712
+ ### So is what fails every time
713
+
714
+ Some lines fail whenever they run, whatever the run is given, and some never do what they say. Those stop the run
715
+ before its first line too, with what to write instead:
716
+
717
+ ```bash
718
+ $ run release
719
+ [runfile] error: /home/you/app/runfiles/release.run: line 4: `+` needs numbers, and `ARG.build` is a string [wrong-type]; `number(ARG.build)` reads it as one
720
+ [runfile] error: /home/you/app/runfiles/release.run: line 9: `RUN.os == "darwin"` is always false: `RUN.os` is only ever `linux`, `mac` or `windows` [never-equal]; write `"mac"`
721
+ ```
722
+
723
+ Nothing is converted, and an argument or an environment variable is always a string — so `ARG.build + 1` fails every
724
+ time, and so does `if ENV.CI`, since an `if` needs `true` or `false`. A call given one argument too many, a regex that
725
+ does not compile, `xs[-1]`, `case "ok"` under `match $ make`, `if code_of($ make)`: each is reported wherever it sits.
726
+ What a value can be is worked out only as far as the file is sure of it — `ENV.PORT ? 3000` is a string or a number, so
727
+ nothing is said about `+` on it. [LANGUAGE-CHECK-RULES.md](LANGUAGE-CHECK-RULES.md) lists every rule, what it leaves
728
+ alone, and why.
729
+
730
+ ### The shell is checked too
731
+
732
+ The shell in a runfile is read before anything runs as well — every `$` line, every `exec` block for `sh` or
733
+ `bash`, every capture — the way bash will read it, with each `{{ … }}` as the one quoted word it becomes. What
734
+ bash would refuse, and what is wrong every time it runs, stops the run with what to write instead:
735
+
736
+ ```bash
737
+ $ run emulate
738
+ [runfile] error: /home/you/app/runfiles/emulate.run: line 4: `$HOME` in `"$HOME/Android/Sdk"` is kept as written, since a string is not shell -- and on line 9 this value is used as a path, which then starts with `$HOME` itself [unexpanded-string]; write `{{ ENV.HOME }}` in its place
739
+ ```
740
+
741
+ A quote left open, a `fi` with no `if`, an interpolation wrapped in quotes of its own, a `*` the shell never
742
+ expands, `cd` as the last command of its shell, `$1` in a shell that is given no arguments, `[ -f .env; then`,
743
+ `sort names.txt > names.txt`, a `find -exec` with nothing to end it: each rule reports only what is wrong every
744
+ time, and never a line that could be right — no style, no guesses. Your editor
745
+ underlines the same findings and `run :lint` reports them; [SHELL-CHECK-RULES.md](SHELL-CHECK-RULES.md) lists
746
+ every rule, what it leaves alone, and why.
747
+
625
748
  ## Properties
626
749
 
627
750
  Set at the top of the file, or inside a block where marked.
628
751
 
629
- | Property | Effect | In a block? |
630
- | --- | --- | :-: |
631
- | `.shell` | Which shell `$` lines use. | ✅ |
632
- | `.env.NAME` | Set an environment variable. | ✅ |
633
- | `.workdir` | Where commands run. | ✅ |
634
- | `.parallel` | Run this block's commands at once. On a `for`, its iterations. | ✅ |
635
- | `.ignore-errors` | Keep going when a command fails. | ✅ |
636
- | `.logging` | Announce each command on stderr before it runs. Off unless set. | ✅ |
637
- | `.env-file` | Load a `.env` file (encrypted values are decrypted in memory). Appends. | ✅ |
638
- | `.add-path` | Prepend a directory to `PATH`. Appends. | ✅ |
639
- | `.watch` | Re-run when matching files change. A `!` prefix excludes. | |
640
- | `.alias` | Another name for this target. | |
641
- | `.detach` | Start the commands and do not wait. | |
642
- | `.only-in-directories` | Machine-wide targets only: offer this one inside these directories. Appends. | |
752
+ | Property | Effect | In a block? | Flag? |
753
+ | --- | --- | :-: | :-: |
754
+ | `.shell` | Which POSIX shell `$` lines use. | | |
755
+ | `.env.NAME` | Set an environment variable. | ✅ | |
756
+ | `.workdir` | Where commands run. | ✅ | |
757
+ | `.ignore-errors` | Keep going when a command fails. | ✅ | ✅ |
758
+ | `.logging` | Announce each command on stderr before it runs. Off unless set. | | ✅ |
759
+ | `.env-file` | Load a `.env` file (encrypted values are decrypted in memory). Appends. | ✅ | |
760
+ | `.add-path` | Prepend a directory to `PATH`. Appends. | ✅ | |
761
+ | `.watch` | Re-run when matching files change. A `!` prefix excludes. | | |
762
+ | `.only-in-directories` | Machine-wide targets only: offer this one inside these directories. Appends. | | |
763
+
764
+ A **flag** is written bare for `= true`, or given a bool. A constant that can never be one is refused where it
765
+ is written — `.ignore-errors = 23` and `.ignore-errors = "true"` are both errors, and the second says to drop
766
+ the quotes. A value the run works out is left to the run: `.ignore-errors = ENV.CI` and
767
+ `.ignore-errors = ARG.p ? ENV.CI ? "false"` are how a flag is decided from outside the file, and there `true`,
768
+ `false`, `1` and `0` are all understood. Anything else stops the run rather than reading as `false`, because
769
+ a flag that quietly did not take effect is found out much later.
770
+
771
+ A property is applied **where it is written**: it can read a binding above it, and it takes effect from there
772
+ down. The environment works the same way — a value reads it as it stands at its own line, so a property can be
773
+ built from what a `.env-file` above it loaded, or from another `.env` above it:
774
+
775
+ ```sh
776
+ # runfiles/build.run
777
+ # Build into a directory the command line picks
778
+
779
+ let out = concat("target-", ARG.profile ? "debug")
780
+
781
+ .workdir = out
782
+
783
+ $ ls
784
+ ```
785
+
786
+ ```sh
787
+ # runfiles/migrate.run
788
+ # Migrate the database the local .env points at
789
+
790
+ .env-file = ".env"
791
+ .env.DATABASE_URL = concat("postgres://", ENV.DB_HOST, "/app")
792
+
793
+ $ sqlx migrate run
794
+ ```
795
+
796
+ Four of them describe the whole file rather than the commands under them — `.shell`, `.logging`, `.watch` and
797
+ `.only-in-directories` — and those have to be written above its first statement.
798
+
799
+ `.shell` names one of the eight shells `$` knows how to drive: `sh`, `bash`, `dash`, `ash`, `zsh`, `ksh`,
800
+ `busybox` or `brush`. Any other interpreter is named on the line instead, with `exec` — which is the same
801
+ mechanism, and says so where it can be read:
802
+
803
+ ```sh
804
+ # runfiles/list.run
805
+ # List the current directory, whatever the platform
806
+
807
+ if RUN.os == "windows"
808
+ exec pwsh
809
+ Get-ChildItem
810
+ end
811
+ else
812
+ $ ls -la
813
+ end
814
+ ```
815
+
816
+ **The target's own `.env.NAME` wins over the caller's environment; a `.env-file` does not.** An assignment
817
+ written in the file is the one way a target can force a value, so `PORT=4000 run serve` still serves on 3000
818
+ under `.env.PORT = "3000"` — and `ENV.PORT` and a command's `$PORT` both say 3000. A file is a default, the
819
+ dotenv convention: what the caller exported beats it. A default the caller *may* override is spelled out, and
820
+ `--help` shows it as one:
821
+
822
+ ```sh
823
+ # runfiles/serve.run
824
+ # Serve locally
825
+
826
+ .env.PORT = ENV.PORT ? "3000"
827
+
828
+ $ node server.js
829
+ ```
643
830
 
644
831
  `.env-file` and `.add-path` **append**, so a block adds to what it inherited rather than replacing it, and a
645
- block's file can be named by something the body worked out — which a header property cannot do, because it
646
- resolves before any statement runs:
832
+ block's file can be named by something that block worked out:
647
833
 
648
834
  ```sh
649
835
  # runfiles/deploy.run
@@ -670,6 +856,33 @@ runfiles/_aws.run → run _aws (a helper other targets call)
670
856
  runfiles/deploy.run → run deploy
671
857
  ```
672
858
 
859
+ **A target that `run`s another hands it its environment.** Whatever a command on that line would be given —
860
+ the caller's `.env`, `.env-file` and `.add-path`, a block's as well as the header's, and what the caller was run
861
+ with itself — is where the target it runs starts. That target layers its own settings over it the way it would
862
+ over your shell: its own `.env` wins, and its own `.add-path` goes in front. So a helper needs no environment of
863
+ its own:
864
+
865
+ ```sh
866
+ # runfiles/_aws.run
867
+ # The AWS CLI, with whatever credentials the calling target loaded
868
+
869
+ $ docker run --rm -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY amazon/aws-cli {{ ARGS }}
870
+ ```
871
+
872
+ ```sh
873
+ # runfiles/backups.run
874
+ # List the production backups
875
+
876
+ .env-file = ".env.production"
877
+
878
+ run _aws s3 ls s3://example-backups
879
+ ```
880
+
881
+ A value that only a `.env-file` supplied is still a default when it arrives, so the target's own `.env-file`
882
+ replaces it: a `ci` target that loads `.env` and runs `test` does not push the development database over
883
+ `test`'s own `.env.test`. `$ run test` is different. A new process can only be handed variables, so there the
884
+ caller's `.env-file` values arrive as if you had exported them, and they beat `test`'s own file.
885
+
673
886
  `do … end` is a block with no condition — somewhere for a property to go when it should cover a few commands
674
887
  and not the whole target:
675
888
 
@@ -738,6 +951,10 @@ excluded it. Relative entries are relative to your home. It is the one property
738
951
  set: their targets are visible to anyone reading the repository, so hiding some by working directory would
739
952
  bring back the very invisibility this exists to fix.
740
953
 
954
+ A scope decides where a target is offered, not what gets checked. `run :lint` inside the machine-wide directory
955
+ checks every file in it, scoped or not — and so does `run :lint --include-global` from anywhere else, since a
956
+ project's lint leaves the directory out otherwise.
957
+
741
958
  **Not in CI.** A runner's home directory is nobody's, so `run` reads none of the three there: what runs is
742
959
  what is checked in and reviewable. This is also why nothing has to be cleaned up after a job.
743
960
 
@@ -770,11 +987,11 @@ whose state is still worth keeping, so a `setup` run under it is still recorded.
770
987
  | `run <target> [args…]` | Run a target |
771
988
  | `run :list` | List every target (`--names`, `--json` for tooling) |
772
989
  | `run :init` | Create `runfiles/` with an example |
773
- | `run :format [path…]` | Format runfiles in place (`--check`, `--stdout`) |
990
+ | `run :lint [path…]` | Format runfiles in place and report anything the runner would refuse (`--check` writes nothing, `--stdout`) |
774
991
  | `run :env <sub>` | Manage `.env` files: `init`, `get`, `set`, `encrypt`, `decrypt`, `rotate`, `inject`, `secret-keys` |
775
992
  | `run :completions <command> <shell>` | `install`, `uninstall` or `output` a completion script for `bash`, `zsh`, `fish` or `powershell` |
776
993
  | `run :generate <editor>` | Task files for `zed`, `jetbrains` or `vscode`, merged into what is there |
777
- | `run :update` | Update the binary |
994
+ | `run :update [version]` | Update the binary to the newest release, or to the tag named; `--channel=github` takes it from the GitHub mirror, `--force` reinstalls |
778
995
 
779
996
  | Flag | |
780
997
  | --- | --- |
@@ -787,11 +1004,29 @@ whose state is still worth keeping, so a `setup` run under it is still recorded.
787
1004
 
788
1005
  Flags belong **before** the target name; everything after it is passed to the target.
789
1006
 
790
- `run :format` has no settings: one shape, everywhere. It reindents with tabs, spaces expressions, places blank
1007
+ `run :lint` puts every runfile into the one shape there is, then reports everything the runner would refuse — a
1008
+ syntax error, an unknown or misplaced property, a call to a function that does not exist, a name nothing binds,
1009
+ a `run` of a target that is not there, a line that [fails every time it runs](LANGUAGE-CHECK-RULES.md), shell the
1010
+ [shell checker](SHELL-CHECK-RULES.md) finds wrong — as
1011
+ `file:line:column`, which is the list your editor underlines. **It
1012
+ exits 0 only when every file is formatted and none of that is wrong**, so a file that passes runs without a
1013
+ parse-time error and opens with nothing underlined. `--check` writes nothing and fails on a file that needs
1014
+ formatting too, which is what a CI step wants.
1015
+
1016
+ ```bash
1017
+ $ run :lint --check
1018
+ runfiles/deploy.run: needs formatting
1019
+ runfiles/deploy.run:12:9: error: unknown function `exists`; did you mean `directory_exists` or `file_exists`?
1020
+ 14 files checked: 1 needs formatting, 1 error in 1 file
1021
+ ```
1022
+
1023
+ A shell finding says what to write instead on the line under it. `run :lint` reports it and leaves the shell as
1024
+ you wrote it: only the layout of a file is ever rewritten.
1025
+
1026
+ Formatting has no settings: one shape, everywhere. It reindents with tabs, spaces expressions, places blank
791
1027
  lines, lays out `json` blocks, and leaves the three things that are not the language's to touch — strings, the
792
- text after `$ `, and `exec` bodies — exactly as written. It refuses a file that does not parse, and checks that
793
- its own output still means the same thing before writing it. `--check` reports what would change and exits 1,
794
- which is what a CI step wants.
1028
+ text after `$ `, and `exec` bodies — exactly as written. A file that does not parse is left alone, and the
1029
+ formatter checks that its output still means the same thing before writing it.
795
1030
 
796
1031
  ## Encrypted environment variables
797
1032
 
@@ -813,33 +1048,48 @@ Keys live in the OS credential store — Keychain, Credential Manager, or Secret
813
1048
  fallback. In CI, pass them as `RUNFILE_PRIVATE_KEYS` (newline-separated) and no credential store is involved.
814
1049
 
815
1050
  Decryption happens in memory; secrets never reach disk. The credential store is only touched when something
816
- actually decrypts, so a locked keyring never gets in the way of an unrelated target.
1051
+ actually decrypts, so a locked keyring never gets in the way of an unrelated target — and it is asked once in
1052
+ a run, however many of the targets that run calls decrypt. In watch mode each re-run asks again, so unlocking
1053
+ the keyring or adding a missing key takes effect on the next save.
817
1054
 
818
1055
  ## Editor support
819
1056
 
820
- `runfile-lsp` ships beside `run` and gives you diagnostics, completion, formatting, go-to-definition and
821
- documentation on hover — from the same parser `run` itself uses, so your editor never disagrees with what will
822
- actually happen.
1057
+ `run :lsp` is a language server, so the binary you already have gives you diagnostics, completion, formatting,
1058
+ go-to-definition and documentation on hover — from the same parser `run` itself uses, so your editor never
1059
+ disagrees with what will actually happen. There is nothing extra to install.
1060
+
1061
+ Diagnostics include every call to a function that does not exist and every name nothing has bound, underlined
1062
+ where it is written, with the name you probably meant. It is the check that stops a run before it starts, so it
1063
+ knows what the `_shared.run` above the file binds.
823
1064
 
824
1065
  Hover anything: a function shows its signature, what it does and a worked example; a property adds whether it
825
1066
  may sit inside a block; `$`, `exec`, `run`, `retry`, `match` and the rest explain the line form itself.
826
1067
 
1068
+ Completion knows where it is. At the start of a line it offers the keywords that fit there — `break` inside a
1069
+ loop, `case` inside a `match`, `end` only when something is open — and everywhere a value goes, the variables in
1070
+ scope: the file's own, its loop's, and those bound in the `_shared.run` above it. Inside `{{ … }}` on a `$` line
1071
+ it completes the same way, and in the rest of a shell line or an `exec` body it stays out of the way.
1072
+
827
1073
  Ctrl+click a `run <target>` to open that target's file, or a variable to jump to where it was bound — including
828
1074
  into the `_shared.run` above it, which is the one definition you cannot find by reading the file in front of
829
1075
  you.
830
1076
 
831
- It also hands `$` lines and shell `exec` bodies to [shellcheck](https://www.shellcheck.net) when it is
832
- installed, mapping findings back to the lines you wrote.
1077
+ The shell in `$` lines and shell `exec` bodies is checked by the runner's own [shell checker](SHELL-CHECK-RULES.md),
1078
+ which reads each `{{ … }}` as the word it becomes — so nothing is reported against text nobody wrote, and there
1079
+ is nothing to install.
833
1080
 
834
- **VS Code** — install the `.vsix` from the [latest release](https://github.com/JoaaoVerona/runfile/releases).
835
- You get a Run button on every target, a task provider, a sidebar tree, and shell lines coloured exactly as the
836
- same command in a `.sh` file. Set `"editor.formatOnSave": true` and saving formats.
1081
+ **VS Code** — install the `.vsix` from the [latest release](https://git.joaoverona.com/joaaoverona/runfile/releases).
1082
+ You get a Run button on every target, a task provider, a sidebar tree, shell lines coloured exactly as the
1083
+ same command in a `.sh` file, and a call to a function that does not exist coloured as an error as soon as you
1084
+ type its `(`. Set `"editor.formatOnSave": true` and saving formats.
837
1085
 
838
1086
  **JetBrains IDEs** read the same grammar: *Settings → Editor → TextMate Bundles*, add the `editors/vscode`
839
1087
  directory from a checkout.
840
1088
 
841
- **Zed, Neovim and Helix** use the tree-sitter grammar in `editors/tree-sitter`, with highlight queries
842
- included. For Neovim with nvim-treesitter:
1089
+ **Zed, Neovim and Helix** point their language-server configuration at `run` with the argument `:lsp` — in
1090
+ Neovim, `vim.lsp.config("runfile", { cmd = { "run", ":lsp" }, filetypes = { "runfile" } })` — and take the
1091
+ tree-sitter grammar in `editors/tree-sitter` for highlighting, with highlight queries included. For Neovim
1092
+ with nvim-treesitter:
843
1093
 
844
1094
  ```lua
845
1095
  require("nvim-treesitter.parsers").get_parser_configs().runfile = {
@@ -905,7 +1155,7 @@ Shell is still there. It is just marked.
905
1155
  | Windows | x86-64, arm64 |
906
1156
 
907
1157
  `$` lines use bash where it exists, Git Bash on Windows, and `sh` otherwise — so one file works everywhere. Set
908
- `.shell` to pin something else.
1158
+ `.shell` to pin one of the other POSIX shells, or name any other interpreter with `exec`.
909
1159
 
910
1160
  ## License
911
1161
 
Binary file
Binary file
Binary file
package/bin/linux-x64/run CHANGED
Binary file
package/bin/run.js CHANGED
@@ -2,10 +2,7 @@
2
2
  const path = require('node:path');
3
3
  const { spawnSync } = require('node:child_process');
4
4
  const KEY = process.platform + '-' + process.arch;
5
- // One launcher for both binaries, `run` and `runfile-lsp`: which one is
6
- // decided by this file's own name, so the second is a copy, not a second file.
7
- const NAME = path.basename(__filename, ".js");
8
- const EXE = process.platform === "win32" ? `${NAME}.exe` : NAME;
5
+ const EXE = process.platform === "win32" ? "run.exe" : "run";
9
6
  const BINARIES = {
10
7
  "linux-x64": EXE,
11
8
  "linux-arm64": EXE,
Binary file
Binary file
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "bin": {
3
- "run": "bin/run.js",
4
- "runfile-lsp": "bin/runfile-lsp.js"
3
+ "run": "bin/run.js"
5
4
  },
6
5
  "description": "Runfile CLI — a cross-platform command runner with its own small language",
7
6
  "engines": {
@@ -14,5 +13,5 @@
14
13
  "license": "MIT",
15
14
  "name": "@runfile/cli",
16
15
  "repository": "https://github.com/JoaaoVerona/runfile",
17
- "version": "1.1.6"
16
+ "version": "1.7.1"
18
17
  }
Binary file
Binary file
Binary file
Binary file
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env node
2
- const path = require('node:path');
3
- const { spawnSync } = require('node:child_process');
4
- const KEY = process.platform + '-' + process.arch;
5
- // One launcher for both binaries, `run` and `runfile-lsp`: which one is
6
- // decided by this file's own name, so the second is a copy, not a second file.
7
- const NAME = path.basename(__filename, ".js");
8
- const EXE = process.platform === "win32" ? `${NAME}.exe` : NAME;
9
- const BINARIES = {
10
- "linux-x64": EXE,
11
- "linux-arm64": EXE,
12
- "darwin-arm64": EXE,
13
- "darwin-x64": EXE,
14
- "win32-x64": EXE,
15
- "win32-arm64": EXE,
16
- };
17
- const bin = BINARIES[KEY];
18
-
19
- if (!bin) {
20
- console.error(`run: unsupported platform ${KEY}. Supported: ${Object.keys(BINARIES).join(', ')}`);
21
- process.exit(1);
22
- }
23
-
24
- const binPath = path.join(__dirname, KEY, bin);
25
- const result = spawnSync(binPath, process.argv.slice(2), { stdio: 'inherit' });
26
-
27
- if (result.error) {
28
- console.error(`run: failed to spawn ${binPath}: ${result.error.message}`);
29
- process.exit(1);
30
- }
31
-
32
- process.exit(result.status == null ? 1 : result.status);
Binary file
Binary file