turnout-cli 0.2.2 → 0.4.0

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
@@ -1,12 +1,164 @@
1
- # turnout-cli
1
+ <p align="center"><img src="https://github.com/lacodda/turnout/raw/main/assets/banner.svg" alt="turnout - a developer's switchyard" width="720"></p>
2
2
 
3
- npm distribution of [turnout](https://github.com/lacodda/turnout) - a developer's switchyard: point local apps at any backend stand, keep servers and secrets at hand, build and deploy from any directory.
3
+ > A developer's switchyard: point local apps at any backend stand, keep servers and secrets at hand, build and deploy - from any directory.
4
+
5
+ <p align="center">
6
+ <a href="https://crates.io/crates/turnout"><img src="https://img.shields.io/crates/v/turnout?style=flat-square" alt="crates.io"></a>
7
+ <a href="https://www.npmjs.com/package/turnout-cli"><img src="https://img.shields.io/npm/v/turnout-cli?style=flat-square" alt="npm"></a>
8
+ <a href="https://github.com/lacodda/turnout/actions"><img src="https://img.shields.io/github/actions/workflow/status/lacodda/turnout/ci.yml?style=flat-square" alt="CI"></a>
9
+ <a href="https://github.com/lacodda/turnout/blob/main/LICENSE"><img src="https://img.shields.io/github/license/lacodda/turnout?style=flat-square" alt="License"></a>
10
+ </p>
11
+
12
+ ## Why
13
+
14
+ Working against several backend stands scatters the day: you `cd` into a folder to start a project, edit `.env` files across repositories to switch a stand, dig through notes for a password, and keep deploy paths in your head.
15
+
16
+ turnout keeps all of it in one place and works from any directory.
17
+
18
+ ## A day in the life
19
+
20
+ Point an app at a stand:
21
+
22
+ ```console
23
+ $ turnout use web staging
24
+ 'web' now uses 'staging'.
25
+ The running gateway picks this up automatically.
26
+ Stand check: https://staging.example.com responded with 200 OK.
27
+ ```
28
+
29
+ Nothing in the project changed - the app still talks to `localhost`, and the [gateway](https://lacodda.github.io/turnout/concepts/gateway/) routes it to the stand you picked. Your session survives the switch, because cookies are kept per app **and** stand.
30
+
31
+ Start working, from wherever you happen to be:
32
+
33
+ ```console
34
+ $ cd ~/dev/web/src/components
35
+ $ turnout dev
36
+ [web] pnpm dev
37
+ ```
38
+
39
+ Move the whole contour at once when the frontend and the API must agree:
40
+
41
+ ```console
42
+ $ turnout use contour prod-eu
43
+ Group 'contour' now uses 'prod-eu':
44
+ web -> prod-eu
45
+ api -> prod-eu
46
+ ```
47
+
48
+ Forgot a name? Leave it out and pick from a list that shows where things point:
49
+
50
+ ```console
51
+ $ turnout use
52
+ ? Switch ›
53
+ ❯ contour group: web, api
54
+ api -> staging
55
+ web -> staging
56
+ ```
57
+
58
+ Ship it:
59
+
60
+ ```console
61
+ $ turnout deploy web -s prod-eu -b
62
+ [web] pnpm build
63
+ ✓ Connected to deploy@prod-eu.example.com:22
64
+ ✓ Backup 20260809-011500.tar.gz created in /var/www/web.backups
65
+ ================> 2.02 MiB/3.11 MiB · 1.81 MiB/s · eta 1s assets/index-b3f0a1.js
66
+ Uploaded 142 files (3.11 MiB) to prod-eu:/var/www/web
67
+ ✓ Ran: systemctl restart web
68
+ Deploy of 'web' to 'prod-eu' finished.
69
+ ```
70
+
71
+ Every long flag has a short form, and nothing runs silently: the upload reports throughput and an ETA, and the steps that talk to the server say so while they wait.
72
+
73
+ And see what has been going on:
74
+
75
+ ```console
76
+ $ turnout status
77
+ turnout 0.4.0
78
+ Data directory: ~/.local/share/lacodda/turnout
79
+ Apps: 2 (api, web)
80
+ Servers: 2 (prod-eu, staging)
81
+ Group: contour (web, api)
82
+ Access: saved for prod-eu
83
+ Bindings:
84
+ api -> staging
85
+ web -> prod-eu
86
+ Gateway: running (pid 24180; web:7100, api:7101)
87
+ Recent:
88
+ 2026-08-09T01:15:02Z deploy web -> prod-eu (142 files)
89
+ 2026-08-09T01:12:44Z use web -> prod-eu
90
+ ```
91
+
92
+ ## What you get
93
+
94
+ - **A dev gateway.** Apps always talk to `localhost`; turnout forwards to the selected stand over HTTP or HTTPS (self-signed certificates allowed per server), rewrites redirects, proxies WebSockets, and keeps a cookie jar per app+stand pair so switching does not log you out.
95
+ - **Secrets in the OS keyring** - Windows Credential Manager, macOS Keychain, Linux Secret Service. Copy a password to the clipboard with one command; nothing lands in a config file, and `status` only ever reports *that* a credential exists.
96
+ - **Commands from any directory.** `dev`, `build`, `test`, `lint` and any custom command run in the right project folder. Commands are taken from your actual `package.json` scripts, so a project whose dev script is `serve` still answers to `turnout dev`.
97
+ - **Deploy over SSH/SFTP** - build, upload, restart, with remote backup and restore when a release goes wrong.
98
+ - **Groups.** Bind a whole contour to one stand with a single `use`.
99
+ - **Nothing to memorize.** Leave a name out and pick it from a list; in bash, Tab completes app, server and group names from your own catalogs. The short alias `tn` is installed alongside.
100
+ - **An action journal.** Every state change appends one JSON line - what happened and to which entities, never secrets or output. `tail`, `grep` and `jq` work on it directly.
101
+
102
+ ## Install
103
+
104
+ **One-line installers.** Windows (PowerShell):
105
+
106
+ ```powershell
107
+ irm https://raw.githubusercontent.com/lacodda/turnout/main/tools/install.ps1 | iex
108
+ ```
109
+
110
+ macOS / Linux:
111
+
112
+ ```bash
113
+ curl -fsSL https://raw.githubusercontent.com/lacodda/turnout/main/tools/install.sh | sh
114
+ ```
115
+
116
+ **With npm:**
4
117
 
5
118
  ```bash
6
119
  npm i -g turnout-cli
7
- turnout setup
8
120
  ```
9
121
 
10
- The package downloads the prebuilt binary for your platform (Windows x86_64, Linux x86_64, macOS arm64) from GitHub Releases on install. On other platforms use `cargo install turnout`.
122
+ **With cargo:**
123
+
124
+ ```bash
125
+ cargo install turnout
126
+ ```
127
+
128
+ **Binary releases** - grab the archive for your platform from [Releases](https://github.com/lacodda/turnout/releases/latest) (Windows x86_64, Linux x86_64, macOS arm64), unpack and put `turnout` on your `PATH`.
129
+
130
+ The installers and the npm package also register the short alias `tn` (skipped if the name is already taken; `TURNOUT_NO_ALIAS=1` opts out). `cargo install` gives you `turnout` only.
131
+
132
+ ## Quick start
133
+
134
+ ```bash
135
+ turnout setup # first-run wizard: creates the data directory
136
+ turnout app add # register a project (detects its commands)
137
+ turnout server add # register a stand
138
+ turnout use # bind one to the other
139
+ turnout gateway start # route traffic through the gateway
140
+ ```
141
+
142
+ Data lives in the platform user data directory (e.g. `%LOCALAPPDATA%\lacodda\turnout` on Windows); set `TURNOUT_DATA_DIR` to override.
143
+
144
+ Full command reference and concepts: **[lacodda.github.io/turnout](https://lacodda.github.io/turnout/)**.
145
+
146
+ ## Status
147
+
148
+ Everything above works today. What is next:
149
+
150
+ - [ ] **Updates and portability** - update notice, `self-update`, `export` / `import` for moving to another machine
151
+ - [ ] **Credentials and paths as their own entities** - one login reused across servers, a remote directory declared once, and named builds so a deploy stays a single word
152
+ - [ ] **Background runs** - `dev --detach`, `ps`, `logs`, `stop`, OS notifications
153
+ - [ ] **Observability** - gateway request log, `doctor`, `report` for handing context to an assistant
154
+ - [ ] **Deploy consists** - atomic deploy and rollback across a group of apps
155
+
156
+ Released versions and what landed in each: [CHANGELOG on the Releases page](https://github.com/lacodda/turnout/releases).
157
+
158
+ ## Documentation
159
+
160
+ The documentation site (Astro Starlight) lives in [`docs/`](https://github.com/lacodda/turnout/tree/main/docs); architecture decision records are in [`docs/adr/`](https://github.com/lacodda/turnout/tree/main/docs/adr).
161
+
162
+ ## License
11
163
 
12
- Documentation: https://lacodda.github.io/turnout/
164
+ MIT (c) [Kirill Lakhtachev](https://lacodda.com)
package/download.js ADDED
@@ -0,0 +1,88 @@
1
+ // Downloads the turnout binary matching this package version from GitHub Releases.
2
+ // Shared by install.js (postinstall, where lifecycle scripts are allowed) and
3
+ // run.js (first-run fallback: npm v12 skips install scripts by default).
4
+ const fs = require("fs");
5
+ const https = require("https");
6
+ const path = require("path");
7
+ const { spawnSync } = require("child_process");
8
+
9
+ const pkg = require("./package.json");
10
+ const REPO = "lacodda/turnout";
11
+ // The wrapper can be patched independently of the Rust binary: an explicit
12
+ // turnout.binary field pins the release tag, otherwise it follows the version.
13
+ const TAG = (pkg.turnout && pkg.turnout.binary) || `v${pkg.version}`;
14
+
15
+ const TARGETS = {
16
+ "win32-x64": ["x86_64-pc-windows-msvc", "zip"],
17
+ "linux-x64": ["x86_64-unknown-linux-gnu", "tar.gz"],
18
+ "darwin-arm64": ["aarch64-apple-darwin", "tar.gz"],
19
+ };
20
+
21
+ const exe = process.platform === "win32" ? "turnout.exe" : "turnout";
22
+ const exePath = path.join(__dirname, exe);
23
+
24
+ function download(url, file, redirects, done) {
25
+ if (redirects > 5) return done(new Error("too many redirects"));
26
+ https
27
+ .get(url, { headers: { "user-agent": "turnout-cli" } }, (res) => {
28
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
29
+ res.resume();
30
+ return download(res.headers.location, file, redirects + 1, done);
31
+ }
32
+ if (res.statusCode !== 200) {
33
+ res.resume();
34
+ return done(new Error(`HTTP ${res.statusCode} for ${url}`));
35
+ }
36
+ const out = fs.createWriteStream(file);
37
+ res.pipe(out);
38
+ out.on("finish", () => out.close(done));
39
+ out.on("error", done);
40
+ })
41
+ .on("error", done);
42
+ }
43
+
44
+ function install(done) {
45
+ const key = `${process.platform}-${process.arch}`;
46
+ const entry = TARGETS[key];
47
+ if (!entry) {
48
+ console.error(`turnout-cli: no prebuilt binary for ${key}; install with: cargo install turnout`);
49
+ process.exit(1);
50
+ }
51
+ const [target, ext] = entry;
52
+ const name = `turnout-${TAG}-${target}`;
53
+ const url = `https://github.com/${REPO}/releases/download/${TAG}/${name}.${ext}`;
54
+ const archive = path.join(__dirname, `archive.${ext}`);
55
+
56
+ console.log(`turnout-cli: downloading ${url}`);
57
+ download(url, archive, 0, (err) => {
58
+ if (err) {
59
+ console.error(`turnout-cli: download failed: ${err.message}`);
60
+ process.exit(1);
61
+ }
62
+ // Extraction must not depend on which tar happens to be first in PATH
63
+ // (Git Bash ships a GNU tar that cannot read zip): zip goes through
64
+ // PowerShell's Expand-Archive, tar.gz through tar on Unix systems.
65
+ const result =
66
+ ext === "zip"
67
+ ? spawnSync(
68
+ "powershell.exe",
69
+ ["-NoProfile", "-NonInteractive", "-Command", "Expand-Archive -LiteralPath 'archive.zip' -DestinationPath . -Force"],
70
+ { cwd: __dirname, stdio: "inherit" },
71
+ )
72
+ : spawnSync("tar", ["-xzf", `archive.${ext}`], { cwd: __dirname, stdio: "inherit" });
73
+ if (result.status !== 0) {
74
+ console.error("turnout-cli: cannot extract the archive");
75
+ process.exit(1);
76
+ }
77
+ fs.renameSync(path.join(__dirname, name, exe), exePath);
78
+ fs.rmSync(path.join(__dirname, name), { recursive: true, force: true });
79
+ fs.rmSync(archive, { force: true });
80
+ if (process.platform !== "win32") {
81
+ fs.chmodSync(exePath, 0o755);
82
+ }
83
+ console.log(`turnout-cli: installed turnout ${TAG}`);
84
+ done();
85
+ });
86
+ }
87
+
88
+ module.exports = { install, exePath };
package/install.js CHANGED
@@ -1,79 +1,4 @@
1
- // Downloads the turnout binary matching this package version from GitHub Releases.
2
- const fs = require("fs");
3
- const https = require("https");
4
- const path = require("path");
5
- const { spawnSync } = require("child_process");
6
-
7
- const pkg = require("./package.json");
8
- const REPO = "lacodda/turnout";
9
- // The wrapper can be patched independently of the Rust binary: an explicit
10
- // turnout.binary field pins the release tag, otherwise it follows the version.
11
- const TAG = (pkg.turnout && pkg.turnout.binary) || `v${pkg.version}`;
12
-
13
- const TARGETS = {
14
- "win32-x64": ["x86_64-pc-windows-msvc", "zip"],
15
- "linux-x64": ["x86_64-unknown-linux-gnu", "tar.gz"],
16
- "darwin-arm64": ["aarch64-apple-darwin", "tar.gz"],
17
- };
18
-
19
- const key = `${process.platform}-${process.arch}`;
20
- const entry = TARGETS[key];
21
- if (!entry) {
22
- console.error(`turnout-cli: no prebuilt binary for ${key}; install with: cargo install turnout`);
23
- process.exit(1);
24
- }
25
- const [target, ext] = entry;
26
- const name = `turnout-${TAG}-${target}`;
27
- const url = `https://github.com/${REPO}/releases/download/${TAG}/${name}.${ext}`;
28
- const exe = process.platform === "win32" ? "turnout.exe" : "turnout";
29
- const archive = path.join(__dirname, `archive.${ext}`);
30
-
31
- function download(url, file, redirects, done) {
32
- if (redirects > 5) return done(new Error("too many redirects"));
33
- https
34
- .get(url, { headers: { "user-agent": "turnout-cli" } }, (res) => {
35
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
36
- res.resume();
37
- return download(res.headers.location, file, redirects + 1, done);
38
- }
39
- if (res.statusCode !== 200) {
40
- res.resume();
41
- return done(new Error(`HTTP ${res.statusCode} for ${url}`));
42
- }
43
- const out = fs.createWriteStream(file);
44
- res.pipe(out);
45
- out.on("finish", () => out.close(done));
46
- out.on("error", done);
47
- })
48
- .on("error", done);
49
- }
50
-
51
- console.log(`turnout-cli: downloading ${url}`);
52
- download(url, archive, 0, (err) => {
53
- if (err) {
54
- console.error(`turnout-cli: download failed: ${err.message}`);
55
- process.exit(1);
56
- }
57
- // Extraction must not depend on which tar happens to be first in PATH
58
- // (Git Bash ships a GNU tar that cannot read zip): zip goes through
59
- // PowerShell's Expand-Archive, tar.gz through tar on Unix systems.
60
- const result =
61
- ext === "zip"
62
- ? spawnSync(
63
- "powershell.exe",
64
- ["-NoProfile", "-NonInteractive", "-Command", "Expand-Archive -LiteralPath 'archive.zip' -DestinationPath . -Force"],
65
- { cwd: __dirname, stdio: "inherit" },
66
- )
67
- : spawnSync("tar", ["-xzf", `archive.${ext}`], { cwd: __dirname, stdio: "inherit" });
68
- if (result.status !== 0) {
69
- console.error("turnout-cli: cannot extract the archive");
70
- process.exit(1);
71
- }
72
- fs.renameSync(path.join(__dirname, name, exe), path.join(__dirname, exe));
73
- fs.rmSync(path.join(__dirname, name), { recursive: true, force: true });
74
- fs.rmSync(archive, { force: true });
75
- if (process.platform !== "win32") {
76
- fs.chmodSync(path.join(__dirname, exe), 0o755);
77
- }
78
- console.log(`turnout-cli: installed turnout ${TAG}`);
79
- });
1
+ // Postinstall entry: pre-downloads the binary where lifecycle scripts are
2
+ // allowed. On npm v12+ this script is skipped by default and run.js downloads
3
+ // the binary on first launch instead.
4
+ require("./download").install(() => {});
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "turnout-cli",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "turnout": {
5
- "binary": "v0.2.2"
5
+ "binary": "v0.4.0"
6
6
  },
7
7
  "description": "A developer's switchyard: point local apps at any backend stand, keep servers and secrets at hand, build and deploy from any directory",
8
8
  "license": "MIT",
@@ -14,9 +14,10 @@
14
14
  },
15
15
  "keywords": ["cli", "proxy", "gateway", "deploy", "dev-environment", "stands"],
16
16
  "bin": {
17
- "turnout": "run.js"
17
+ "turnout": "run.js",
18
+ "tn": "run.js"
18
19
  },
19
- "files": ["install.js", "run.js"],
20
+ "files": ["download.js", "install.js", "run.js"],
20
21
  "scripts": {
21
22
  "postinstall": "node install.js"
22
23
  },
package/run.js CHANGED
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env node
2
- // Thin launcher: forwards everything to the downloaded turnout binary.
3
- const path = require("path");
2
+ // Thin launcher: forwards everything to the turnout binary, downloading it on
3
+ // first run when the postinstall script was skipped (npm v12 default).
4
+ const fs = require("fs");
4
5
  const { spawnSync } = require("child_process");
6
+ const { install, exePath } = require("./download");
5
7
 
6
- const exe = path.join(__dirname, process.platform === "win32" ? "turnout.exe" : "turnout");
7
- if (!require("fs").existsSync(exe)) {
8
- console.error("turnout-cli: binary missing - reinstall the package (npm i -g turnout-cli)");
9
- process.exit(1);
8
+ function exec() {
9
+ const result = spawnSync(exePath, process.argv.slice(2), { stdio: "inherit" });
10
+ process.exit(result.status === null ? 1 : result.status);
11
+ }
12
+
13
+ if (fs.existsSync(exePath)) {
14
+ exec();
15
+ } else {
16
+ console.error("turnout-cli: binary not present yet - downloading it now");
17
+ install(exec);
10
18
  }
11
- const result = spawnSync(exe, process.argv.slice(2), { stdio: "inherit" });
12
- process.exit(result.status === null ? 1 : result.status);