@zendero/runctl 0.1.3 → 0.1.5

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
@@ -10,59 +10,100 @@ Picks a **free port**, runs your **dev server in the background**, and keeps **P
10
10
 
11
11
  ## Install
12
12
 
13
- The published package on npm is **`@zendero/runctl`**. The CLI binary on your PATH is still **`runctl`**.
13
+ Published name on npm is **`@zendero/runctl`**; the CLI on your PATH is **`runctl`**.
14
14
 
15
- **From the npm registry (recommended):**
15
+ | Goal | What to run |
16
+ |------|-------------|
17
+ | Use runctl **inside one repo** (recommended) | `pnpm add -D @zendero/runctl` — also `npm install -D` / `yarn add -D` |
18
+ | **`runctl` everywhere** (global) | `pnpm add -g @zendero/runctl` or the curl installer below |
19
+ | Track **main from GitHub** as a dev dependency | `pnpm add -D "github:DoctorKhan/runctl#main"` (still resolves as `@zendero/runctl`; reinstall to update) |
20
+
21
+ ### Global install: package manager vs script
22
+
23
+ **Package manager** is the straightforward choice if you already use pnpm or npm:
16
24
 
17
25
  ```bash
18
- pnpm add -D @zendero/runctl # or npm install -D / yarn add -D
26
+ pnpm add -g @zendero/runctl
19
27
  ```
20
28
 
21
- **Global CLI** (`runctl` on your PATH everywhere):
29
+ From Git only:
22
30
 
23
31
  ```bash
24
- pnpm add -g @zendero/runctl # or npm install -g
32
+ pnpm add -g "github:DoctorKhan/runctl#main"
25
33
  ```
26
34
 
27
- **Global install via curl** uses a single script: [`scripts/install-global.sh`](scripts/install-global.sh)
35
+ **[`scripts/install-global.sh`](scripts/install-global.sh)** is for “one command” setup, **CI**, or when you want **npm first, then Git** without writing two install lines yourself. It requires **bash**, **pnpm or npm** on `PATH`, and network access.
36
+
37
+ One-liner (same URL the script header documents):
28
38
 
29
39
  ```bash
30
- curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" |
31
- bash
40
+ curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash
32
41
  ```
33
42
 
34
- With no arguments, `install-global.sh` prompts on a TTY; otherwise it defaults to registry install with Git fallback. Pass arguments to force a mode:
43
+ Pass script arguments after `bash` (stdin pipe has no argv). To pick a **mode** explicitly:
35
44
 
36
45
  ```bash
37
- curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" |
38
- bash -s -- --registry
46
+ curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash -s -- --registry
39
47
  ```
40
48
 
49
+ ### `install-global.sh` reference
50
+
51
+ If you do **not** pass **`--registry`**, **`--git`**, **`--auto`**, or **`--interactive`**: on an **interactive TTY** with **`CI` not `1`**, the script **prompts** for install source (and related choices). Otherwise it behaves like **`--auto`**: **global install from the npm registry** first; if that fails, **retry from Git** (same URL/ref as `--git`).
52
+
53
+ **Modes** — each mode picks *where* the global install comes from. Under the hood the script runs **`pnpm add -g …`** or **`npm install -g …`** once per successful path (auto can run **twice**: registry attempt, then Git if the first fails).
54
+
55
+ | Mode | What it does | When to use it |
56
+ |------|----------------|----------------|
57
+ | **`--registry`** | **Only** installs `RUNCTL_PACKAGE` (default `@zendero/runctl`) from the npm registry. **No** Git fallback. | You want the published package only—e.g. CI that must not clone Git, or you know npm is enough. |
58
+ | **`--git`** | **Only** installs from Git: `RUNCTL_GIT_BASE` + `#` + ref (default ref `main`, overridable with `--ref`). **No** registry attempt first. | You want `main`/a branch/tag from the repo, or the registry is unreachable. |
59
+ | **`--auto`** | Tries **`--registry`** first; on **failure**, runs the same Git install as **`--git`**. | Headless installs, pipes, CI: resilient default when you’re fine with either source. |
60
+ | **`--interactive`** | Prompts for **registry / git / auto**, optional **Git ref** when git/auto applies, and **pnpm vs npm** if both exist—**only** when a TTY is available. | You want to choose at install time instead of memorizing flags. |
61
+
62
+ If **`--interactive`** is requested but there is **no usable TTY** (or `CI=1`), the script **falls back to `--auto`** and prints a short notice.
63
+
64
+ **Flags**
65
+
66
+ | Flag | Meaning |
67
+ |------|---------|
68
+ | `--pm pnpm` \| `--pm npm` | Use that package manager (must exist on `PATH`) |
69
+ | `--ref <ref>` | Git ref for `--git` or for the Git step of `--auto` (default: `main`) |
70
+
71
+ **Environment variables** (optional)
72
+
73
+ | Variable | Purpose |
74
+ |----------|---------|
75
+ | `RUNCTL_PACKAGE` | npm package name (default: `@zendero/runctl`) |
76
+ | `RUNCTL_GIT_BASE` | Git URL without fragment (default: `git+https://github.com/DoctorKhan/runctl.git`) |
77
+ | `RUNCTL_GIT_REF` | Default ref when not overridden by `--ref` (default: `main`) |
78
+
79
+ **Examples**
80
+
81
+ Registry only (good for locked-down CI that should not hit Git):
82
+
41
83
  ```bash
42
- curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" |
43
- bash -s -- --auto
84
+ curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash -s -- --registry
44
85
  ```
