@zendero/runctl 0.1.8 → 0.1.10

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
@@ -102,6 +102,14 @@ Use npm explicitly (e.g. no pnpm on the machine):
102
102
  curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash -s -- --pm npm --registry
103
103
  ```
104
104
 
105
+ If `runctl --help` still looks old after install/update, remove the legacy package that can shadow this CLI and reinstall:
106
+
107
+ ```bash
108
+ pnpm remove -g runctl
109
+ pnpm add -g @zendero/runctl@latest
110
+ hash -r
111
+ ```
112
+
105
113
  `--help` on the script prints the same usage summary.
106
114
 
107
115
  ---
@@ -127,23 +135,37 @@ Add scripts to your `package.json`:
127
135
 
128
136
  **`predev`:** If you define `predev` next to `dev` (e.g. a doctor step) and your script name is `dev:*` or `dev_*` without its own `pre<script>`, runctl runs `predev` once before starting. Set `RUNCTL_SKIP_PREDEV=1` to skip.
129
137
 
138
+ **Dashboard / API-only (split names):** Avoid `runctl start --script dev` when `dev` is the runctl wrapper — that loops. Use a dedicated script for the real server:
139
+
140
+ ```json
141
+ {
142
+ "scripts": {
143
+ "dev": "runctl start --script dev:workbench",
144
+ "dev:workbench": "node --env-file=.env src/dashboard/server.js",
145
+ "dev:stop": "runctl stop"
146
+ }
147
+ }
148
+ ```
149
+
150
+ Listen on `process.env.PORT` (runctl sets it). Optional: `runctl start --script dev:workbench --open` to open the browser after start, or `runctl start … && runctl open .`.
151
+
130
152
  ---
131
153
 
132
154
  ## Commands
133
155
 
134
156
  | Command | What it does |
135
157
  |---------|-------------|
136
- | `runctl start` \| `runctl dev` | Start dev server (same command; picks free port, backgrounds) |
158
+ | `runctl start` \| `runctl dev` | Start dev server (same command; picks free port, backgrounds). Flags: `--script`, `--open` (open browser after a successful start) |
137
159
  | `runctl stop [dir]` | Stop daemons & release ports |
138
160
  | `runctl status [dir]` | Show `.run` state for this package |
139
161
  | `runctl ps` | List running programs with PID, port, service, project |
140
- | `runctl logs [dir] [service]` | Tail `.run/logs/<service>.log` (default service: `web`) |
162
+ | `runctl logs [dir] [service]` | Tail `.run/logs/<service>.log` (default service: **`RUNCTL_SERVICE`**, else `package.json` `name` basename, else `web`) |
141
163
  | `runctl ports` | List user-wide port registry (`~/.run`) |
142
164
  | `runctl ports gc` | Clean up stale port claims |
143
165
  | `runctl env expand <manifest> [--out file]` | Generate `.env.local` from manifest |
144
- | `runctl doctor [dir]` | Check Node 18+, `lsof`, package manager, `package.json` |
166
+ | `runctl doctor [dir]` | Check Node 18+, `lsof`, package manager, `package.json`; reminds that child scripts get **`PORT`** / **`HOST`** (custom servers should `listen` on `process.env.PORT`) |
145
167
  | `runctl update` | Refresh global CLI: default **`auto`** (npm `@latest`, then Git). **`runctl update npm`** / **`git`** / **`auto`** or flags **`--registry`** / **`--git`** / **`--auto`**; **`runctl update --help`**; env `RUNCTL_PACKAGE`, `RUNCTL_GIT_BASE`, `RUNCTL_GIT_REF` (aligned with [`install-global.sh`](scripts/install-global.sh)) |
146
- | `runctl version` | Print package version and install path |
168
+ | `runctl version` \| `runctl --version` \| `runctl -v` | Print package version and install path (supported interchangeably) |
147
169
 
148
170
  **Monorepo:** `runctl start ./apps/web --script dev:server`
149
171
 
@@ -160,7 +182,7 @@ Add scripts to your `package.json`:
160
182
  | **`predev`** + split `dev` / `dev:server` | **Supported** — see above. |
161
183
  | Monorepo app in a subfolder | Use `runctl start ./apps/web`. |
162
184
  | **No `package.json`** (Python, Go, etc.) | **Not a fit** — this tool is for Node package scripts. |
163
- | Custom Node entry (gateways, CLIs) | **Weak fit** — `PORT`/`HOST` are set, but no framework CLI flags. |
185
+ | Custom Node entry (gateways, CLIs) | **Weak fit** — `PORT`/`HOST` are injected; bind with `server.listen(process.env.PORT)` (see `runctl doctor`). |
164
186
 
165
187
  ---
166
188
 
@@ -168,7 +190,13 @@ Add scripts to your `package.json`:
168
190
 
169
191
  [`examples/consumer-package.json`](examples/consumer-package.json) · [`docs/vercel-and-env.md`](docs/vercel-and-env.md) · [`examples/env.manifest.example`](examples/env.manifest.example)
170
192
 
171
- **Develop this repo:** `pnpm install` `./run.sh` (default **doctor**, like `elata-bio-sdk/run.sh`) `./run.sh ports`
193
+ **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`**.
194
+
195
+ **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).
196
+
197
+ **Roadmap (ideas):** `runctl exec` (one-off commands with the same port / `.run` contract as `start`); optional HTTP health gate before “ready”.
198
+
199
+ **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))
172
200
 
