@zendero/runctl 0.1.1 → 0.1.3
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 +56 -9
- package/bin/runctl +107 -6
- package/docs/vercel-and-env.md +2 -2
- package/examples/consumer-package.json +2 -0
- package/examples/run.sh.example +2 -2
- package/lib/run-lib.sh +45 -5
- package/package.json +5 -1
- package/scripts/install-global.sh +247 -12
package/README.md
CHANGED
|
@@ -10,22 +10,57 @@ Picks a **free port**, runs your **dev server in the background**, and keeps **P
|
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
The published package on npm is **`@zendero/runctl`**. The CLI binary on your PATH is still **`runctl`**.
|
|
14
|
+
|
|
15
|
+
**From the npm registry (recommended):**
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
pnpm add -D runctl # or npm install -D / yarn add -D
|
|
18
|
+
pnpm add -D @zendero/runctl # or npm install -D / yarn add -D
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
**Global CLI** (`runctl` on your PATH everywhere):
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
|
-
pnpm add -g runctl # or npm install -g
|
|
24
|
+
pnpm add -g @zendero/runctl # or npm install -g
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Global install via curl** uses a single script: [`scripts/install-global.sh`](scripts/install-global.sh)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" |
|
|
31
|
+
bash
|
|
32
|
+
```
|
|
33
|
+
|
|
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:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" |
|
|
38
|
+
bash -s -- --registry
|
|
23
39
|
```
|
|
24
40
|
|
|
25
|
-
|
|
41
|
+
```bash
|
|
42
|
+
curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" |
|
|
43
|
+
bash -s -- --auto
|
|
44
|
+
```
|
|
26
45
|
|
|
27
46
|
```bash
|
|
28
|
-
curl -fsSL https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh |
|
|
47
|
+
curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" |
|
|
48
|
+
bash -s -- --git --ref main
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Optional flags: `--pm pnpm|npm`, `--ref <git-ref>`. Optional env: `RUNCTL_PACKAGE`, `RUNCTL_GIT_BASE`, `RUNCTL_GIT_REF`.
|
|
52
|
+
|
|
53
|
+
**Without curl:**
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pnpm add -g @zendero/runctl
|
|
57
|
+
pnpm add -g "github:DoctorKhan/runctl#main"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Project dependency from GitHub** (not global): dependency resolves to **`@zendero/runctl`**. Reinstall to pull the latest `main`:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pnpm add -D "github:DoctorKhan/runctl#main"
|
|
29
64
|
```
|
|
30
65
|
|
|
31
66
|
---
|
|
@@ -57,14 +92,16 @@ Add scripts to your `package.json`:
|
|
|
57
92
|
|
|
58
93
|
| Command | What it does |
|
|
59
94
|
|---------|-------------|
|
|
60
|
-
| `runctl start
|
|
95
|
+
| `runctl start` \| `runctl dev` | Start dev server (same command; picks free port, backgrounds) |
|
|
61
96
|
| `runctl stop [dir]` | Stop daemons & release ports |
|
|
62
97
|
| `runctl status [dir]` | Show `.run` state for this package |
|
|
98
|
+
| `runctl logs [dir] [service]` | Tail `.run/logs/<service>.log` (default service: `web`) |
|
|
63
99
|
| `runctl ports` | List user-wide port registry (`~/.run`) |
|
|
64
100
|
| `runctl ports gc` | Clean up stale port claims |
|
|
65
101
|
| `runctl env expand <manifest> [--out file]` | Generate `.env.local` from manifest |
|
|
66
|
-
| `runctl
|
|
67
|
-
| `runctl
|
|
102
|
+
| `runctl doctor [dir]` | Check Node 18+, `lsof`, package manager, `package.json` |
|
|
103
|
+
| `runctl update` | Update the global `@zendero/runctl` install |
|
|
104
|
+
| `runctl version` | Print package version and install path |
|
|
68
105
|
|
|
69
106
|
**Monorepo:** `runctl start ./apps/web --script dev:server`
|
|
70
107
|
|
|
@@ -89,4 +126,14 @@ Add scripts to your `package.json`:
|
|
|
89
126
|
|
|
90
127
|
[`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)
|
|
91
128
|
|
|
92
|
-
**Develop this repo:** `pnpm install` → `./run.sh ports`
|
|
129
|
+
**Develop this repo:** `pnpm install` → `./run.sh` (default **doctor**, like `elata-bio-sdk/run.sh`) → `./run.sh ports`
|
|
130
|
+
|
|
131
|
+
**Publish (maintainers)** — workflow similar to elata’s release preflight, scaled for one package:
|
|
132
|
+
|
|
133
|
+
| Step | Command |
|
|
134
|
+
|------|--------|
|
|
135
|
+
| Preflight | `./run.sh release-check` or `pnpm run release-check` |
|
|
136
|
+
| Publish | `./run.sh release latest` or `pnpm run release` |
|
|
137
|
+
| Promote dist-tag | After publishing under `next`, `./run.sh promote` sets **latest** for the version in `package.json` |
|
|
138
|
+
|
|
139
|
+
Put `NPM_TOKEN` in `.env`. `release` / `npm-whoami` use a **temporary `NPM_CONFIG_USERCONFIG`** so a stale `~/.npmrc` token does not override `.env` (npm 10+ / pnpm). Token lines can use `NPM_TOKEN=` or `npm_token=`; quoted values are supported without `source`-ing secrets as shell code first.
|
package/bin/runctl
CHANGED
|
@@ -16,13 +16,15 @@ ${_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} [dir] [--script name]
|
|
19
|
+
${_c}start${_r} | ${_c}dev${_r} [dir] [--script name] Start dev server ${_d}(alias: dev = 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
|
+
${_c}logs${_r} [dir] [service] Tail .run/logs/<service>.log ${_d}(default service: web)${_r}
|
|
23
24
|
${_c}ports${_r} List user-wide port registry (~/.run)
|
|
24
25
|
${_c}ports gc${_r} Clean up stale port claims
|
|
25
26
|
${_c}env expand${_r} <manifest> [opts] Generate .env.local from manifest
|
|
27
|
+
${_c}doctor${_r} Check Node, tooling, and project basics
|
|
26
28
|
${_c}update${_r} Update runctl to latest version
|
|
27
29
|
|
|
28
30
|
${_y}Options${_r}
|
|
@@ -80,12 +82,20 @@ cmd_start() {
|
|
|
80
82
|
cmd_open() {
|
|
81
83
|
local proj
|
|
82
84
|
proj="$(_resolve_proj "$@")"
|
|
85
|
+
# shellcheck source=../lib/run-lib.sh
|
|
86
|
+
source "$RUNCTL_PKG_ROOT/lib/run-lib.sh"
|
|
87
|
+
run_project_init "$proj"
|
|
83
88
|
local ports_env="$proj/.run/ports.env"
|
|
84
89
|
if [[ ! -f "$ports_env" ]]; then
|
|
85
90
|
echo "runctl open: no running server found (.run/ports.env missing)" >&2
|
|
86
91
|
echo "Start one first: runctl start" >&2
|
|
87
92
|
exit 1
|
|
88
93
|
fi
|
|
94
|
+
if ! run_local_has_live_service; then
|
|
95
|
+
echo "runctl open: no running server found (saved port is stale)" >&2
|
|
96
|
+
echo "Start one first: runctl start" >&2
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
89
99
|
local port="" host=""
|
|
90
100
|
while IFS= read -r line; do
|
|
91
101
|
case "$line" in
|
|
@@ -165,32 +175,123 @@ cmd_env() {
|
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
cmd_update() {
|
|
168
|
-
echo "runctl update: installing latest runctl..."
|
|
178
|
+
echo "runctl update: installing latest @zendero/runctl..."
|
|
169
179
|
if command -v pnpm >/dev/null 2>&1; then
|
|
170
|
-
pnpm add -g runctl@latest
|
|
180
|
+
pnpm add -g @zendero/runctl@latest
|
|
171
181
|
elif command -v npm >/dev/null 2>&1; then
|
|
172
|
-
npm install -g runctl@latest
|
|
182
|
+
npm install -g @zendero/runctl@latest
|
|
173
183
|
else
|
|
174
184
|
echo "runctl: pnpm or npm required to update." >&2
|
|
175
185
|
exit 1
|
|
176
186
|
fi
|
|
177
187
|
}
|
|
178
188
|
|
|
189
|
+
cmd_doctor() {
|
|
190
|
+
local proj
|
|
191
|
+
proj="$(pwd)"
|
|
192
|
+
[[ $# -ge 1 && -d "$1" ]] && proj="$(cd "$1" && pwd)"
|
|
193
|
+
echo "runctl doctor"
|
|
194
|
+
local ec=0
|
|
195
|
+
if command -v node >/dev/null 2>&1 && node -e 'process.exit(parseInt(process.versions.node,10)>=18?0:1)' 2>/dev/null; then
|
|
196
|
+
printf ' %-12s %s\n' "node:" "$(node -v) (ok)"
|
|
197
|
+
else
|
|
198
|
+
printf ' %-12s %s\n' "node:" "missing or < 18 (required)" >&2
|
|
199
|
+
ec=1
|
|
200
|
+
fi
|
|
201
|
+
if command -v lsof >/dev/null 2>&1; then
|
|
202
|
+
printf ' %-12s %s\n' "lsof:" "present (port detection / gc)"
|
|
203
|
+
else
|
|
204
|
+
printf ' %-12s %s\n' "lsof:" "missing — install for free-port + ports gc" >&2
|
|
205
|
+
fi
|
|
206
|
+
local pm=""
|
|
207
|
+
if command -v pnpm >/dev/null 2>&1; then pm="pnpm $(pnpm -v 2>/dev/null || echo '')"
|
|
208
|
+
elif command -v npm >/dev/null 2>&1; then pm="npm $(npm -v 2>/dev/null || echo '')"
|
|
209
|
+
elif command -v bun >/dev/null 2>&1; then pm="bun $(bun -v 2>/dev/null || echo '')"
|
|
210
|
+
elif command -v yarn >/dev/null 2>&1; then pm="yarn $(yarn -v 2>/dev/null || echo '')"
|
|
211
|
+
else
|
|
212
|
+
pm="none found"
|
|
213
|
+
ec=1
|
|
214
|
+
fi
|
|
215
|
+
printf ' %-12s %s\n' "package mgr:" "$pm"
|
|
216
|
+
if [[ -f "$proj/package.json" ]]; then
|
|
217
|
+
printf ' %-12s %s\n' "package.json:" "$proj (ok)"
|
|
218
|
+
else
|
|
219
|
+
printf ' %-12s %s\n' "package.json:" "not found in $proj" >&2
|
|
220
|
+
ec=1
|
|
221
|
+
fi
|
|
222
|
+
return "$ec"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
cmd_logs() {
|
|
226
|
+
local proj lines="${RUNCTL_LOG_LINES:-80}"
|
|
227
|
+
proj="$(pwd)"
|
|
228
|
+
local -a pos=()
|
|
229
|
+
while [[ $# -gt 0 ]]; do
|
|
230
|
+
case "$1" in
|
|
231
|
+
-n | --lines)
|
|
232
|
+
lines="${2:-80}"
|
|
233
|
+
shift 2
|
|
234
|
+
;;
|
|
235
|
+
-h | --help)
|
|
236
|
+
echo "Usage: runctl logs [dir] [service] (default service: web)" >&2
|
|
237
|
+
echo " runctl logs [--lines N]" >&2
|
|
238
|
+
exit 0
|
|
239
|
+
;;
|
|
240
|
+
*)
|
|
241
|
+
pos+=("$1")
|
|
242
|
+
shift
|
|
243
|
+
;;
|
|
244
|
+
esac
|
|
245
|
+
done
|
|
246
|
+
if [[ ${#pos[@]} -ge 1 && -d "${pos[0]}" ]]; then
|
|
247
|
+
proj="$(cd "${pos[0]}" && pwd)"
|
|
248
|
+
pos=("${pos[@]:1}")
|
|
249
|
+
fi
|
|
250
|
+
local svc="${pos[0]:-web}"
|
|
251
|
+
# shellcheck source=../lib/run-lib.sh
|
|
252
|
+
source "$RUNCTL_PKG_ROOT/lib/run-lib.sh"
|
|
253
|
+
run_project_init "$proj"
|
|
254
|
+
local logf="$RUN_LOCAL_STATE/logs/${svc}.log"
|
|
255
|
+
if [[ ! -f "$logf" ]]; then
|
|
256
|
+
echo "runctl logs: no file at $logf" >&2
|
|
257
|
+
if [[ -d "$RUN_LOCAL_STATE/logs" ]]; then
|
|
258
|
+
echo "Available logs:" >&2
|
|
259
|
+
ls -1 "$RUN_LOCAL_STATE/logs" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
260
|
+
fi
|
|
261
|
+
exit 1
|
|
262
|
+
fi
|
|
263
|
+
echo "runctl logs: $logf (last $lines lines)"
|
|
264
|
+
tail -n "$lines" "$logf"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
cmd_version_print() {
|
|
268
|
+
local ver="?"
|
|
269
|
+
if command -v node >/dev/null 2>&1; then
|
|
270
|
+
ver="$(
|
|
271
|
+
node -e 'const fs=require("fs"); console.log(JSON.parse(fs.readFileSync(process.argv[1],"utf8")).version);' \
|
|
272
|
+
"$RUNCTL_PKG_ROOT/package.json" 2>/dev/null || echo "?"
|
|
273
|
+
)"
|
|
274
|
+
fi
|
|
275
|
+
printf '%s\n' "runctl $ver — $RUNCTL_PKG_ROOT"
|
|
276
|
+
}
|
|
277
|
+
|
|
179
278
|
main() {
|
|
180
279
|
local cmd="${1:-help}"
|
|
181
280
|
shift || true
|
|
182
281
|
case "$cmd" in
|
|
183
|
-
start)
|
|
282
|
+
start | dev) cmd_start "$@" ;;
|
|
184
283
|
open) cmd_open "$@" ;;
|
|
185
284
|
stop) cmd_stop "$@" ;;
|
|
186
285
|
status) cmd_status "$@" ;;
|
|
286
|
+
logs) cmd_logs "$@" ;;
|
|
187
287
|
ports) cmd_ports "$@" ;;
|
|
188
288
|
env) cmd_env "$@" ;;
|
|
289
|
+
doctor) cmd_doctor "$@" ;;
|
|
189
290
|
update) cmd_update ;;
|
|
190
291
|
|
|
191
292
|
# Plumbing
|
|
192
293
|
lib-path) printf '%s\n' "$RUNCTL_PKG_ROOT/lib/run-lib.sh" ;;
|
|
193
|
-
version | -v)
|
|
294
|
+
version | -v) cmd_version_print ;;
|
|
194
295
|
help | -h | --help) usage ;;
|
|
195
296
|
|
|
196
297
|
# Hidden backward-compat aliases
|
package/docs/vercel-and-env.md
CHANGED
|
@@ -12,8 +12,8 @@ Keep a **manifest** (not loaded directly by Next/Vite) that lists each real valu
|
|
|
12
12
|
- Expand to a generated file that frameworks **do** load:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
pnpm exec runctl
|
|
16
|
-
# or: node node_modules/runctl/scripts/expand-env-manifest.mjs env.manifest --out .env.local
|
|
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
|
|
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).
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"dev:server": "next dev",
|
|
5
5
|
"dev:stop": "runctl stop",
|
|
6
6
|
"dev:status": "runctl status",
|
|
7
|
+
"dev:logs": "runctl logs",
|
|
8
|
+
"doctor": "runctl doctor",
|
|
7
9
|
"ports": "runctl ports",
|
|
8
10
|
"ports:gc": "runctl ports gc",
|
|
9
11
|
"env:expand": "runctl env expand env.manifest --out .env.local"
|
package/examples/run.sh.example
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Optional shell entrypoint — most projects only need package.json scripts.
|
|
3
|
-
# Prerequisite: pnpm add -D runctl (Node >= 18)
|
|
3
|
+
# Prerequisite: pnpm add -D @zendero/runctl (Node >= 18)
|
|
4
4
|
|
|
5
5
|
set -euo pipefail
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
8
8
|
cd "$ROOT"
|
|
9
9
|
|
|
10
10
|
if [[ -z "${RUN_LIB:-}" ]]; then
|
|
11
|
-
_rp="${RUNCTL_PACKAGE
|
|
11
|
+
_rp="${RUNCTL_PACKAGE:-@zendero/runctl}"
|
|
12
12
|
RUN_LIB="$(
|
|
13
13
|
cd "$ROOT" && RUNCTL_PACKAGE="$_rp" node -e "
|
|
14
14
|
const p = require('path');
|
package/lib/run-lib.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# run-lib.sh — npm package
|
|
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
5
|
|
|
@@ -90,6 +90,28 @@ run_pid_alive() {
|
|
|
90
90
|
kill -0 "$1" 2>/dev/null
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
run_pid_listens_on_port() {
|
|
94
|
+
local pid="$1"
|
|
95
|
+
local port="$2"
|
|
96
|
+
[[ -n "$pid" && -n "$port" ]] || return 1
|
|
97
|
+
command -v lsof >/dev/null 2>&1 || return 1
|
|
98
|
+
lsof -iTCP:"$port" -sTCP:LISTEN -n -P 2>/dev/null | awk 'NR>1 {print $2}' | sort -u | grep -qx "$pid"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
run_local_has_live_service() {
|
|
102
|
+
local f pid
|
|
103
|
+
shopt -s nullglob
|
|
104
|
+
for f in "$RUN_LOCAL_STATE/pids"/*.pid; do
|
|
105
|
+
pid="$(cat "$f" 2>/dev/null || true)"
|
|
106
|
+
if [[ -n "$pid" ]] && run_pid_alive "$pid"; then
|
|
107
|
+
shopt -u nullglob
|
|
108
|
+
return 0
|
|
109
|
+
fi
|
|
110
|
+
done
|
|
111
|
+
shopt -u nullglob
|
|
112
|
+
return 1
|
|
113
|
+
}
|
|
114
|
+
|
|
93
115
|
run_port_listening() {
|
|
94
116
|
local port="$1"
|
|
95
117
|
command -v lsof >/dev/null 2>&1 || return 1
|
|
@@ -297,13 +319,31 @@ run_daemon_start() {
|
|
|
297
319
|
local logf="$RUN_LOCAL_STATE/logs/${name}.log"
|
|
298
320
|
if [[ -f "$pidf" ]]; then
|
|
299
321
|
local oldpid
|
|
322
|
+
local port=""
|
|
323
|
+
local port_service=""
|
|
300
324
|
oldpid="$(cat "$pidf")"
|
|
325
|
+
if [[ -f "$RUN_LOCAL_STATE/ports.env" ]]; then
|
|
326
|
+
while IFS='=' read -r k v; do
|
|
327
|
+
case "$k" in
|
|
328
|
+
PORT) port="$v" ;;
|
|
329
|
+
RUN_DEV_SERVICE) port_service="$v" ;;
|
|
330
|
+
esac
|
|
331
|
+
done <"$RUN_LOCAL_STATE/ports.env"
|
|
332
|
+
fi
|
|
301
333
|
if run_pid_alive "$oldpid"; then
|
|
302
|
-
|
|
303
|
-
|
|
334
|
+
if [[ -n "$port" ]] && [[ -z "$port_service" || "$port_service" == "$name" ]] && run_pid_listens_on_port "$oldpid" "$port"; then
|
|
335
|
+
echo "run_daemon_start: ${name} already running (pid $oldpid)" >&2
|
|
336
|
+
return 1
|
|
337
|
+
fi
|
|
338
|
+
if [[ -z "$port" ]]; then
|
|
339
|
+
echo "run_daemon_start: ${name} already running (pid $oldpid)" >&2
|
|
340
|
+
return 1
|
|
341
|
+
fi
|
|
342
|
+
echo "run_daemon_start: cleared stale pid for ${name} (pid $oldpid not listening on port $port)" >&2
|
|
343
|
+
else
|
|
344
|
+
echo "run_daemon_start: cleared stale pid for ${name} (was $oldpid)" >&2
|
|
304
345
|
fi
|
|
305
346
|
rm -f "$pidf"
|
|
306
|
-
echo "run_daemon_start: cleared stale pid for ${name} (was $oldpid)" >&2
|
|
307
347
|
fi
|
|
308
348
|
nohup "$@" >>"$logf" 2>&1 &
|
|
309
349
|
local pid=$!
|
|
@@ -414,7 +454,7 @@ run_global_gc() {
|
|
|
414
454
|
stale=1
|
|
415
455
|
fi
|
|
416
456
|
if [[ "$stale" -eq 0 ]] && [[ -n "$pid" ]] && command -v lsof >/dev/null 2>&1; then
|
|
417
|
-
if !
|
|
457
|
+
if ! run_pid_listens_on_port "$pid" "$port"; then
|
|
418
458
|
stale=1
|
|
419
459
|
fi
|
|
420
460
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zendero/runctl",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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,10 @@
|
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
39
|
"run": "bash ./run.sh",
|
|
40
|
+
"doctor": "bash ./run.sh doctor",
|
|
41
|
+
"release-check": "bash ./run.sh release-check",
|
|
42
|
+
"promote": "bash ./run.sh promote",
|
|
43
|
+
"release": "bash ./run.sh release latest",
|
|
40
44
|
"env:expand": "node ./scripts/expand-env-manifest.mjs",
|
|
41
45
|
"link-global": "pnpm link --global"
|
|
42
46
|
}
|
|
@@ -1,13 +1,248 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
#
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Global installer for @zendero/runctl.
|
|
3
|
+
#
|
|
3
4
|
# curl -fsSL https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh | bash
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
#
|
|
6
|
+
# Modes:
|
|
7
|
+
# --interactive prompt for package manager + install source
|
|
8
|
+
# --registry install from npm only
|
|
9
|
+
# --git install from Git only
|
|
10
|
+
# --auto install from npm, then fall back to Git
|
|
11
|
+
#
|
|
12
|
+
# Flags:
|
|
13
|
+
# --pm <pnpm|npm>
|
|
14
|
+
# --ref <git-ref>
|
|
15
|
+
# --help
|
|
16
|
+
#
|
|
17
|
+
# Optional env:
|
|
18
|
+
# RUNCTL_PACKAGE npm package name (default: @zendero/runctl)
|
|
19
|
+
# RUNCTL_GIT_BASE git URL without ref (default: git+https://github.com/DoctorKhan/runctl.git)
|
|
20
|
+
# RUNCTL_GIT_REF git ref for Git installs (default: main)
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
PKG="${RUNCTL_PACKAGE:-@zendero/runctl}"
|
|
24
|
+
GIT_BASE="${RUNCTL_GIT_BASE:-git+https://github.com/DoctorKhan/runctl.git}"
|
|
25
|
+
DEFAULT_GIT_REF="${RUNCTL_GIT_REF:-main}"
|
|
26
|
+
|
|
27
|
+
MODE=""
|
|
28
|
+
PM=""
|
|
29
|
+
GIT_REF="$DEFAULT_GIT_REF"
|
|
30
|
+
|
|
31
|
+
say() {
|
|
32
|
+
printf '%s\n' "$*"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
usage() {
|
|
36
|
+
cat <<'EOF'
|
|
37
|
+
Usage: install-global.sh [mode] [flags]
|
|
38
|
+
|
|
39
|
+
Modes:
|
|
40
|
+
--interactive Prompt for install options when a TTY is available.
|
|
41
|
+
--registry Install from the npm registry only.
|
|
42
|
+
--git Install from Git only.
|
|
43
|
+
--auto Install from the npm registry, then fall back to Git.
|
|
44
|
+
|
|
45
|
+
Flags:
|
|
46
|
+
--pm <pnpm|npm> Force a package manager.
|
|
47
|
+
--ref <git-ref> Git ref to use with --git or --auto fallback.
|
|
48
|
+
--help Show this help.
|
|
49
|
+
|
|
50
|
+
Default behavior:
|
|
51
|
+
If no mode is provided, use --interactive when a TTY is available.
|
|
52
|
+
Otherwise default to --auto.
|
|
53
|
+
EOF
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
has_tty() {
|
|
57
|
+
[[ -t 1 ]] && [[ -r /dev/tty ]]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
prompt_choice() {
|
|
61
|
+
local prompt="$1"
|
|
62
|
+
local default_value="$2"
|
|
63
|
+
shift 2
|
|
64
|
+
local options=("$@")
|
|
65
|
+
local answer
|
|
66
|
+
|
|
67
|
+
while true; do
|
|
68
|
+
printf '%s ' "$prompt" > /dev/tty
|
|
69
|
+
IFS= read -r answer < /dev/tty || answer=""
|
|
70
|
+
answer="${answer:-$default_value}"
|
|
71
|
+
|
|
72
|
+
for option in "${options[@]}"; do
|
|
73
|
+
if [[ "$answer" == "$option" ]]; then
|
|
74
|
+
printf '%s' "$answer"
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
say "Please choose one of: ${options[*]}" > /dev/tty
|
|
80
|
+
done
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
prompt_text() {
|
|
84
|
+
local prompt="$1"
|
|
85
|
+
local default_value="$2"
|
|
86
|
+
local answer
|
|
87
|
+
|
|
88
|
+
printf '%s ' "$prompt" > /dev/tty
|
|
89
|
+
IFS= read -r answer < /dev/tty || answer=""
|
|
90
|
+
printf '%s' "${answer:-$default_value}"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pick_package_manager() {
|
|
94
|
+
if [[ -n "$PM" ]]; then
|
|
95
|
+
if [[ "$PM" != "pnpm" && "$PM" != "npm" ]]; then
|
|
96
|
+
say "runctl: unsupported package manager: $PM" >&2
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
if ! command -v "$PM" >/dev/null 2>&1; then
|
|
100
|
+
say "runctl: requested package manager not found on PATH: $PM" >&2
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
printf '%s' "$PM"
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if command -v pnpm >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
|
|
108
|
+
if [[ "$MODE" == "interactive" ]]; then
|
|
109
|
+
prompt_choice "Package manager? [pnpm/npm] (default: pnpm)" "pnpm" "pnpm" "npm"
|
|
110
|
+
else
|
|
111
|
+
printf 'pnpm'
|
|
112
|
+
fi
|
|
113
|
+
return 0
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
if command -v pnpm >/dev/null 2>&1; then
|
|
117
|
+
printf 'pnpm'
|
|
118
|
+
return 0
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
if command -v npm >/dev/null 2>&1; then
|
|
122
|
+
printf 'npm'
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
say "runctl: neither pnpm nor npm was found on PATH." >&2
|
|
127
|
+
exit 1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
run_install() {
|
|
131
|
+
local manager="$1"
|
|
132
|
+
local source_kind="$2"
|
|
133
|
+
local target="$3"
|
|
134
|
+
|
|
135
|
+
if [[ "$manager" == "pnpm" ]]; then
|
|
136
|
+
pnpm add -g "$target"
|
|
137
|
+
else
|
|
138
|
+
npm install -g "$target"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
say
|
|
142
|
+
if [[ "$source_kind" == "git" ]]; then
|
|
143
|
+
say "Installed @zendero/runctl from Git — run: runctl help"
|
|
144
|
+
else
|
|
145
|
+
say "Installed $PKG — run: runctl help"
|
|
146
|
+
fi
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fallback_install() {
|
|
150
|
+
local manager="$1"
|
|
151
|
+
local git_target="$2"
|
|
152
|
+
|
|
153
|
+
say "runctl: registry install failed; trying Git ($git_target)..." >&2
|
|
154
|
+
run_install "$manager" "git" "$git_target"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
parse_args() {
|
|
158
|
+
while [[ $# -gt 0 ]]; do
|
|
159
|
+
case "$1" in
|
|
160
|
+
--interactive)
|
|
161
|
+
MODE="interactive"
|
|
162
|
+
;;
|
|
163
|
+
--registry)
|
|
164
|
+
MODE="registry"
|
|
165
|
+
;;
|
|
166
|
+
--git)
|
|
167
|
+
MODE="git"
|
|
168
|
+
;;
|
|
169
|
+
--auto)
|
|
170
|
+
MODE="auto"
|
|
171
|
+
;;
|
|
172
|
+
--pm)
|
|
173
|
+
[[ $# -ge 2 ]] || { say "runctl: --pm requires a value" >&2; exit 1; }
|
|
174
|
+
PM="$2"
|
|
175
|
+
shift
|
|
176
|
+
;;
|
|
177
|
+
--ref)
|
|
178
|
+
[[ $# -ge 2 ]] || { say "runctl: --ref requires a value" >&2; exit 1; }
|
|
179
|
+
GIT_REF="$2"
|
|
180
|
+
shift
|
|
181
|
+
;;
|
|
182
|
+
--help|-h)
|
|
183
|
+
usage
|
|
184
|
+
exit 0
|
|
185
|
+
;;
|
|
186
|
+
*)
|
|
187
|
+
say "runctl: unknown argument: $1" >&2
|
|
188
|
+
usage >&2
|
|
189
|
+
exit 1
|
|
190
|
+
;;
|
|
191
|
+
esac
|
|
192
|
+
shift
|
|
193
|
+
done
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
main() {
|
|
197
|
+
local manager git_target
|
|
198
|
+
|
|
199
|
+
parse_args "$@"
|
|
200
|
+
|
|
201
|
+
if [[ -z "$MODE" ]]; then
|
|
202
|
+
if has_tty && [[ "${CI:-}" != "1" ]]; then
|
|
203
|
+
MODE="interactive"
|
|
204
|
+
else
|
|
205
|
+
MODE="auto"
|
|
206
|
+
fi
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
manager="$(pick_package_manager)"
|
|
210
|
+
git_target="${GIT_BASE}#${GIT_REF}"
|
|
211
|
+
|
|
212
|
+
if [[ "$MODE" == "interactive" ]] && (! has_tty || [[ "${CI:-}" == "1" ]]); then
|
|
213
|
+
say "runctl: no interactive TTY detected; defaulting to --auto." >&2
|
|
214
|
+
MODE="auto"
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
case "$MODE" in
|
|
218
|
+
interactive)
|
|
219
|
+
say "runctl interactive installer"
|
|
220
|
+
say
|
|
221
|
+
say "This will install the global \`runctl\` CLI."
|
|
222
|
+
say
|
|
223
|
+
MODE="$(prompt_choice "Install source? [registry/git/auto] (default: auto)" "auto" "registry" "git" "auto")"
|
|
224
|
+
if [[ "$MODE" == "git" || "$MODE" == "auto" ]]; then
|
|
225
|
+
GIT_REF="$(prompt_text "Git ref to use? (default: ${GIT_REF})" "$GIT_REF")"
|
|
226
|
+
git_target="${GIT_BASE}#${GIT_REF}"
|
|
227
|
+
fi
|
|
228
|
+
;;
|
|
229
|
+
esac
|
|
230
|
+
|
|
231
|
+
case "$MODE" in
|
|
232
|
+
registry)
|
|
233
|
+
run_install "$manager" "registry" "$PKG"
|
|
234
|
+
;;
|
|
235
|
+
git)
|
|
236
|
+
say "runctl: installing from Git ($git_target)..." >&2
|
|
237
|
+
run_install "$manager" "git" "$git_target"
|
|
238
|
+
;;
|
|
239
|
+
auto)
|
|
240
|
+
if run_install "$manager" "registry" "$PKG"; then
|
|
241
|
+
return 0
|
|
242
|
+
fi
|
|
243
|
+
fallback_install "$manager" "$git_target"
|
|
244
|
+
;;
|
|
245
|
+
esac
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
main "$@"
|