45
86
 
87
+ Explicit auto (same as non-interactive default, but spelled out):
88
+
46
89
  ```bash
47
- curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" |
48
- bash -s -- --git --ref main
90
+ curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash -s -- --auto
49
91
  ```
50
92
 
51
- Optional flags: `--pm pnpm|npm`, `--ref <git-ref>`. Optional env: `RUNCTL_PACKAGE`, `RUNCTL_GIT_BASE`, `RUNCTL_GIT_REF`.
52
-
53
- **Without curl:**
93
+ Git only, specific ref:
54
94
 
55
95
  ```bash
56
- pnpm add -g @zendero/runctl
57
- pnpm add -g "github:DoctorKhan/runctl#main"
96
+ curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash -s -- --git --ref main
58
97
  ```
59
98
 
60
- **Project dependency from GitHub** (not global): dependency resolves to **`@zendero/runctl`**. Reinstall to pull the latest `main`:
99
+ Use npm explicitly (e.g. no pnpm on the machine):
61
100
 
62
101
  ```bash
63
- pnpm add -D "github:DoctorKhan/runctl#main"
102
+ curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash -s -- --pm npm --registry
64
103
  ```
65
104
 
105
+ `--help` on the script prints the same usage summary.
106
+
66
107
  ---
67
108
 
68
109
  ## Quick start
@@ -95,12 +136,13 @@ Add scripts to your `package.json`:
95
136
  | `runctl start` \| `runctl dev` | Start dev server (same command; picks free port, backgrounds) |
96
137
  | `runctl stop [dir]` | Stop daemons & release ports |
97
138
  | `runctl status [dir]` | Show `.run` state for this package |
139
+ | `runctl ps` | List running programs with PID, port, service, project |
98
140
  | `runctl logs [dir] [service]` | Tail `.run/logs/<service>.log` (default service: `web`) |
99
141
  | `runctl ports` | List user-wide port registry (`~/.run`) |
100
142
  | `runctl ports gc` | Clean up stale port claims |
101
143
  | `runctl env expand <manifest> [--out file]` | Generate `.env.local` from manifest |
102
144
  | `runctl doctor [dir]` | Check Node 18+, `lsof`, package manager, `package.json` |