173
201
  **Publish (maintainers)** — workflow similar to elata’s release preflight, scaled for one package:
174
202
 
package/bin/runctl CHANGED
@@ -16,12 +16,12 @@ ${_b}runctl${_r} — dev-server orchestrator & port manager
16
16
  ${_y}Usage:${_r} runctl <command> [options]
17
17
 
18
18
  ${_y}Commands${_r}
19
- ${_c}start${_r} | ${_c}dev${_r} [dir] [--script name] Start dev server ${_d}(alias: dev = start)${_r}
19
+ ${_c}start${_r} | ${_c}dev${_r} [dir] [--script name] [--open] Start dev server ${_d}(alias: dev = start; ${_c}--open${_r} = browser after start)${_r}
20
20
  ${_c}open${_r} [dir] Open running dev server in browser
21
21
  ${_c}stop${_r} [dir] Stop daemons & release ports
22
22
  ${_c}status${_r} [dir] Show running state
23
23
  ${_c}ps${_r} List running programs with ports/projects
24
- ${_c}logs${_r} [dir] [service] Tail .run/logs/<service>.log ${_d}(default service: web)${_r}
24
+ ${_c}logs${_r} [dir] [service] Tail .run/logs/<service>.log ${_d}(default: ${_c}RUNCTL_SERVICE${_r} or package name, else web)${_r}
25
25
  ${_c}ports${_r} List user-wide port registry (~/.run)
26
26
  ${_c}ports gc${_r} Clean up stale port claims
27
27
  ${_c}env expand${_r} <manifest> [opts] Generate .env.local from manifest
@@ -30,6 +30,8 @@ ${_y}Commands${_r}
30
30
 
31
31
  ${_y}Options${_r}
32
32
  ${_c}--script${_r} <name> Package script to run ${_d}(default: dev)${_r}
33
+ ${_c}--open${_r} After a successful start, open the app URL ${_d}(same as ${_c}runctl open${_r})${_r}
34
+ ${_c}version${_r} | ${_c}--version${_r} | ${_c}-v${_r} Print version and install path
33
35
  ${_c}--help${_r}, ${_c}-h${_r} Show this help
34
36
 
35
37
  ${_d}Quick start — add to package.json:
@@ -48,12 +50,13 @@ _resolve_proj() {
48
50
  }
49
51
 
50
52
  cmd_start() {
51
- local proj script="" args=()
53
+ local proj script="" args=() open_after=0
52
54
  proj="$(pwd)"
53
55
 
54
56
  while [[ $# -gt 0 ]]; do
55
57
  case "$1" in
56
58
  --script) script="${2:-}"; shift 2 ;;
59
+ --open) open_after=1; shift ;;
57
60
  --) shift; args+=("$@"); break ;;
58
61
  -h | --help) usage; exit 0 ;;
59
62
  -*)
@@ -77,7 +80,12 @@ cmd_start() {
77
80
  # shellcheck source=../lib/run-lib.sh
78
81
  source "$RUNCTL_PKG_ROOT/lib/run-lib.sh"
79
82
  run_project_init "$proj"
80
- run_with_lock run_start_package_dev "${args[@]+"${args[@]}"}"
83
+ if ! run_with_lock run_start_package_dev "${args[@]+"${args[@]}"}"; then
84
+ return 1
85
+ fi
86
+ if [[ "$open_after" -eq 1 ]]; then
87
+ cmd_open "$proj"
88
+ fi
81
89
  }
82
90
 
