@zendero/runctl 0.1.11 → 0.1.13

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
@@ -196,11 +196,13 @@ Listen on `process.env.PORT` (runctl sets it). Optional: `runctl start --script
196
196
 
197
197
  **CLI vs `run-lib.sh`:** Most apps only need the **`runctl`** binary and `package.json` scripts. For shell-heavy repos, [`examples/run.sh.example`](examples/run.sh.example) shows sourcing **`lib/run-lib.sh`** (same library the CLI uses). Resolve the installed path with **`runctl lib-path`**.
198
198
 
199
+ **Scaffold `run.sh`:** `runctl run-sh --write` writes that example to `./run.sh` (honors `RUNCTL_PROJECT_ROOT`; use `-C dir` for another directory). Use `--force` to replace an existing file. Equivalent to redirecting `runctl run-sh` to `run.sh`, but also sets the executable bit.
200
+
199
201
  **CI:** Prefer **`pnpm add -D @zendero/runctl`** (or a global install) so `runctl` is on `PATH` with a stable version. **`pnpm dlx @zendero/runctl`** is fine for one-off recovery; avoid relying on it for every CI job (cold cache / latency).
200
202
 
201
203
  **Roadmap (ideas):** `runctl exec` (one-off commands with the same port / `.run` contract as `start`); optional HTTP health gate before “ready”.
202
204
 
203
- **Develop this repo:** `pnpm install` → `./run.sh` (default **doctor**, like `elata-bio-sdk/run.sh`) → `./run.sh ports` · **`pnpm test`** runs [`tests/run-all.sh`](tests/run-all.sh) (Jest-style output: suites, ✓/✗, `PASS`/`FAIL` per file, shared helpers in [`tests/lib/test-runner.sh`](tests/lib/test-runner.sh))
205
+ **Develop this repo:** `pnpm install` → `./run.sh` (thin runner; default **help**) delegates to **`bin/runctl`**. Maintainer npm flows live in [`scripts/maintain.sh`](scripts/maintain.sh). **`pnpm test`** runs [`tests/run-all.sh`](tests/run-all.sh) (Jest-style output: suites, ✓/✗, `PASS`/`FAIL` per file, shared helpers in [`tests/lib/test-runner.sh`](tests/lib/test-runner.sh))
204
206
 
205
207
  **Publish (maintainers)** — workflow similar to elata’s release preflight, scaled for one package:
206
208
 
package/bin/runctl CHANGED
@@ -2,6 +2,11 @@
2
2
  # Runctl CLI — installed via pnpm/npm (devDependency .bin or global).
3
3
  set -euo pipefail
4
4
  RUNCTL_PKG_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)"
5
+ # Load optional repo .env / npm token (consumer project: set RUNCTL_PROJECT_ROOT or run from repo root).
6
+ # shellcheck source=../lib/repo-env.sh
7
+ source "$RUNCTL_PKG_ROOT/lib/repo-env.sh"
8
+ runctl_load_repo_env "${RUNCTL_PROJECT_ROOT:-$PWD}"
9
+
5
10
 
6
11
  # Volta resolves pnpm from the *current* project's packageManager for `pnpm -v`, but
7
12
  # `pnpm add -g` uses the default global pnpm (often older). Run pnpm as this package
@@ -18,25 +23,17 @@ _runctl_read_pnpm_version_from_package_json() {
18
23
  ' "$RUNCTL_PKG_ROOT/package.json" 2>/dev/null || true
19
24
  }
20
25
 
21
- # Shown after runctl update (pnpm) and in --help. pnpm often suggests "pnpm self-update" but that
22
- # updates a different notion of "current" than the pnpm used for global -g installs (Volta/Corepack).
26
+ # After a successful pnpm global update: short tip (global pnpm vs project pin).
23
27
  _runctl_print_pnpm_version_fixup_hint() {
24
28
  local ver
25
29
  ver="$(_runctl_read_pnpm_version_from_package_json)"
26
30
  [[ -n "$ver" ]] || ver="X.Y.Z"
27
31
  cat >&2 <<EOF
28
32
 
29
- runctl update: pnpm may show "Update available" or tell you to run pnpm self-update; self-update
30
- often does nothing useful here: it follows your project packageManager, while global installs use a
31
- separate default pnpm. If the footer still shows an older pnpm than you expect, fix that default — do
32
- not rely on pnpm self-update alone. Replace ${ver} with the version you want (match package.json).
33
-
34
- volta install pnpm@${ver}
35
- corepack enable && corepack prepare pnpm@${ver} --activate
36
- RUNCTL_PNPM_VERSION=${ver} runctl update git
37
- runctl update --pm npm
38
-
39
- Hide this hint: RUNCTL_UPDATE_SKIP_PNPM_HINT=1 (also suppressed when CI is set.)
33
+ ${_d}┌${_r} ${_g}runctl${_r} ${_b}tip${_r} ${_d}· global pnpm can differ from your repo's packageManager pin.${_r}
34
+ ${_d}│${_r} ${_c}volta install pnpm@${ver}${_r} ${_d}·${_r} ${_c}corepack prepare pnpm@${ver} --activate${_r}
35
+ ${_d}│${_r} ${_c}RUNCTL_PNPM_VERSION=${ver} runctl update git${_r} ${_d}·${_r} ${_c}runctl update --pm npm${_r}
36
+ ${_d}└${_r} ${_d}hide:${_r} ${_c}RUNCTL_UPDATE_SKIP_PNPM_HINT=1${_r}
40
37
  EOF
41
38
  }
42
39
 
@@ -74,6 +71,19 @@ if [[ -t 1 ]]; then
74
71
  _c=$'\033[36m' _y=$'\033[33m' _g=$'\033[32m'
75
72
  fi
76
73
 
74
+ # Semver from this package's package.json (for --version and update banners).
75
+ _runctl_pkg_version() {
76
+ local ver="?"
77
+ if command -v node >/dev/null 2>&1; then
78
+ ver="$(
79
+ node -e 'const fs=require("fs"); console.log(JSON.parse(fs.readFileSync(process.argv[1],"utf8")).version);' \
80
+ "$RUNCTL_PKG_ROOT/package.json" 2>/dev/null || echo "?"
81
+ )"
82
+ fi
83
+ printf '%s' "$ver"
84
+ }
85
+
86
+
77
87
  usage() {
78
88
  cat <<EOF
79
89
  ${_b}runctl${_r} — dev-server orchestrator & port manager
@@ -91,12 +101,18 @@ ${_y}Commands${_r}
91
101
  ${_c}ports gc${_r} Clean up stale port claims
92
102
  ${_c}env expand${_r} <manifest> [opts] Generate .env.local from manifest
93
103
  ${_c}doctor${_r} Check Node, tooling, and project basics
104
+ ${_c}run-sh${_r} [--print|--write] [opts] Emit example ${_d}run.sh${_r} ${_d}(see runctl run-sh --help)${_r}
94
105
  ${_c}update${_r} [npm|git|auto] [opts] Update global runctl ${_d}(default: auto; see runctl update --help)${_r}
106
+ ${_c}install${_r} [args] Install dependencies ${_d}(pnpm preferred; npm fallback when PM=pnpm)${_r}
107
+ ${_c}run${_r} <script> [args] ${_d}package manager run (PM env, default pnpm)${_r}
108
+ ${_c}exec${_r} [args] ${_d}package manager exec${_r}
109
+ ${_c}test${_r} [args] ${_d}package manager test${_r}
95
110
 
96
111
  ${_y}Options${_r}
97
112
  ${_c}--script${_r} <name> Package script to run ${_d}(default: dev)${_r}
98
113
  ${_c}--open${_r} After a successful start, open the app URL ${_d}(same as ${_c}runctl open${_r})${_r}
99
- ${_c}version${_r} | ${_c}--version${_r} | ${_c}-v${_r} Print version and install path
114
+ ${_c}RUNCTL_PROJECT_ROOT${_r} Repo root for .env + install/run/test ${_d}(default: cwd)${_r}
115
+ ${_c}version${_r} | ${_c}--version${_r} | ${_c}-v${_r} Print version and package install path
100
116
  ${_c}--help${_r}, ${_c}-h${_r} Show this help
101
117
 
102
118
  ${_d}Quick start — add to package.json:
@@ -415,9 +431,10 @@ cmd_update() {
415
431
  fi
416
432
  [[ -n "$MODE" ]] || MODE="auto"
417
433
 
418
- local pm git_target
434
+ local pm git_target ver_now
419
435
  pm="$(cmd_update_pick_pm "$PM")" || exit 1
420
436
  git_target="${GIT_BASE}#${GIT_REF}"
437
+ ver_now="$(_runctl_pkg_version)"
421
438
 
422
439
  _remove_conflicting_global_runctl() {
423
440
  # A legacy package named "runctl" can own the same binary name and shadow @zendero/runctl.
@@ -450,12 +467,12 @@ cmd_update() {
450
467
 
451
468
  _update_registry() {
452
469
  local spec="${PKG}@latest"
453
- echo "runctl update: installing latest from registry ($spec)..."
470
+ printf '%s\n' "${_c}▶${_r} ${_b}runctl update${_r} ${_d}·${_r} ${_c}v${ver_now}${_r} ${_d}·${_r} registry ${_g}${spec}${_r}" >&2
454
471
  _remove_conflicting_global_runctl
455
472
  if [[ "$pm" == "pnpm" ]]; then
456
- _runctl_pnpm_exec add -g --force "$spec"
473
+ _runctl_pnpm_exec add -g --force --loglevel error "$spec"
457
474
  else
458
- npm install -g --force "$spec"
475
+ npm install -g --force --silent --no-fund --no-audit "$spec"
459
476
  fi
460
477
  }
461
478
 
@@ -463,17 +480,17 @@ cmd_update() {
463
480
  local sha spec
464
481
  if sha="$(_resolve_git_ref_sha "$GIT_BASE" "$GIT_REF")"; then
465
482
  spec="${GIT_BASE}#${sha}"
466
- echo "runctl update: installing from Git ref $GIT_REF (resolved to ${sha:0:12})..."
483
+ printf '%s\n' "${_c}▶${_r} ${_b}runctl update${_r} ${_d}·${_r} ${_c}v${ver_now}${_r} ${_d}·${_r} git ${_g}${GIT_REF}${_r} ${_d}@${sha:0:12}${_r}" >&2
467
484
  else
468
485
  spec="$git_target"
469
- echo "runctl update: installing from Git ($git_target)..."
486
+ printf '%s\n' "${_c}▶${_r} ${_b}runctl update${_r} ${_d}·${_r} ${_c}v${ver_now}${_r} ${_d}·${_r} git ${_g}${git_target}${_r}" >&2
470
487
  echo "runctl update: could not resolve ref to SHA; proceeding with ref directly." >&2
471
488
  fi
472
489
  _remove_conflicting_global_runctl
473
490
  if [[ "$pm" == "pnpm" ]]; then
474
- _runctl_pnpm_exec add -g --force "$spec"
491
+ _runctl_pnpm_exec add -g --force --loglevel error "$spec"
475
492
  else
476
- npm install -g --force "$spec"
493
+ npm install -g --force --silent --no-fund --no-audit "$spec"
477
494
  fi
478
495
  }
479
496
 
@@ -496,6 +513,10 @@ cmd_update() {
496
513
  ;;
497
514
  esac
498
515
 
516
+ local ver_installed
517
+ ver_installed="$(_runctl_pkg_version)"
518
+ printf '%s\n' "${_g}✓${_r} ${_b}runctl${_r} ${_c}v${ver_installed}${_r} ${_d}· global install refreshed${_r}" >&2
519
+
499
520
  _runctl_maybe_print_pnpm_update_hint "$pm"
500
521
  }
501
522
 
@@ -582,15 +603,185 @@ cmd_logs() {
582
603
  tail -n "$lines" "$logf"
583
604
  }
584
605
 
606
+ _runctl_pm() {
607
+ printf '%s' "${PM:-pnpm}"
608
+ }
609
+
610
+ cmd_install() {
611
+ local root="${RUNCTL_PROJECT_ROOT:-$PWD}"
612
+ if [[ "$(_runctl_pm)" == "pnpm" ]]; then
613
+ if command -v pnpm >/dev/null 2>&1; then
614
+ (cd "$root" && exec pnpm install "$@")
615
+ elif command -v npm >/dev/null 2>&1; then
616
+ echo "runctl install: pnpm not found; using npm" >&2
617
+ (cd "$root" && exec npm install "$@")
618
+ else
619
+ echo "runctl install: install pnpm (preferred) or npm" >&2
620
+ exit 1
621
+ fi
622
+ else
623
+ local pm="$(_runctl_pm)"
624
+ command -v "$pm" >/dev/null 2>&1 || {
625
+ echo "runctl install: $pm not on PATH" >&2
626
+ exit 1
627
+ }
628
+ (cd "$root" && exec "$pm" install "$@")
629
+ fi
630
+ }
631
+
632
+ cmd_run() {
633
+ local root="${RUNCTL_PROJECT_ROOT:-$PWD}"
634
+ local pm="$(_runctl_pm)"
635
+ command -v "$pm" >/dev/null 2>&1 || {
636
+ echo "runctl run: $pm not on PATH (set PM=…)" >&2
637
+ exit 1
638
+ }
639
+ (cd "$root" && exec "$pm" run "$@")
640
+ }
641
+
642
+ cmd_exec() {
643
+ local root="${RUNCTL_PROJECT_ROOT:-$PWD}"
644
+ local pm="$(_runctl_pm)"
645
+ command -v "$pm" >/dev/null 2>&1 || {
646
+ echo "runctl exec: $pm not on PATH (set PM=…)" >&2
647
+ exit 1
648
+ }
649
+ (cd "$root" && exec "$pm" exec "$@")
650
+ }
651
+
652
+ cmd_test() {
653
+ local root="${RUNCTL_PROJECT_ROOT:-$PWD}"
654
+ local pm="$(_runctl_pm)"
655
+ command -v "$pm" >/dev/null 2>&1 || {
656
+ echo "runctl test: $pm not on PATH (set PM=…)" >&2
657
+ exit 1
658
+ }
659
+ (cd "$root" && exec "$pm" test "$@")
660
+ }
661
+
662
+ cmd_run_sh_usage() {
663
+ cat <<EOF
664
+ ${_b}runctl run-sh${_r} — emit the canonical example ${_c}run.sh${_r} (thin wrapper → runctl)
665
+
666
+ ${_y}Usage:${_r}
667
+ runctl run-sh [--print]
668
+ runctl run-sh --write [--force] [-C dir]
669
+
670
+ ${_y}Description${_r}
671
+ Reads ${_c}examples/run.sh.example${_r} from this package (falls back to ${_c}run.sh${_r}).
672
+
673
+ ${_c}--print${_r} ${_d}(default)${_r} prints to stdout — redirect into your repo:
674
+ ${_d}runctl run-sh > run.sh && chmod +x run.sh${_r}
675
+
676
+ ${_c}--write${_r} writes ${_c}run.sh${_r} under the target directory (default: ${_c}\$PWD${_r} or ${_c}RUNCTL_PROJECT_ROOT${_r} when set).
677
+ Refuses to overwrite an existing file unless ${_c}--force${_r}.
678
+
679
+ ${_y}Options${_r}
680
+ ${_c}--print${_r} Print to stdout ${_d}(default when --write not used)${_r}
681
+ ${_c}--write${_r} | ${_c}-w${_r} Write ${_c}run.sh${_r} in the target directory
682
+ ${_c}--force${_r} | ${_c}-f${_r} Overwrite ${_c}run.sh${_r} if it already exists
683
+ ${_c}-C${_r} ${_c}dir${_r} | ${_c}--dir${_r} ${_c}dir${_r} Target directory (default: ${_c}RUNCTL_PROJECT_ROOT${_r} or cwd)
684
+ ${_c}-h${_r} | ${_c}--help${_r} Show this help
685
+ EOF
686
+ }
687
+
688
+ _runctl_run_sh_example_path() {
689
+ local ex="$RUNCTL_PKG_ROOT/examples/run.sh.example"
690
+ local fb="$RUNCTL_PKG_ROOT/run.sh"
691
+ if [[ -f "$ex" && -r "$ex" ]]; then
692
+ printf '%s' "$ex"
693
+ return 0
694
+ fi
695
+ if [[ -f "$fb" && -r "$fb" ]]; then
696
+ printf '%s' "$fb"
697
+ return 0
698
+ fi
699
+ return 1
700
+ }
701
+
702
+ cmd_run_sh() {
703
+ local action=print
704
+ local force=0
705
+ local target_dir=""
706
+
707
+ while [[ $# -gt 0 ]]; do
708
+ case "$1" in
709
+ --print)
710
+ action=print
711
+ shift
712
+ ;;
713
+ --write | -w)
714
+ action=write
715
+ shift
716
+ ;;
717
+ --force | -f)
718
+ force=1
719
+ shift
720
+ ;;
721
+ -C | --dir)
722
+ if [[ $# -lt 2 ]]; then
723
+ echo "runctl run-sh: $1 requires a directory" >&2
724
+ exit 1
725
+ fi
726
+ target_dir="$(cd "$2" && pwd)" || exit 1
727
+ shift 2
728
+ ;;
729
+ -h | --help)
730
+ cmd_run_sh_usage
731
+ return 0
732
+ ;;
733
+ -*)
734
+ echo "runctl run-sh: unknown option: $1" >&2
735
+ cmd_run_sh_usage >&2
736
+ exit 1
737
+ ;;
738
+ *)
739
+ echo "runctl run-sh: unexpected argument: $1" >&2
740
+ cmd_run_sh_usage >&2
741
+ exit 1
742
+ ;;
743
+ esac
744
+ done
745
+
746
+ if [[ -z "$target_dir" ]]; then
747
+ if [[ -n "${RUNCTL_PROJECT_ROOT:-}" && -d "$RUNCTL_PROJECT_ROOT" ]]; then
748
+ target_dir="$(cd "$RUNCTL_PROJECT_ROOT" && pwd)"
749
+ else
750
+ target_dir="$(pwd)"
751
+ fi
752
+ fi
753
+
754
+ local path
755
+ path="$(_runctl_run_sh_example_path)" || {
756
+ echo "runctl run-sh: example not found under $RUNCTL_PKG_ROOT" >&2
757
+ exit 1
758
+ }
759
+
760
+ if [[ "$action" == "write" ]]; then
761
+ local out="$target_dir/run.sh"
762
+ if [[ -e "$out" && "$force" -ne 1 ]]; then
763
+ echo "runctl run-sh: $out already exists (use --force to overwrite)" >&2
764
+ exit 1
765
+ fi
766
+ cat "$path" >"$out"
767
+ chmod +x "$out"
768
+ echo "runctl run-sh: wrote $out" >&2
769
+ return 0
770
+ fi
771
+
772
+ cat "$path"
773
+ }
774
+
585
775
  cmd_version_print() {
586
- local ver="?"
587
- if command -v node >/dev/null 2>&1; then
588
- ver="$(
589
- node -e 'const fs=require("fs"); console.log(JSON.parse(fs.readFileSync(process.argv[1],"utf8")).version);' \
590
- "$RUNCTL_PKG_ROOT/package.json" 2>/dev/null || echo "?"
591
- )"
776
+ local ver
777
+ ver="$(_runctl_pkg_version)"
778
+ if [[ -t 1 ]]; then
779
+ printf '%s\n\n' "${_b}runctl${_r} ${_c}v${ver}${_r}"
780
+ else
781
+ printf 'runctl %s\n\n' "$ver"
592
782
  fi
593
- printf '%s\n' "runctl $ver — $RUNCTL_PKG_ROOT"
783
+ printf '%s\n' "${_d}Install:${_r}"
784
+ printf '%s\n' " $RUNCTL_PKG_ROOT"
594
785
  }
595
786
 
596
787
  main() {
@@ -607,6 +798,11 @@ main() {
607
798
  env) cmd_env "$@" ;;
608
799
  doctor) cmd_doctor "$@" ;;
609
800
  update) cmd_update "$@" ;;
801
+ install) cmd_install "$@" ;;
802
+ run) cmd_run "$@" ;;
803
+ exec) cmd_exec "$@" ;;
804
+ test) cmd_test "$@" ;;
805
+ run-sh) cmd_run_sh "$@" ;;
610
806
 
611
807
  # Plumbing
612
808
  lib-path) printf '%s\n' "$RUNCTL_PKG_ROOT/lib/run-lib.sh" ;;
@@ -13,7 +13,7 @@ Keep a **manifest** (not loaded directly by Next/Vite) that lists each real valu
13
13
 
14
14
  ```bash