103
- | `runctl update` | Update the global `@zendero/runctl` install |
145
+ | `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)) |
104
146
  | `runctl version` | Print package version and install path |
105
147
 
106
148
  **Monorepo:** `runctl start ./apps/web --script dev:server`
package/bin/runctl CHANGED
@@ -20,12 +20,13 @@ ${_y}Commands${_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
+ ${_c}ps${_r} List running programs with ports/projects
23
24
  ${_c}logs${_r} [dir] [service] Tail .run/logs/<service>.log ${_d}(default service: web)${_r}
24
25
  ${_c}ports${_r} List user-wide port registry (~/.run)
25
26
  ${_c}ports gc${_r} Clean up stale port claims
26
27
  ${_c}env expand${_r} <manifest> [opts] Generate .env.local from manifest
27
28
  ${_c}doctor${_r} Check Node, tooling, and project basics
28
- ${_c}update${_r} Update runctl to latest version
29
+ ${_c}update${_r} [npm|git|auto] [opts] Update global runctl ${_d}(default: auto; see runctl update --help)${_r}
29
30
 
30
31
  ${_y}Options${_r}
31
32
  ${_c}--script${_r} <name> Package script to run ${_d}(default: dev)${_r}
@@ -156,6 +157,23 @@ cmd_ports() {
156
157
  esac
157
158
  }
158
159
 
160
+ cmd_ps() {
161
+ # shellcheck source=../lib/run-lib.sh
162
+ source "$RUNCTL_PKG_ROOT/lib/run-lib.sh"
163
+ run_global_list_running
164
+ local stale="${RUN_LAST_STALE_COUNT:-0}"
165
+ if [[ "$stale" =~ ^[0-9]+$ ]] && (( stale > 0 )); then
166
+ echo ""
167
+ echo "runctl ps: found $stale stale registry entr$( (( stale == 1 )) && echo "y" || echo "ies" ); cleaning..."
168
+ (run_global_gc >/dev/null 2>&1) &
169
+ local gc_pid=$!
170
+ wait "$gc_pid" || true
171
+ echo "runctl ps: refreshed"
172
+ echo ""
173
+ run_global_list_running
174
+ fi
175
+ }
176
+
159
177
  cmd_env() {
160
178
  local sub="${1:-}"
161
179
  shift || true
@@ -174,16 +192,182 @@ cmd_env() {
174
192
  esac
175
193
  }
176
194
 
177
- cmd_update() {
178
- echo "runctl update: installing latest @zendero/runctl..."
195
+ cmd_update_usage() {
196
+ cat <<EOF
197
+ ${_b}runctl update${_r} — refresh the global CLI (pnpm/npm global install)
198
+
199
+ ${_y}Usage:${_r} runctl update [npm|git|auto] [options]
200
+
201
+ ${_y}Source${_r} ${_d}(pick one; default when omitted: ${_c}auto${_r})${_r}
202
+ ${_c}npm${_r} | ${_c}--registry${_r} Latest from npm only (${_d}${RUNCTL_PACKAGE:-@zendero/runctl}@latest${_r})
203
+ ${_c}git${_r} | ${_c}--git${_r} From Git only (${_d}RUNCTL_GIT_BASE + ref${_r})
204
+ ${_c}auto${_r} | ${_c}--auto${_r} Try npm @latest, then Git if that fails
205
+
206
+ ${_y}Options${_r}
207
+ ${_c}--pm${_r} ${_d}pnpm|npm${_r} Package manager ${_d}(default: pnpm if on PATH, else npm)${_r}
208
+ ${_c}--ref${_r} ${_d}<ref>${_r} Git ref for --git / Git fallback ${_d}(default: RUNCTL_GIT_REF or main)${_r}
209
+ ${_c}-h${_r}, ${_c}--help${_r} This help
210
+
211
+ ${_y}Environment${_r} ${_d}(optional; same as install-global.sh)${_r}
212
+ RUNCTL_PACKAGE npm name ${_d}(default: @zendero/runctl)${_r}
213
+ RUNCTL_GIT_BASE Git URL without fragment ${_d}(default: git+https://github.com/DoctorKhan/runctl.git)${_r}
214
+ RUNCTL_GIT_REF default Git ref ${_d}(default: main)${_r}
215
+ EOF
216
+ }
217
+
218
+ cmd_update_pick_pm() {
219
+ if [[ -n "$1" ]]; then
220
+ case "$1" in
221
+ pnpm | npm) ;;
222
+ *)
223
+ echo "runctl update: unsupported --pm: $1 (use pnpm or npm)" >&2
224
+ return 1
225
+ ;;
226
+ esac
227
+ if ! command -v "$1" >/dev/null 2>&1; then
228
+ echo "runctl update: package manager not on PATH: $1" >&2
229
+ return 1
230
+ fi
231
+ printf '%s' "$1"
232
+ return 0
233
+ fi
179
234
  if command -v pnpm >/dev/null 2>&1; then
180
- pnpm add -g @zendero/runctl@latest
181
- elif command -v npm >/dev/null 2>&1; then
182
- npm install -g @zendero/runctl@latest
183
- else
184
- echo "runctl: pnpm or npm required to update." >&2
235
+ printf 'pnpm'
236
+ return 0
237
+ fi
238
+ if command -v npm >/dev/null 2>&1; then
239
+ printf 'npm'
240
+ return 0
241
+ fi
242
+ echo "runctl update: pnpm or npm required on PATH." >&2
243
+ return 1
244
+ }
245
+
246
+ cmd_update() {
247
+ local MODE="" PM="" GIT_REF="${RUNCTL_GIT_REF:-main}"
248
+ local PKG="${RUNCTL_PACKAGE:-@zendero/runctl}"
249
+ local GIT_BASE="${RUNCTL_GIT_BASE:-git+https://github.com/DoctorKhan/runctl.git}"
250
+ local -a POS=()
251
+
252
+ while [[ $# -gt 0 ]]; do
253
+ case "$1" in
254
+ --registry)
255
+ MODE="registry"
256
+ shift
257
+ ;;
258
+ --git)
259
+ MODE="git"
260
+ shift
261
+ ;;
262
+ --auto)
263
+ MODE="auto"
264
+ shift
265
+ ;;
266
+ --pm)
267
+ [[ $# -ge 2 ]] || {
268
+ echo "runctl update: --pm requires a value" >&2
269
+ exit 1
270
+ }
271
+ PM="$2"
272
+ shift 2
273
+ ;;
274
+ --ref)
275
+ [[ $# -ge 2 ]] || {
276
+ echo "runctl update: --ref requires a value" >&2
277
+ exit 1
278
+ }
279
+ GIT_REF="$2"
280
+ shift 2
281
+ ;;
282
+ -h | --help)
283
+ cmd_update_usage
284
+ exit 0
285
+ ;;
286
+ -*)
287
+ echo "runctl update: unknown option: $1" >&2
288
+ cmd_update_usage >&2
289
+ exit 1
290
+ ;;
291
+ *)
292
+ POS+=("$1")
293
+ shift
294
+ ;;
295
+ esac
296
+ done
297
+
298
+ local POS_MODE=""
299
+ if [[ ${#POS[@]} -gt 1 ]]; then
300
+ echo "runctl update: too many arguments (${POS[*]}). Use at most one of: npm, git, auto" >&2
301
+ exit 1
302
+ fi
303
+ if [[ ${#POS[@]} -eq 1 ]]; then
304
+ case "${POS[0]}" in
305
+ npm)
306
+ POS_MODE="registry"
307
+ ;;
308
+ git)
309
+ POS_MODE="git"
310
+ ;;
311
+ auto)
312
+ POS_MODE="auto"
313
+ ;;
314
+ *)
315
+ echo "runctl update: unknown argument: ${POS[0]} (expected: npm, git, auto — runctl update --help)" >&2
316
+ exit 1
317
+ ;;
318
+ esac
319
+ fi
320
+ if [[ -n "$MODE" && -n "$POS_MODE" && "$MODE" != "$POS_MODE" ]]; then
321
+ echo "runctl update: conflicting source (npm/git/auto vs --registry/--git/--auto)" >&2
185
322
  exit 1
186
323
  fi
324
+ if [[ -n "$POS_MODE" ]]; then
325
+ MODE="$POS_MODE"
326
+ fi
327
+ [[ -n "$MODE" ]] || MODE="auto"
328
+
329
+ local pm git_target
330
+ pm="$(cmd_update_pick_pm "$PM")" || exit 1
331
+ git_target="${GIT_BASE}#${GIT_REF}"
332
+
333
+ _update_registry() {
334
+ local spec="${PKG}@latest"
335
+ echo "runctl update: installing latest from registry ($spec)..."
336
+ if [[ "$pm" == "pnpm" ]]; then
337
+ pnpm add -g "$spec"
338
+ else
339
+ npm install -g "$spec"
340
+ fi
341
+ }
342
+
343
+ _update_git() {
344
+ echo "runctl update: installing from Git ($git_target)..."
345
+ if [[ "$pm" == "pnpm" ]]; then
346
+ pnpm add -g "$git_target"
347
+ else
348
+ npm install -g "$git_target"
349
+ fi
350
+ }
351
+
352
+ case "$MODE" in
353
+ registry)
354
+ _update_registry
355
+ ;;
356
+ git)
357
+ _update_git
358
+ ;;
359
+ auto)
360
+ if _update_registry; then
361
+ return 0
362
+ fi
363
+ echo "runctl update: registry install failed; trying Git..." >&2
364
+ _update_git
365
+ ;;
366
+ *)
367
+ echo "runctl update: internal error: unknown mode $MODE" >&2
368
+ exit 1
369
+ ;;
370
+ esac
187
371
  }
188
372
 
189
373
  cmd_doctor() {
@@ -283,11 +467,12 @@ main() {
283
467
  open) cmd_open "$@" ;;
284
468
  stop) cmd_stop "$@" ;;
285
469
  status) cmd_status "$@" ;;
470
+ ps) cmd_ps "$@" ;;
286
471
  logs) cmd_logs "$@" ;;
287
472
  ports) cmd_ports "$@" ;;
288
473
  env) cmd_env "$@" ;;
289
474
  doctor) cmd_doctor "$@" ;;
290
- update) cmd_update ;;
475
+ update) cmd_update "$@" ;;
291
476
 
292
477
  # Plumbing
293
478
  lib-path) printf '%s\n' "$RUNCTL_PKG_ROOT/lib/run-lib.sh" ;;
package/lib/run-lib.sh CHANGED
@@ -501,6 +501,64 @@ run_global_list_ports() {
501
501
  rm -f "$tmp"
502
502
  }
503
503
 
504
+ run_pid_program_name() {
505
+ local pid="$1"
506
+ [[ -n "$pid" ]] || return 1
507
+ ps -p "$pid" -o comm= 2>/dev/null | awk '{$1=$1; print}'
508
+ }
509
+
510
+ run_global_list_running() {
511
+ mkdir -p "$RUN_GLOBAL_STATE/ports"
512
+ local f port tmp pid svc proot _line _k _v prog stale_count
513
+ stale_count=0
514
+ tmp="$(mktemp "${TMPDIR:-/tmp}/runlib-running.XXXXXX")"
515
+ shopt -s nullglob
516
+ for f in "$RUN_GLOBAL_STATE/ports"/*; do
517
+ [[ -f "$f" ]] || continue
518
+ port="$(basename "$f")"
519
+ [[ "$port" =~ ^[0-9]+$ ]] || continue
520
+ pid="" svc="" proot="" prog=""
521
+ while IFS= read -r _line; do
522
+ [[ -z "$_line" ]] && continue
523
+ _k="${_line%%=*}"
524
+ _v="${_line#*=}"
525
+ case "$_k" in
526
+ pid) pid="$_v" ;;
527
+ service) svc="$_v" ;;
528
+ project_root) proot="$_v" ;;
529
+ esac
530
+ done <"$f"
531
+ if [[ -z "$pid" ]]; then
532
+ stale_count=$((stale_count + 1))
533
+ continue
534
+ fi
535
+ if ! run_pid_alive "$pid"; then
536
+ stale_count=$((stale_count + 1))
537
+ continue
538
+ fi
539
+ if ! run_pid_listens_on_port "$pid" "$port"; then
540
+ stale_count=$((stale_count + 1))
541
+ continue
542
+ fi
543
+ prog="$(run_pid_program_name "$pid" || true)"
544
+ [[ -n "$prog" ]] || prog="(unknown)"
545
+ printf '%s\t%s\t%s\t%s\t%s\n' "$pid" "$prog" "$port" "${svc:--}" "${proot:--}" >>"$tmp"
546
+ done
547
+ shopt -u nullglob
548
+ printf '%-8s %-20s %-6s %-10s %s\n' "PID" "PROGRAM" "PORT" "SERVICE" "PROJECT"
549
+ printf '%-8s %-20s %-6s %-10s %s\n' "--------" "--------------------" "------" "----------" "-------"
550
+ if [[ -s "$tmp" ]]; then
551
+ sort -n -k1,1 "$tmp" | while IFS=$'\t' read -r pid prog port svc proot; do
552
+ printf '%-8s %-20s %-6s %-10s %s\n' "$pid" "$prog" "$port" "$svc" "$proot"
553
+ done
554
+ else
555
+ printf '%s\n' "(no running programs)"
556
+ fi
557
+ rm -f "$tmp"
558
+ RUN_LAST_STALE_COUNT="$stale_count"
559
+ export RUN_LAST_STALE_COUNT
560
+ }
561
+
504
562
  run_local_status() {
505
563
  printf 'runctl status\n'
506
564
  printf ' %-10s %s\n' "project:" "$RUN_PROJECT_ROOT"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zendero/runctl",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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,6 +37,7 @@
37
37
  ],
38
38
  "scripts": {
39
39
  "run": "bash ./run.sh",
40
+ "test": "bash ./tests/runctl-ps.test.sh",
40
41
  "doctor": "bash ./run.sh doctor",
41
42
  "release-check": "bash ./run.sh release-check",
42
43
  "promote": "bash ./run.sh promote",