83
91
  cmd_open() {
@@ -330,6 +338,19 @@ cmd_update() {
330
338
  pm="$(cmd_update_pick_pm "$PM")" || exit 1
331
339
  git_target="${GIT_BASE}#${GIT_REF}"
332
340
 
341
+ _remove_conflicting_global_runctl() {
342
+ # A legacy package named "runctl" can own the same binary name and shadow @zendero/runctl.
343
+ # Best-effort remove before reinstalling the intended package.
344
+ if [[ "$PKG" != "@zendero/runctl" ]]; then
345
+ return 0
346
+ fi
347
+ if [[ "$pm" == "pnpm" ]]; then
348
+ pnpm remove -g runctl >/dev/null 2>&1 || true
349
+ else
350
+ npm uninstall -g runctl >/dev/null 2>&1 || true
351
+ fi
352
+ }
353
+
333
354
  _git_base_for_ls_remote() {
334
355
  local base="$1"
335
356
  printf '%s' "${base#git+}"
@@ -349,6 +370,7 @@ cmd_update() {
349
370
  _update_registry() {
350
371
  local spec="${PKG}@latest"
351
372
  echo "runctl update: installing latest from registry ($spec)..."
373
+ _remove_conflicting_global_runctl
352
374
  if [[ "$pm" == "pnpm" ]]; then
353
375
  pnpm add -g --force "$spec"
354
376
  else
@@ -366,6 +388,7 @@ cmd_update() {
366
388
  echo "runctl update: installing from Git ($git_target)..."
367
389
  echo "runctl update: could not resolve ref to SHA; proceeding with ref directly." >&2
368
390
  fi
391
+ _remove_conflicting_global_runctl
369
392
  if [[ "$pm" == "pnpm" ]]; then
370
393
  pnpm add -g --force "$spec"
371
394
  else
@@ -427,6 +450,7 @@ cmd_doctor() {
427
450
  printf ' %-12s %s\n' "package.json:" "not found in $proj" >&2
428
451
  ec=1
429
452
  fi
453
+ printf ' %-12s %s\n' "child env:" "PORT and HOST are set for package scripts; custom Node servers should listen on process.env.PORT (and bind HOST if needed)."
430
454
  return "$ec"
431
455
  }
432
456
 
@@ -441,7 +465,8 @@ cmd_logs() {
441
465
  shift 2
442
466
  ;;
443
467
  -h | --help)
444
- echo "Usage: runctl logs [dir] [service] (default service: web)" >&2
468
+ echo "Usage: runctl logs [dir] [service]" >&2
469
+ echo " Default service: RUNCTL_SERVICE, else package.json name, else web." >&2
445
470
  echo " runctl logs [--lines N]" >&2
446
471
  exit 0
447
472
  ;;
@@ -455,10 +480,13 @@ cmd_logs() {
455
480
  proj="$(cd "${pos[0]}" && pwd)"
456
481
  pos=("${pos[@]:1}")
457
482
  fi
458
- local svc="${pos[0]:-web}"
483
+ local svc="${pos[0]:-}"
459
484
  # shellcheck source=../lib/run-lib.sh
460
485
  source "$RUNCTL_PKG_ROOT/lib/run-lib.sh"
461
486
  run_project_init "$proj"
487
+ if [[ -z "$svc" ]]; then
488
+ svc="$(run_default_service_name)"
489
+ fi
462
490
  local logf="$RUN_LOCAL_STATE/logs/${svc}.log"
463
491
  if [[ ! -f "$logf" ]]; then
464
492
  echo "runctl logs: no file at $logf" >&2
@@ -500,7 +528,7 @@ main() {
500
528
 
501
529
  # Plumbing
502
530
  lib-path) printf '%s\n' "$RUNCTL_PKG_ROOT/lib/run-lib.sh" ;;
503
- version | -v) cmd_version_print ;;
531
+ version | -v | --version) cmd_version_print ;;
504
532
  help | -h | --help) usage ;;
505
533
 
506
534
  # Hidden backward-compat aliases
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env bash
2
2
  # Optional shell entrypoint — most projects only need package.json scripts.
3
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`).
4
10
 
5
11
  set -euo pipefail
6
12
 
package/lib/run-lib.sh CHANGED
@@ -2,6 +2,7 @@
2
2
  # run-lib.sh — npm package `@zendero/runctl`: project .run/ + user ~/.run registry.
3
3
  # Requires bash. Port/dev automation requires Node.js >= 18 (see package.json engines).
4
4
  # Intentionally no global `set -e` — this file is usually sourced.
5
+ # Consumer shell example: examples/run.sh.example (most projects use the `runctl` CLI instead).
5
6
 
6
7
  if [[ -z "${BASH_VERSION:-}" ]]; then
7
8
  echo "run-lib.sh: must be sourced or executed with bash (not zsh)" >&2
@@ -146,6 +147,50 @@ run_require_node() {
146
147
  }
147
148
  }
148
149
 
150
+ # Basename for .run/logs/<name>.log and RUN_DEV_SERVICE — override with RUNCTL_SERVICE.
151
+ run_sanitize_service_name() {
152
+ local s="${1:-web}"
153
+ s="${s//[^a-zA-Z0-9._-]/-}"
154
+ s="$(printf '%s' "$s" | sed 's/^-*//;s/-*$//')"
155
+ [[ -z "$s" ]] && s="web"
156
+ printf '%s\n' "$s"
157
+ }
158
+
159
+ run_default_service_name() {
160
+ if [[ -n "${RUNCTL_SERVICE:-}" ]]; then
161
+ run_sanitize_service_name "${RUNCTL_SERVICE}"
162
+ return 0
163
+ fi
164
+ [[ -f "$RUN_PROJECT_ROOT/package.json" ]] || {
165
+ printf '%s\n' web
166
+ return 0
167
+ }
168
+ command -v node >/dev/null 2>&1 || {
169
+ printf '%s\n' web
170
+ return 0
171
+ }
172
+ node -e '
173
+ const fs = require("fs");
174
+ const path = require("path");
175
+ const root = process.argv[1];
176
+ let pkg = {};
177
+ try {
178
+ pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
179
+ } catch {
180
+ process.stdout.write("web" + "\n");
181
+ process.exit(0);
182
+ }
183
+ const n = pkg.name;
184
+ if (typeof n !== "string" || !n) {
185
+ process.stdout.write("web" + "\n");
186
+ process.exit(0);
187
+ }
188
+ const base = n.includes("/") ? n.split("/").pop() : n;
189
+ const safe = String(base).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "web";
190
+ process.stdout.write(safe + "\n");
191
+ ' "$RUN_PROJECT_ROOT" 2>/dev/null || printf '%s\n' web
192
+ }
193
+
149
194
  # True if package.json defines scripts.<name> (Node required).
150
195
  run_package_has_script() {
151
196
  local name="$1"
@@ -242,14 +287,16 @@ EOF
242
287
 
243
288
  # Start \`pnpm|yarn|npm run <script>\` in the background with a free port and register it.
244
289
  # Script name defaults to \`dev\`; set RUNCTL_PM_RUN_SCRIPT (e.g. dev:server) when \`dev\` is the runctl wrapper.
245
- # Usage: run_start_package_dev [service_name=web] [base_port|auto=auto] [extra args after script --]
290
+ # Default service name: RUNCTL_SERVICE, else package.json name (basename), else web — see run_default_service_name.
291
+ # Usage: run_start_package_dev [service_name] [base_port|auto=auto] [extra args after script --]
246
292
  run_start_package_dev() {
247
293
  run_require_node || return 1
248
294
  [[ -f "$RUN_PROJECT_ROOT/package.json" ]] || {
249
295
  echo "run_start_package_dev: no package.json in $RUN_PROJECT_ROOT" >&2
250
296
  return 1
251
297
  }
252
- local svc="web"
298
+ local svc
299
+ svc="$(run_default_service_name)"
253
300
  local base_raw="auto"
254
301
  if [[ $# -ge 1 ]]; then svc="$1"; shift; fi
255
302
  if [[ $# -ge 1 ]]; then base_raw="$1"; shift; fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zendero/runctl",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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",
@@ -37,7 +37,7 @@
37
37
  ],
38
38
  "scripts": {
39
39
  "run": "bash ./run.sh",
40
- "test": "bash ./tests/runctl-ps.test.sh",
40
+ "test": "bash ./tests/run-all.sh",
41
41
  "doctor": "bash ./run.sh doctor",
42
42
  "release-check": "bash ./run.sh release-check",
43
43
  "promote": "bash ./run.sh promote",
@@ -132,10 +132,20 @@ run_install() {
132
132
  local source_kind="$2"
133
133
  local target="$3"
134
134
 
135
+ # A legacy package named "runctl" can shadow the same CLI binary.
136
+ # Remove it before installing @zendero/runctl.
137
+ if [[ "$PKG" == "@zendero/runctl" ]]; then
138
+ if [[ "$manager" == "pnpm" ]]; then
139
+ pnpm remove -g runctl >/dev/null 2>&1 || true
140
+ else
141
+ npm uninstall -g runctl >/dev/null 2>&1 || true
142
+ fi
143
+ fi
144
+
135
145
  if [[ "$manager" == "pnpm" ]]; then
136
- pnpm add -g "$target"
146
+ pnpm add -g --force "$target"
137
147
  else
138
- npm install -g "$target"
148
+ npm install -g --force "$target"
139
149
  fi
140
150
 
141
151
  say