15
15
  pnpm exec runctl env expand env.manifest --out .env.local
16
- # or: node node_modules/@zendero/runctl/scripts/expand-env-manifest.mjs env.manifest --out .env.local
16
+ # or: runctl env expand env.manifest --out .env.local
17
17
  ```
18
18
 
19
19
  Add **`.env.local`** (and optionally `.env.manifest` if it contains no secrets—usually it *does*, so keep the manifest **gitignored** or use a **`.env.manifest.example`** without real values).
@@ -81,6 +81,6 @@ Sometimes two env vars exist for historical reasons. If both client and server c
81
81
 
82
82
  | Goal | Approach |
83
83
  |------|-----------|
84
- | Same value, many names **locally** | `env.manifest` + `expand-env-manifest.mjs` → `.env.local` |
84
+ | Same value, many names **locally** | `env.manifest` + `runctl env expand` → `.env.local` |
85
85
  | Match Vercel → laptop | `vercel env pull` + merge with generated `.env.local` |
86
86
  | Same value, many names **on Vercel** | Duplicate in UI, or scripted `vercel env add`, or a secrets manager with Vercel sync |
@@ -1,82 +1,91 @@
1
1
  #!/usr/bin/env bash
2
- # Optional shell entrypoint most projects only need package.json scripts.
3
- # Prerequisite: pnpm add -D @zendero/runctl (Node >= 18)
4
- #
5
- # When to use this vs the runctl CLI:
6
- # - Prefer `runctl start` / package.json scripts (see README) for normal dev.
7
- # - Source lib/run-lib.sh (this file) for advanced shell integration: custom
8
- # wrappers, CI, or repos that already drive everything through ./run.sh.
9
- # Shipped path: package `lib/run-lib.sh` (see `runctl lib-path`).
10
-
2
+ # Thin project runner: sets RUNCTL_PROJECT_ROOT + PM/PORT, then delegates to bin/runctl.
3
+ # Maintainer npm flows: scripts/maintain.sh
11
4
  set -euo pipefail
12
5
 
13
- ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
- cd "$ROOT"
6
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
7
+ export RUNCTL_PROJECT_ROOT="$ROOT"
8
+ export RUNCTL_PKG_ROOT="$ROOT"
9
+ export RUN_SH_TASK="${RUN_SH_TASK:-}"
10
+
11
+ PM="${PM:-pnpm}"
12
+ export PM
13
+ RUNCTL_DEV_SCRIPT="${RUNCTL_DEV_SCRIPT:-dev}"
14
+ PORT="${PORT:-3000}"
15
15
 
16
- if [[ -z "${RUN_LIB:-}" ]]; then
17
- _rp="${RUNCTL_PACKAGE:-@zendero/runctl}"
18
- RUN_LIB="$(
19
- cd "$ROOT" && RUNCTL_PACKAGE="$_rp" node -e "
20
- const p = require('path');
21
- const n = process.env.RUNCTL_PACKAGE;
22
- console.log(p.join(p.dirname(require.resolve(n + '/package.json')), 'lib', 'run-lib.sh'));
23
- "
24
- )" || {
25
- echo "run.sh: add runctl — pnpm add -D ${_rp}" >&2
26
- exit 1
27
- }
28
- fi
29
- [[ -f "$RUN_LIB" ]] || {
30
- echo "run.sh: RUN_LIB missing: $RUN_LIB" >&2
16
+ die() {
17
+ echo "run.sh${RUN_SH_TASK:+ ($RUN_SH_TASK)}: $*" >&2
31
18
  exit 1
32
19
  }
33
- # shellcheck source=/dev/null
34
- source "$RUN_LIB"
35
- run_project_init "$ROOT"
20
+
21
+ runctl_cmd() {
22
+ if [[ -x "$ROOT/bin/runctl" ]]; then
23
+ exec "$ROOT/bin/runctl" "$@"
24
+ elif [[ -x "$ROOT/node_modules/.bin/runctl" ]]; then
25
+ exec "$ROOT/node_modules/.bin/runctl" "$@"
26
+ elif command -v pnpm >/dev/null 2>&1 && pnpm exec runctl version >/dev/null 2>&1; then
27
+ exec pnpm exec runctl "$@"
28
+ elif command -v runctl >/dev/null 2>&1; then
29
+ exec runctl "$@"
30
+ else
31
+ die "runctl not found — install deps or add @zendero/runctl"
32
+ fi
33
+ }
36
34
 
37
35
  usage() {
38
36
  cat <<'EOF'
39
- Usage: ./run.sh <command>
37
+ Usage: ./run.sh <command> [args]
40
38
 
41
- dev [name] [auto|PORT] [-- extra args]
42
- stop Stop daemons + release port claims
43
- status Local .run state + ports.env
44
- install pnpm install
39
+ install | run | exec | test → runctl (same as: runctl install|run|exec|test)
40
+ dev | start → runctl start this repo (--script RUNCTL_DEV_SCRIPT)
41
+ open | stop | status | doctor | ports | ps | logs | env | update | …
42
+ env-expand | lib-path
43
+ release-check | publish | release | promote | npm-whoami → scripts/maintain.sh
45
44
 
46
- Examples:
47
- ./run.sh dev
48
- ./run.sh dev web auto -- --turbo
45
+ Environment:
46
+ RUNCTL_PROJECT_ROOT Set automatically to this script’s directory
47
+ RUNCTL_DEV_SCRIPT npm script for dev/start (default: dev)
48
+ PORT Passed to dev/start
49
+ PM package manager (default: pnpm)
49
50
  EOF
51
+ echo ""
52
+ runctl_cmd help
50
53
  }
51
54
 
52
- cmd="${1:-dev}"
53
- shift || true
55
+ main() {
56
+ [[ $# -eq 0 ]] && set -- help
57
+ local cmd="${1:-}"
58
+ shift || true
59
+
60
+ case "$cmd" in
61
+ install | run | exec | test)
62
+ RUN_SH_TASK="$cmd"
63
+ export RUN_SH_TASK
64
+ runctl_cmd "$cmd" "$@"
65
+ ;;
66
+ dev | start)
67
+ export PORT
68
+ runctl_cmd start "$ROOT" --script "$RUNCTL_DEV_SCRIPT" "$@"
69
+ ;;
70
+ release-check | publish | release | promote | npm-whoami)
71
+ exec "$ROOT/scripts/maintain.sh" "$cmd" "$@"
72
+ ;;
73
+ ports-gc | gc)
74
+ runctl_cmd ports gc "$@"
75
+ ;;
76
+ lib-path)
77
+ runctl_cmd lib-path
78
+ ;;
79
+ env-expand | expand-env)
80
+ runctl_cmd env expand "$@"
81
+ ;;
82
+ help | -h | --help)
83
+ usage
84
+ ;;
85
+ *)
86
+ runctl_cmd "$cmd" "$@"
87
+ ;;
88
+ esac
89
+ }
54
90
 
55
- case "$cmd" in
56
- dev)
57
- run_with_lock run_start_package_dev "$@"
58
- ;;
59
- stop)
60
- run_stop_all
61
- ;;
62
- status)
63
- run_local_status
64
- ;;
65
- install)
66
- if command -v pnpm >/dev/null 2>&1; then
67
- (cd "$ROOT" && pnpm install)
68
- elif command -v npm >/dev/null 2>&1; then
69
- (cd "$ROOT" && npm install)
70
- else
71
- echo "Install pnpm or npm" >&2
72
- exit 1
73
- fi
74
- ;;
75
- help | -h | --help)
76
- usage
77
- ;;
78
- *)
79
- usage
80
- exit 1
81
- ;;
82
- esac
91
+ main "$@"
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # Optional repo-root env for npm auth — sourced by bin/runctl and scripts/maintain.sh.
3
+ # Resolves root as: $1, else RUNCTL_PROJECT_ROOT, else PWD.
4
+ # Idempotent per shell (RUNCTL_REPO_ENV_LOADED).
5
+
6
+ runctl_load_repo_env() {
7
+ [[ -n "${RUNCTL_REPO_ENV_LOADED:-}" ]] && return 0
8
+ local root="${1:-${RUNCTL_PROJECT_ROOT:-$PWD}}"
9
+
10
+ if [[ -z "${NPM_TOKEN:-}" ]] && [[ -f "$root/.env" ]]; then
11
+ local line key val
12
+ while IFS= read -r line || [[ -n "$line" ]]; do
13
+ line="${line#"${line%%[![:space:]]*}"}"
14
+ line="${line%"${line##*[![:space:]]}"}"
15
+ [[ -z "$line" || "$line" == \#* ]] && continue
16
+ if [[ "$line" == export\ * ]]; then
17
+ line="${line#export }"
18
+ fi
19
+ [[ "$line" == *"="* ]] || continue
20
+ key="${line%%=*}"
21
+ val="${line#*=}"
22
+ if [[ "$key" != "NPM_TOKEN" && "$key" != "npm_token" ]]; then
23
+ continue
24
+ fi
25
+ val="${val%$'\r'}"
26
+ if [[ "$val" == \"*\" ]]; then
27
+ val="${val#\"}"
28
+ val="${val%\"}"
29
+ elif [[ "$val" == \'*\' ]]; then
30
+ val="${val#\'}"
31
+ val="${val%\'}"
32
+ fi
33
+ export NPM_TOKEN="$val"
34
+ break
35
+ done <"$root/.env"
36
+ fi
37
+
38
+ if [[ -f "$root/.env" ]]; then
39
+ set -a
40
+ # shellcheck source=/dev/null
41
+ source "$root/.env"
42
+ set +a
43
+ fi
44
+
45
+ if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then
46
+ if [[ -n "${NPM_TOKEN:-}" ]]; then
47
+ export NODE_AUTH_TOKEN="$NPM_TOKEN"
48
+ elif [[ -n "${npm_token:-}" ]]; then
49
+ export NODE_AUTH_TOKEN="$npm_token"
50
+ fi
51
+ fi
52
+
53
+ export RUNCTL_REPO_ENV_LOADED=1
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zendero/runctl",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Picks a free port, runs your dev server in the background, and keeps PID + port state in .run/ so projects don't collide.",
5
5
  "author": "DoctorKhan",
6
6
  "homepage": "https://github.com/DoctorKhan/runctl#readme",
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "files": [
31
31
  "bin",
32
+ "run.sh",
32
33
  "lib",
33
34
  "scripts",
34
35
  "examples",
@@ -42,7 +43,7 @@
42
43
  "release-check": "bash ./run.sh release-check",
43
44
  "promote": "bash ./run.sh promote",
44
45
  "release": "bash ./run.sh release latest",
45
- "env:expand": "node ./scripts/expand-env-manifest.mjs",
46
+ "env:expand": "bash ./run.sh env-expand",
46
47
  "link-global": "pnpm link --global"
47
48
  }
48
49
  }
package/run.sh ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # Thin project runner: sets RUNCTL_PROJECT_ROOT + PM/PORT, then delegates to bin/runctl.
3
+ # Maintainer npm flows: scripts/maintain.sh
4
+ set -euo pipefail
5
+
6
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
7
+ export RUNCTL_PROJECT_ROOT="$ROOT"
8
+ export RUNCTL_PKG_ROOT="$ROOT"
9
+ export RUN_SH_TASK="${RUN_SH_TASK:-}"
10
+
11
+ PM="${PM:-pnpm}"
12
+ export PM
13
+ RUNCTL_DEV_SCRIPT="${RUNCTL_DEV_SCRIPT:-dev}"
14
+ PORT="${PORT:-3000}"
15
+
16
+ die() {
17
+ echo "run.sh${RUN_SH_TASK:+ ($RUN_SH_TASK)}: $*" >&2
18
+ exit 1
19
+ }
20
+
21
+ runctl_cmd() {
22
+ if [[ -x "$ROOT/bin/runctl" ]]; then
23
+ exec "$ROOT/bin/runctl" "$@"
24
+ elif [[ -x "$ROOT/node_modules/.bin/runctl" ]]; then
25
+ exec "$ROOT/node_modules/.bin/runctl" "$@"
26
+ elif command -v pnpm >/dev/null 2>&1 && pnpm exec runctl version >/dev/null 2>&1; then
27
+ exec pnpm exec runctl "$@"
28
+ elif command -v runctl >/dev/null 2>&1; then
29
+ exec runctl "$@"
30
+ else
31
+ die "runctl not found — install deps or add @zendero/runctl"
32
+ fi
33
+ }
34
+
35
+ usage() {
36
+ cat <<'EOF'
37
+ Usage: ./run.sh <command> [args]
38
+
39
+ install | run | exec | test → runctl (same as: runctl install|run|exec|test)
40
+ dev | start → runctl start this repo (--script RUNCTL_DEV_SCRIPT)
41
+ open | stop | status | doctor | ports | ps | logs | env | update | …
42
+ env-expand | lib-path
43
+ release-check | publish | release | promote | npm-whoami → scripts/maintain.sh
44
+
45
+ Environment:
46
+ RUNCTL_PROJECT_ROOT Set automatically to this script’s directory
47
+ RUNCTL_DEV_SCRIPT npm script for dev/start (default: dev)
48
+ PORT Passed to dev/start
49
+ PM package manager (default: pnpm)
50
+ EOF
51
+ echo ""
52
+ runctl_cmd help
53
+ }
54
+
55
+ main() {
56
+ [[ $# -eq 0 ]] && set -- help
57
+ local cmd="${1:-}"
58
+ shift || true
59
+
60
+ case "$cmd" in
61
+ install | run | exec | test)
62
+ RUN_SH_TASK="$cmd"
63
+ export RUN_SH_TASK
64
+ runctl_cmd "$cmd" "$@"
65
+ ;;
66
+ dev | start)
67
+ export PORT
68
+ runctl_cmd start "$ROOT" --script "$RUNCTL_DEV_SCRIPT" "$@"
69
+ ;;
70
+ release-check | publish | release | promote | npm-whoami)
71
+ exec "$ROOT/scripts/maintain.sh" "$cmd" "$@"
72
+ ;;
73
+ ports-gc | gc)
74
+ runctl_cmd ports gc "$@"
75
+ ;;
76
+ lib-path)
77
+ runctl_cmd lib-path
78
+ ;;
79
+ env-expand | expand-env)
80
+ runctl_cmd env expand "$@"
81
+ ;;
82
+ help | -h | --help)
83
+ usage
84
+ ;;
85
+ *)
86
+ runctl_cmd "$cmd" "$@"
87
+ ;;
88
+ esac
89
+ }
90
+
91
+ main "$@"
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env bash
2
+ # Maintainer-only: npm publish, release, promote, release-check, whoami.
3
+ # Invoked via ./run.sh <command> or directly: scripts/maintain.sh <command>
4
+ set -euo pipefail
5
+
6
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
7
+
8
+ die() {
9
+ echo "maintain: $*" >&2
10
+ exit 1
11
+ }
12
+
13
+ export RUNCTL_PROJECT_ROOT="$ROOT"
14
+ # shellcheck source=../lib/repo-env.sh
15
+ source "$ROOT/lib/repo-env.sh"
16
+ runctl_load_repo_env "$ROOT"
17
+
18
+ in_repo() {
19
+ (cd "$ROOT" && "$@")
20
+ }
21
+
22
+ npm_publish_token() {
23
+ printf '%s' "${NPM_TOKEN:-${NODE_AUTH_TOKEN:-${npm_token:-}}}"
24
+ }
25
+
26
+ runctl_npm_userconfig_from_token() {
27
+ local tok="$1"
28
+ local f
29
+ f="$(mktemp "${TMPDIR:-/tmp}/runctl-npm-auth.XXXXXX")" || return 1
30
+ chmod 600 "$f" || true
31
+ printf '//registry.npmjs.org/:_authToken=%s\n' "$tok" >"$f" || {
32
+ rm -f "$f"
33
+ return 1
34
+ }
35
+ printf '%s' "$f"
36
+ }
37
+
38
+ validate_dist_tag() {
39
+ case "${1:-latest}" in
40
+ latest | next) return 0 ;;
41
+ *)
42
+ echo "maintain publish: unsupported dist-tag '$1'. Use: latest or next." >&2
43
+ return 1
44
+ ;;
45
+ esac
46
+ }
47
+
48
+ release_commit_and_push_if_needed() {
49
+ local dist_tag="$1"
50
+ local pkg_name="$2"
51
+ local pkg_version="$3"
52
+ if ! command -v git >/dev/null 2>&1; then
53
+ echo "maintain release: git not found; skipping commit/push step" >&2
54
+ return 0
55
+ fi
56
+ if ! git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
57
+ echo "maintain release: not in a git worktree; skipping commit/push step" >&2
58
+ return 0
59
+ fi
60
+ if [[ -n "$(git -C "$ROOT" status --porcelain)" ]]; then
61
+ local branch
62
+ branch="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
63
+ [[ -n "$branch" && "$branch" != "HEAD" ]] || die "release commit/push requires a branch checkout (not detached HEAD)"
64
+ git -C "$ROOT" add -A
65
+ git -C "$ROOT" commit -m "release: publish ${pkg_name}@${pkg_version} (${dist_tag})"
66
+ git -C "$ROOT" push -u origin "$branch"
67
+ echo "maintain release: committed and pushed release changes on $branch"
68
+ else
69
+ echo "maintain release: no local git changes; skipping commit/push"
70
+ fi
71
+ }
72
+
73
+ package_field() {
74
+ local field="$1"
75
+ node -e '
76
+ const fs = require("fs");
77
+ const path = require("path");
78
+ const f = path.join(process.argv[1], "package.json");
79
+ const pkg = JSON.parse(fs.readFileSync(f, "utf8"));
80
+ const key = process.argv[2];
81
+ if (!pkg[key]) process.exit(1);
82
+ process.stdout.write(String(pkg[key]));
83
+ ' "$ROOT" "$field"
84
+ }
85
+
86
+ publish_failed_hint() {
87
+ local op="$1"
88
+ echo "maintain ${op}: publish failed." >&2
89
+ echo " If ./run.sh npm-whoami works: check scoped write / maintainer on npm." >&2
90
+ echo " Run ./run.sh release-check before publish." >&2
91
+ }
92
+
93
+ maint_release_check() {
94
+ echo "maintain release-check: runctl doctor..."
95
+ "$ROOT/bin/runctl" doctor "$ROOT" || die "doctor failed"
96
+ echo "maintain release-check: pack --dry-run..."
97
+ if command -v pnpm >/dev/null 2>&1; then
98
+ in_repo pnpm pack --dry-run >/dev/null || die "pnpm pack --dry-run failed"
99
+ elif command -v npm >/dev/null 2>&1; then
100
+ in_repo npm pack --dry-run >/dev/null || die "npm pack --dry-run failed"
101
+ else
102
+ die "install pnpm or npm for release-check"
103
+ fi
104
+ local tok _rc _ec
105
+ tok="$(npm_publish_token)"
106
+ if [[ -n "$tok" ]]; then
107
+ echo "maintain release-check: npm whoami..."
108
+ _rc="$(runctl_npm_userconfig_from_token "$tok")" || die "could not write temp npmrc"
109
+ NPM_CONFIG_USERCONFIG="$_rc" npm whoami || {
110
+ _ec=$?
111
+ rm -f "$_rc"
112
+ die "npm whoami failed (exit $_ec)"
113
+ }
114
+ rm -f "$_rc"
115
+ else
116
+ echo "maintain release-check: skip npm whoami (no NPM_TOKEN / npm_token in .env)" >&2
117
+ fi
118
+ echo "maintain release-check: ok"
119
+ }
120
+
121
+ maint_publish_or_release() {
122
+ local cmd="$1"
123
+ shift
124
+ if [[ "${1:-}" == "all" ]]; then
125
+ shift
126
+ fi
127
+ local dist_tag="${1:-latest}"
128
+ validate_dist_tag "$dist_tag" || exit 1
129
+ if [[ $# -gt 0 ]]; then
130
+ shift
131
+ fi
132
+ local token pkg_name pkg_version remote_version
133
+ token="$(npm_publish_token)"
134
+ [[ -n "$token" ]] || die "missing npm token — set NPM_TOKEN in .env (or NODE_AUTH_TOKEN / npm_token)"
135
+ export NODE_AUTH_TOKEN="$token"
136
+ export PUBLISH_OK=1
137
+ pkg_name="$(package_field name)"
138
+ pkg_version="$(package_field version)"
139
+ remote_version="$(npm view "${pkg_name}@${pkg_version}" version 2>/dev/null || true)"
140
+
141
+ local is_dry_run=0 arg
142
+ for arg in "$@"; do
143
+ if [[ "$arg" == "--dry-run" ]]; then
144
+ is_dry_run=1
145
+ break
146
+ fi
147
+ done
148
+
149
+ if [[ -n "$remote_version" && "$is_dry_run" -eq 0 ]]; then
150
+ echo "maintain ${cmd}: ${pkg_name}@${pkg_version} already exists; bumping patch version..."
151
+ if command -v pnpm >/dev/null 2>&1; then
152
+ in_repo pnpm version patch --no-git-tag-version || die "pnpm version patch failed"
153
+ else
154
+ in_repo npm version patch --no-git-tag-version || die "npm version patch failed"
155
+ fi
156
+ pkg_version="$(package_field version)"
157
+ echo "maintain ${cmd}: publishing ${pkg_name}@${pkg_version} with dist-tag '${dist_tag}'"
158
+ elif [[ -n "$remote_version" ]]; then
159
+ echo "maintain ${cmd}: ${pkg_name}@${pkg_version} already exists; not bumping during --dry-run"
160
+ fi
161
+
162
+ local npmrc _pub_ok=0
163
+ npmrc="$(runctl_npm_userconfig_from_token "$token")" || exit 1
164
+
165
+ if command -v pnpm >/dev/null 2>&1; then
166
+ (cd "$ROOT" && NPM_CONFIG_USERCONFIG="$npmrc" pnpm publish --access public --tag "$dist_tag" --no-git-checks "$@") && _pub_ok=1
167
+ elif command -v npm >/dev/null 2>&1; then
168
+ (cd "$ROOT" && NPM_CONFIG_USERCONFIG="$npmrc" npm publish --access public --tag "$dist_tag" --no-git-checks "$@") && _pub_ok=1
169
+ else
170
+ rm -f "$npmrc"
171
+ die "install pnpm (preferred) or npm"
172
+ fi
173
+ rm -f "$npmrc"
174
+ if [[ "$_pub_ok" -ne 1 ]]; then
175
+ publish_failed_hint "$cmd"
176
+ exit 1
177
+ fi
178
+ if [[ "$cmd" == "release" && "$is_dry_run" -eq 0 ]]; then
179
+ release_commit_and_push_if_needed "$dist_tag" "$pkg_name" "$pkg_version"
180
+ fi
181
+ }
182
+
183
+ maint_promote() {
184
+ local tok npmrc name ver
185
+ tok="$(npm_publish_token)"
186
+ [[ -n "$tok" ]] || die "set NPM_TOKEN (or npm_token) in .env for promote"
187
+ name="$(package_field name)"
188
+ ver="$(package_field version)"
189
+ npmrc="$(runctl_npm_userconfig_from_token "$tok")" || exit 1
190
+ echo "maintain promote: npm dist-tag add ${name}@${ver} latest"
191
+ if NPM_CONFIG_USERCONFIG="$npmrc" npm dist-tag add "${name}@${ver}" latest; then
192
+ rm -f "$npmrc"
193
+ echo "maintain promote: ok"
194
+ else
195
+ rm -f "$npmrc"
196
+ die "dist-tag add failed (is ${name}@${ver} published?)"
197
+ fi
198
+ }
199
+
200
+ maint_npm_whoami() {
201
+ local _tok _rc _ec
202
+ command -v npm >/dev/null 2>&1 || die "npm not found"
203
+ _tok="$(npm_publish_token)"
204
+ [[ -n "$_tok" ]] || die "set NPM_TOKEN or NODE_AUTH_TOKEN or npm_token in .env"
205
+ _rc="$(runctl_npm_userconfig_from_token "$_tok")" || exit 1
206
+ NPM_CONFIG_USERCONFIG="$_rc" npm whoami
207
+ _ec=$?
208
+ rm -f "$_rc"
209
+ exit "$_ec"
210
+ }
211
+
212
+ maint_usage() {
213
+ cat <<'EOF'
214
+ Maintainer commands (scripts/maintain.sh):
215
+
216
+ release-check Preflight: doctor + pack --dry-run + optional npm whoami
217
+ publish [all] [tag] Publish package to npm
218
+ release [all] [tag] Publish, then commit+push if needed
219
+ promote npm dist-tag add <pkg>@<version> latest
220
+ npm-whoami npm whoami using token from .env
221
+
222
+ Also available via: ./run.sh <same command>
223
+ EOF
224
+ }
225
+
226
+ main() {
227
+ [[ $# -eq 0 ]] && set -- help
228
+ local cmd="${1:-}"
229
+ shift || true
230
+ case "$cmd" in
231
+ release-check)
232
+ maint_release_check
233
+ ;;
234
+ publish | release)
235
+ maint_publish_or_release "$cmd" "$@"
236
+ ;;
237
+ promote)
238
+ maint_promote
239
+ ;;
240
+ npm-whoami)
241
+ maint_npm_whoami
242
+ ;;
243
+ help | -h | --help)
244
+ maint_usage
245
+ ;;
246
+ *)
247
+ echo "maintain: unknown command: $cmd" >&2
248
+ echo "" >&2
249
+ maint_usage >&2
250
+ exit 1
251
+ ;;
252
+ esac
253
+ }
254
+
255
+ main "$@"