mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(install): fold in both reviews of #460
The two reviews on the PR (DeepSeek Harness, then Claude) found one class of
bug twice and a list of smaller ones; all of them land here, each pinned in
test/install-sh-invariants.test.ts and, where it is bash logic, driven in the
bash:3.2 CI step as well.
The Start line the done screen prints is now composed in one place
(start_command_hint) from every non-default value, the same five the exec
branch exports through export_bind_env, so "do not start" under a sub-path or
a custom port no longer prints a bare `codeman web`. The --lan / --tailscale /
env preset paths read ${CODEMAN_PASSWORD:-$EXISTING_PASSWORD}: a flag re-run on
a unit that carried a password used to rewrite it without the password and
with the unauthenticated ack. --password and --port flip RECONFIGURE so they
reach the unit instead of taking the quiet update path, and `install.sh name`
re-syncs the unit's base URL after the mapping is re-added.
Also: the sudo keepalive is ended before the exec into the foreground server
(exec skips the EXIT trap, and the loop keys on $$); Ctrl+C in the HTTPS-toggle
poll is trapped for the poll only and skips Tailscale for the run instead of
killing the installer; uninstall asks before removing a LaunchDaemon this
installer never wrote; a foreign daemon gets a launchctl kickstart hint and the
done screen stops claiming the new build is running; the preflight summary
reads the Tailscale state with a line grep when node is not installed yet; the
LAN security notice uses the configured port; a bare re-run ends on the done
screen; a build failure after a rename names the install.sh tailscale
recovery; TS_JOINED_HERE (written, never read) is gone; the plan doc and
architecture-invariants say what the code does. A minor changeset is included.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'aicodeman': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Installer v2. `curl -fsSL https://getcodeman.com/install | bash` now looks at the machine first, asks at most three questions up front (how the dashboard is reached, optionally what to call the machine on your tailnet, whether to run Codeman as a background service), does the install unattended behind progress spinners with the output in `~/.codeman/install.log`, and ends on the URL with a QR code to scan. One consent covers every missing package and sudo asks for your password once. Flags pipe through `bash -s --` (`--tailscale | --lan | --local`, `--name <n> | --no-rename`, `--service | --run | --no-start`, `--yes`, `--password`, `--port`), `install.sh status` prints the URL and the QR code again, and the cloudflared question moved out of the main flow into `install.sh cloudflared`. On the Tailscale route, a `:443` that already belongs to another app gets Codeman under `https://<node>/codeman` (or on a second port) instead of a dead end, the node can be renamed opt-in (`--name`, `install.sh name`, undone by uninstall), and the HTTPS-certificates toggle is polled with the admin page opened for you. Also fixed on the way: the installer's own `npm install` no longer lets the postinstall start a stray server on port 3000 (the service crash-looped on EADDRINUSE while the done screen said "running"), the LAN address comes from the default route rather than the first interface, a hand-written LaunchDaemon on a headless Mac is left alone, a flag re-run keeps an existing dashboard password, and the done screen's start command carries the sub-path and port it was installed with.
|
||||||
@@ -121,6 +121,8 @@ jobs:
|
|||||||
[[ "$LAUNCH_CHOICE" == "2" ]]
|
[[ "$LAUNCH_CHOICE" == "2" ]]
|
||||||
check_tailscale() { return 0; }
|
check_tailscale() { return 0; }
|
||||||
ts_status_field() { case "$1" in "s.BackendState") printf Running ;; "s.Self && s.Self.DNSName") printf "box.tail.ts.net." ;; esac; }
|
ts_status_field() { case "$1" in "s.BackendState") printf Running ;; "s.Self && s.Self.DNSName") printf "box.tail.ts.net." ;; esac; }
|
||||||
|
ts_backend_state() { printf Running; }
|
||||||
|
ts_dns_name() { printf box.tail.ts.net; }
|
||||||
ts_serve_443_target_port() { printf 8080; }
|
ts_serve_443_target_port() { printf 8080; }
|
||||||
ts_serve_find_port_mapping() { :; }
|
ts_serve_find_port_mapping() { :; }
|
||||||
ts_serve_port_used() { return 1; }
|
ts_serve_port_used() { return 1; }
|
||||||
@@ -130,7 +132,17 @@ jobs:
|
|||||||
RENAMED=""; tailscale_rename_node() { RENAMED="$1"; }
|
RENAMED=""; tailscale_rename_node() { RENAMED="$1"; }
|
||||||
TS_NAME=""; tailscale_choose_name >/dev/null 2>&1
|
TS_NAME=""; tailscale_choose_name >/dev/null 2>&1
|
||||||
[[ -z "$RENAMED" ]]
|
[[ -z "$RENAMED" ]]
|
||||||
echo "bash $BASH_VERSION: question phase (flags, launch default, occupied :443, rename opt-in) ok"
|
# A flag re-run keeps the password the unit already carries (and so
|
||||||
|
# never writes the unauthenticated ack), and the hand-start line the
|
||||||
|
# done screen prints carries every non-default value.
|
||||||
|
read_existing_binding() { EXISTING_FOUND=1; EXISTING_HOST=0.0.0.0; EXISTING_PASSWORD=s3cret; EXISTING_ACK=0; EXISTING_BASE_URL=""; }
|
||||||
|
CODEMAN_HOST=0.0.0.0; CODEMAN_TAILSCALE=0; unset CODEMAN_PASSWORD; BIND_ACK=0
|
||||||
|
choose_network_binding >/dev/null 2>&1
|
||||||
|
[[ "$BIND_PASSWORD" == "s3cret" && "$BIND_ACK" == "0" ]]
|
||||||
|
BIND_HOST=0.0.0.0; BIND_PASSWORD=x; BIND_ACK=0; BIND_BASE_URL=/codeman; CODEMAN_PORT=4000
|
||||||
|
[[ "$(start_command_hint)" == "CODEMAN_HOST=0.0.0.0 CODEMAN_PASSWORD="*" CODEMAN_BASE_URL=/codeman CODEMAN_PORT=4000 codeman web" ]]
|
||||||
|
RECONFIGURE=0; parse_flags --port 4001; [[ "$RECONFIGURE" == "1" ]]
|
||||||
|
echo "bash $BASH_VERSION: question phase (flags, launch default, occupied :443, rename opt-in, kept password, start line) ok"
|
||||||
'
|
'
|
||||||
|
|
||||||
- name: CLI catalogue artifacts are in sync with stock.ts
|
- name: CLI catalogue artifacts are in sync with stock.ts
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Implementation detail extracted from `CLAUDE.md` so that file stays small enough
|
|||||||
|
|
||||||
### Default bind, and the non-loopback warning path
|
### Default bind, and the non-loopback warning path
|
||||||
|
|
||||||
**Default bind is loopback-only; non-loopback without a password starts but warns** — since COD-29 (PR #107) the web server defaults to `--host 127.0.0.1` (was `0.0.0.0`). As of **0.9.0** binding a non-loopback host (`--host`/`-H`/`CODEMAN_HOST`) without `CODEMAN_PASSWORD` **no longer refuses to start — it starts and prints a loud warning** listing the fixes (set `CODEMAN_PASSWORD`, bind loopback + tunnel/`tailscale serve`, or `--allow-unauthenticated-network` / `CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1` to acknowledge → terser note). Host classification is `isLoopbackBindHost()` in `network-auth-policy.ts`; the warn-vs-start logic is in `server.ts` `start()`; flags wired in `cli.ts`. ⚠️ Operational note: the production systemd unit runs `node dist/index.js web --https` with no `--host`, so it binds **localhost only** — reach it remotely via `tailscale serve`/tunnel to `127.0.0.1`, or add `Environment=CODEMAN_HOST=0.0.0.0` + `Environment=CODEMAN_PASSWORD=…` to `~/.config/systemd/user/codeman-web.service`. A loopback bind is reachable through a same-host tunnel (cloudflared/tailscale → `127.0.0.1`) but NOT by a browser hitting the box's LAN IP. Auth user defaults to `admin`. **Installer note** (1.8.x, `install.sh`): interactive installs now PROMPT for the binding, defaulting to LAN (`0.0.0.0`) with a required password prompt (skipping the password needs an explicit confirm and prints a loud warning); non-interactive installs keep loopback unless `CODEMAN_HOST` is preset, and re-runs/updates preserve the EXISTING binding (`read_existing_binding()` parses the current systemd unit / launchd plist). The server binary's own default is unchanged. **Full model: `docs/security-architecture.md`.**
|
**Default bind is loopback-only; non-loopback without a password starts but warns** — since COD-29 (PR #107) the web server defaults to `--host 127.0.0.1` (was `0.0.0.0`). As of **0.9.0** binding a non-loopback host (`--host`/`-H`/`CODEMAN_HOST`) without `CODEMAN_PASSWORD` **no longer refuses to start — it starts and prints a loud warning** listing the fixes (set `CODEMAN_PASSWORD`, bind loopback + tunnel/`tailscale serve`, or `--allow-unauthenticated-network` / `CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1` to acknowledge → terser note). Host classification is `isLoopbackBindHost()` in `network-auth-policy.ts`; the warn-vs-start logic is in `server.ts` `start()`; flags wired in `cli.ts`. ⚠️ Operational note: the production systemd unit runs `node dist/index.js web --https` with no `--host`, so it binds **localhost only** — reach it remotely via `tailscale serve`/tunnel to `127.0.0.1`, or add `Environment=CODEMAN_HOST=0.0.0.0` + `Environment=CODEMAN_PASSWORD=…` to `~/.config/systemd/user/codeman-web.service`. A loopback bind is reachable through a same-host tunnel (cloudflared/tailscale → `127.0.0.1`) but NOT by a browser hitting the box's LAN IP. Auth user defaults to `admin`. **Installer note** (`install.sh`, installer v2 since 2026-09-20): interactive installs PROMPT for the binding up front, defaulting to Tailscale when the node is already connected, else LAN (`0.0.0.0`), and to the existing choice on a re-run; a LAN bind asks for a password, and skipping it is a confirm that DEFAULTS TO YES (owner decision 2026-09-20) and ends on a loud red notice; non-interactive installs keep loopback unless `CODEMAN_HOST` is preset, and re-runs/updates preserve the EXISTING binding AND password (`read_existing_binding()` parses the current systemd unit / launchd plist, and the `--lan`/`--tailscale`/env preset paths read `${CODEMAN_PASSWORD:-$EXISTING_PASSWORD}`, since a flag re-run used to rewrite a password-protected unit open). The server binary's own default is unchanged. **Full model: `docs/security-architecture.md`.**
|
||||||
|
|
||||||
### Instance isolation and the multi-instance attach danger
|
### Instance isolation and the multi-instance attach danger
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,20 @@ Verification record for phase 1 (all on the maintainer's box, 2026-09-20):
|
|||||||
absent / logged out / HTTPS toggle off, the rename against a real node (the
|
absent / logged out / HTTPS toggle off, the rename against a real node (the
|
||||||
off-rename-re-add order is implemented but only unit-driven), macOS, uninstall. The
|
off-rename-re-add order is implemented but only unit-driven), macOS, uninstall. The
|
||||||
Mac mini and a throwaway VM are the venues; see section 8.
|
Mac mini and a throwaway VM are the venues; see section 8.
|
||||||
|
- **Review fixes (2026-09-21)**, from the two reviews on PR #460 (DeepSeek Harness, then
|
||||||
|
Claude): the done screen's Start line is composed from every non-default value
|
||||||
|
(`start_command_hint`, shared with the exec branch as `export_bind_env`), so "do not
|
||||||
|
start" under a sub-path or a custom port no longer prints a bare `codeman web`; the
|
||||||
|
`--lan`/`--tailscale`/env preset paths keep an existing password instead of rewriting
|
||||||
|
the unit open; `--password`/`--port` flip `RECONFIGURE` so they reach the unit;
|
||||||
|
`install.sh name` re-syncs the unit's base URL after a rename; the sudo keepalive is
|
||||||
|
ended before the `exec` into the foreground server; Ctrl+C in the HTTPS-toggle poll
|
||||||
|
skips Tailscale instead of killing the run; `uninstall` asks before removing a
|
||||||
|
LaunchDaemon it never wrote; a foreign LaunchDaemon gets a restart hint and the done
|
||||||
|
screen stops claiming the new build is running; the preflight summary reads the
|
||||||
|
Tailscale state without node; the LAN security notice uses the configured port; a
|
||||||
|
bare re-run ends on the done screen; a build failure after a rename names the
|
||||||
|
`install.sh tailscale` recovery; `TS_JOINED_HERE` is gone.
|
||||||
|
|
||||||
Goal, in one sentence: a user runs the one-liner, answers at most three questions, walks
|
Goal, in one sentence: a user runs the one-liner, answers at most three questions, walks
|
||||||
away during the build, and comes back to `https://<name>.<tailnet>.ts.net` printed with a
|
away during the build, and comes back to `https://<name>.<tailnet>.ts.net` printed with a
|
||||||
@@ -88,7 +102,7 @@ unit, so they cannot simply be merged. Left as-is in this plan (see section 9).
|
|||||||
| Option | Resulting URL | What it needs | Side effects | Verdict |
|
| Option | Resulting URL | What it needs | Side effects | Verdict |
|
||||||
| ------ | ------------- | ------------- | ------------ | ------- |
|
| ------ | ------------- | ------------- | ------------ | ------- |
|
||||||
| **A. Node name** (today) | `https://tnode.tailf80371.ts.net` | `tailscale serve --bg 3000` | none | **Default.** Zero admin-console work, matches the maintainer's prod. |
|
| **A. Node name** (today) | `https://tnode.tailf80371.ts.net` | `tailscale serve --bg 3000` | none | **Default.** Zero admin-console work, matches the maintainer's prod. |
|
||||||
| **B. Rename the node** | `https://codeman-tnode.tailf80371.ts.net` | `tailscale set --hostname codeman-<host>` (operator or root) | Renames the machine tailnet-wide: ssh targets, other serve URLs, the admin console entry. Tailscale de-dups a clash as `-1`. The cert follows the new name. | **Opt-in**, default YES only when the installer itself just joined this machine to the tailnet (nobody depends on the old name yet), default NO on a pre-existing node. |
|
| **B. Rename the node** | `https://codeman-tnode.tailf80371.ts.net` | `tailscale set --hostname codeman-<host>` (operator or root) | Renames the machine tailnet-wide: ssh targets, other serve URLs, the admin console entry. Tailscale de-dups a clash as `-1`. The cert follows the new name. | **Opt-in, default NO everywhere** (owner decision 2026-09-20: the machine is used for other things, so a bare Enter never renames it). The proposal was YES when the installer itself had just joined the tailnet; rejected. |
|
||||||
| **C. Tailscale Service** | `https://codeman.tailf80371.ts.net` | tailscale >= 1.86 on the host; the host must have a **tag-based identity** ("You cannot use a device authenticated with a user account as a Service host"); the service is defined in the admin console first; the host is then approved there (or via `autoApprovers.services`). Public beta since 2025-10-28, all plans. | Re-authenticating a personal machine as a tagged node changes its identity (SSH ACLs, user attribution). Known daemon quirk: approval is not picked up until `serve clear` + re-advertise (tailscale/tailscale#18821). | **Detect and hint only** in this round. The maintainer's own node has `Self.Tags: null`, so it could not host one without re-tagging. Worth a real flow once someone with a tagged fleet asks. |
|
| **C. Tailscale Service** | `https://codeman.tailf80371.ts.net` | tailscale >= 1.86 on the host; the host must have a **tag-based identity** ("You cannot use a device authenticated with a user account as a Service host"); the service is defined in the admin console first; the host is then approved there (or via `autoApprovers.services`). Public beta since 2025-10-28, all plans. | Re-authenticating a personal machine as a tagged node changes its identity (SSH ACLs, user attribution). Known daemon quirk: approval is not picked up until `serve clear` + re-advertise (tailscale/tailscale#18821). | **Detect and hint only** in this round. The maintainer's own node has `Self.Tags: null`, so it could not host one without re-tagging. Worth a real flow once someone with a tagged fleet asks. |
|
||||||
| **D. Sub-path** | `https://tnode.tailf80371.ts.net/codeman` | `tailscale serve --bg --set-path /codeman 3000` plus `--base-url /codeman` on the server | Codeman runs under a prefix. Hooks are unaffected (they hit the raw port with no prefix, which `rewriteUrl` already tolerates). Serve forwards the prefix unchanged, which is exactly the shape `--base-url` was built for. | **The answer when `:443` root is already taken.** Replaces today's replace-or-nothing prompt. |
|
| **D. Sub-path** | `https://tnode.tailf80371.ts.net/codeman` | `tailscale serve --bg --set-path /codeman 3000` plus `--base-url /codeman` on the server | Codeman runs under a prefix. Hooks are unaffected (they hit the raw port with no prefix, which `rewriteUrl` already tolerates). Serve forwards the prefix unchanged, which is exactly the shape `--base-url` was built for. | **The answer when `:443` root is already taken.** Replaces today's replace-or-nothing prompt. |
|
||||||
| **E. Second port** | `https://tnode.tailf80371.ts.net:8443` | `tailscale serve --bg --https=8443 3000` | Port in the URL; the beta-preview recipe already uses this. | Fallback when the user rejects D. |
|
| **E. Second port** | `https://tnode.tailf80371.ts.net:8443` | `tailscale serve --bg --https=8443 3000` | Port in the URL; the beta-preview recipe already uses this. | Fallback when the user rejects D. |
|
||||||
@@ -129,7 +143,7 @@ re-add**.
|
|||||||
Choose [1/2/3] (default 1):
|
Choose [1/2/3] (default 1):
|
||||||
|
|
||||||
2/3 Name this machine "codeman-tnode" on your tailnet? [y/N]
|
2/3 Name this machine "codeman-tnode" on your tailnet? [y/N]
|
||||||
(only shown for option 1; default Y when the installer just joined the tailnet)
|
(only shown for option 1; default no, always)
|
||||||
|
|
||||||
3/3 Run Codeman as a background service that starts on boot? [Y/n]
|
3/3 Run Codeman as a background service that starts on boot? [Y/n]
|
||||||
|
|
||||||
@@ -226,8 +240,10 @@ The state machine from the previous plan stays; these are the changes.
|
|||||||
1. **Preflight, before the build** (`tailscale_preflight`): installed? -> install
|
1. **Preflight, before the build** (`tailscale_preflight`): installed? -> install
|
||||||
(Linux: official script; macOS: brew cask, else download link and wait). Logged in? ->
|
(Linux: official script; macOS: brew cask, else download link and wait). Logged in? ->
|
||||||
`tailscale up` with the URL printed prominently and a 5-minute poll. Operator (Linux):
|
`tailscale up` with the URL printed prominently and a 5-minute poll. Operator (Linux):
|
||||||
grant once under the single sudo session. HTTPS certs: poll instead of ask. Record
|
grant once under the single sudo session. HTTPS certs: poll instead of ask (Ctrl+C
|
||||||
`TS_JOINED_HERE=1` when this run performed the login: it drives the rename default.
|
during the poll skips Tailscale for this run rather than ending the installer). The
|
||||||
|
rename default does not depend on whether this run performed the login (decided NO
|
||||||
|
everywhere), so nothing records it.
|
||||||
2. **Name** (`tailscale_choose_name`, question 2/3): shown only on the Tailscale route.
|
2. **Name** (`tailscale_choose_name`, question 2/3): shown only on the Tailscale route.
|
||||||
Default `codeman-<oshostname>` sanitized to `[a-z0-9-]`, max 63. Applied with
|
Default `codeman-<oshostname>` sanitized to `[a-z0-9-]`, max 63. Applied with
|
||||||
`ts_cmd_serve set --hostname`, then poll `.Self.DNSName` until it carries the new name
|
`ts_cmd_serve set --hostname`, then poll `.Self.DNSName` until it carries the new name
|
||||||
@@ -325,7 +341,7 @@ items never ran on a fresh machine:
|
|||||||
|
|
||||||
1. Tailscale absent, declined -> local-only, done screen shows the retrofit command.
|
1. Tailscale absent, declined -> local-only, done screen shows the retrofit command.
|
||||||
2. Tailscale absent, accepted -> install, login URL, operator, certs toggle polled, rename
|
2. Tailscale absent, accepted -> install, login URL, operator, certs toggle polled, rename
|
||||||
default YES, service, serve, URL verified, QR scans on a phone, PWA installs.
|
question shown (default no), service, serve, URL verified, QR scans on a phone, PWA installs.
|
||||||
3. Tailscale present and logged in on a pre-existing node -> rename default NO, URL is the
|
3. Tailscale present and logged in on a pre-existing node -> rename default NO, URL is the
|
||||||
node name, `serve status` gains exactly one entry.
|
node name, `serve status` gains exactly one entry.
|
||||||
4. `:443` root occupied -> path option -> `https://<node>/codeman` answers, hooks still
|
4. `:443` root occupied -> path option -> `https://<node>/codeman` answers, hooks still
|
||||||
@@ -352,9 +368,9 @@ Services flow if a tagged-fleet user asks for `codeman.<tailnet>.ts.net`.
|
|||||||
|
|
||||||
Decisions for the maintainer:
|
Decisions for the maintainer:
|
||||||
|
|
||||||
1. **Rename default.** Proposed: default YES only when this run joined the tailnet,
|
1. **Rename default.** Decided 2026-09-20: always NO; the yes answer, `--name` and
|
||||||
default NO otherwise, never on re-runs. The alternative is always NO with `--name` as
|
`install.sh name` are the ways in. (The proposal was YES only when this run had joined
|
||||||
the only way in.
|
the tailnet, NO otherwise; rejected because the host is used for other things.)
|
||||||
2. **Name pattern.** `codeman-<hostname>` (proposed; unique per machine, and two Codemans
|
2. **Name pattern.** `codeman-<hostname>` (proposed; unique per machine, and two Codemans
|
||||||
on one tailnet stay distinguishable) versus plain `codeman` (nicer once, collides on the
|
on one tailnet stay distinguishable) versus plain `codeman` (nicer once, collides on the
|
||||||
second install, Tailscale silently appends `-1`).
|
second install, Tailscale silently appends `-1`).
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ unattended. You can leave while it builds. What it asks you:
|
|||||||
2. **How the dashboard should be reachable.** Three choices:
|
2. **How the dashboard should be reachable.** Three choices:
|
||||||
- **Tailscale** (recommended for phone access): keeps the loopback bind, installs
|
- **Tailscale** (recommended for phone access): keeps the loopback bind, installs
|
||||||
Tailscale if needed, logs in, enables the tailnet HTTPS toggle (it opens the admin
|
Tailscale if needed, logs in, enables the tailnet HTTPS toggle (it opens the admin
|
||||||
page for you and waits), then configures `tailscale serve` after the build and
|
page for you and waits; Ctrl+C there skips Tailscale for this run), then configures `tailscale serve` after the build and
|
||||||
verifies the result end to end. If another app already owns `:443` on your node,
|
verifies the result end to end. If another app already owns `:443` on your node,
|
||||||
you choose between a sub-path (`https://<machine>.<tailnet>.ts.net/codeman`, the
|
you choose between a sub-path (`https://<machine>.<tailnet>.ts.net/codeman`, the
|
||||||
default), a second port, replacing the other mapping, or skipping.
|
default), a second port, replacing the other mapping, or skipping.
|
||||||
@@ -88,7 +88,9 @@ curl -fsSL https://getcodeman.com/install | bash -s -- --local --run
|
|||||||
`--tailscale` / `--lan` / `--local` answer the access question, `--name <n>` / `--no-rename`
|
`--tailscale` / `--lan` / `--local` answer the access question, `--name <n>` / `--no-rename`
|
||||||
the name, `--service` / `--run` / `--no-start` the last one. `--yes` takes every default
|
the name, `--service` / `--run` / `--no-start` the last one. `--yes` takes every default
|
||||||
(it still waits on a Tailscale login URL, and a network bind still asks for a password).
|
(it still waits on a Tailscale login URL, and a network bind still asks for a password).
|
||||||
`--port <n>` moves Codeman off 3000; the service file and the serve mapping follow it.
|
`--port <n>` moves Codeman off 3000; the service file and the serve mapping follow it. On an
|
||||||
|
existing install, `--port` and `--password` re-run the setup so the service file picks them up,
|
||||||
|
and a re-run with `--lan` or `--tailscale` keeps the password the service already has.
|
||||||
|
|
||||||
**Automation and CI**: with no terminal attached, any step that would change the system
|
**Automation and CI**: with no terminal attached, any step that would change the system
|
||||||
aborts with instructions instead of running silently. Set `CODEMAN_NONINTERACTIVE=1` to
|
aborts with instructions instead of running silently. Set `CODEMAN_NONINTERACTIVE=1` to
|
||||||
|
|||||||
+179
-52
@@ -95,14 +95,14 @@ TS_READY="0"
|
|||||||
TS_SERVE_MODE=""
|
TS_SERVE_MODE=""
|
||||||
TS_SERVE_PATH="/codeman"
|
TS_SERVE_PATH="/codeman"
|
||||||
TS_SERVE_PORT="8443"
|
TS_SERVE_PORT="8443"
|
||||||
# Set to 1 when THIS run performed the `tailscale up` login.
|
|
||||||
TS_JOINED_HERE="0"
|
|
||||||
# --name / CODEMAN_TAILSCALE_NAME, and --no-rename.
|
# --name / CODEMAN_TAILSCALE_NAME, and --no-rename.
|
||||||
TS_NAME="${CODEMAN_TAILSCALE_NAME:-}"
|
TS_NAME="${CODEMAN_TAILSCALE_NAME:-}"
|
||||||
TS_NO_RENAME="0"
|
TS_NO_RENAME="0"
|
||||||
# Set to 1 when a rename took our serve mapping down (it is keyed by the old
|
# Set to 1 when a rename took our serve mapping down (it is keyed by the old
|
||||||
# name), so the caller knows to re-add it and never adds one that was not there.
|
# name), so the caller knows to re-add it and never adds one that was not there.
|
||||||
TS_MAPPING_REMOVED_BY_RENAME="0"
|
TS_MAPPING_REMOVED_BY_RENAME="0"
|
||||||
|
# Set by the INT trap that is armed only while ensure_tailnet_https polls.
|
||||||
|
TS_HTTPS_POLL_INTERRUPTED="0"
|
||||||
# Where a rename is recorded so uninstall can offer to undo it (tailscaled does
|
# Where a rename is recorded so uninstall can offer to undo it (tailscaled does
|
||||||
# not remember previous names).
|
# not remember previous names).
|
||||||
TS_RENAME_RECORD="$HOME/.codeman/tailscale-rename"
|
TS_RENAME_RECORD="$HOME/.codeman/tailscale-rename"
|
||||||
@@ -256,7 +256,7 @@ print_security_notice() {
|
|||||||
echo -e " ${DIM}Details: docs/security-architecture.md${NC}"
|
echo -e " ${DIM}Details: docs/security-architecture.md${NC}"
|
||||||
elif [[ "$BIND_HOST" == "0.0.0.0" ]]; then
|
elif [[ "$BIND_HOST" == "0.0.0.0" ]]; then
|
||||||
echo -e " ${YELLOW}${BOLD}Security:${NC}"
|
echo -e " ${YELLOW}${BOLD}Security:${NC}"
|
||||||
echo -e " The dashboard is reachable from your network at port 3000 and is"
|
echo -e " The dashboard is reachable from your network at port ${CODEMAN_PORT:-3000} and is"
|
||||||
echo -e " password-protected (user ${BOLD}admin${NC}). Keep that password strong:"
|
echo -e " password-protected (user ${BOLD}admin${NC}). Keep that password strong:"
|
||||||
echo -e " whoever logs in can run commands through your agents."
|
echo -e " whoever logs in can run commands through your agents."
|
||||||
echo -e " For access from OUTSIDE your network, prefer Tailscale or a tunnel."
|
echo -e " For access from OUTSIDE your network, prefer Tailscale or a tunnel."
|
||||||
@@ -296,15 +296,26 @@ print_security_notice() {
|
|||||||
# Cleanup on Failure
|
# Cleanup on Failure
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
cleanup() {
|
# End the spinner and the sudo keepalive. Called from the EXIT trap, and by
|
||||||
local exit_code=$?
|
# hand right before `exec` in main(): exec replaces this shell WITHOUT running
|
||||||
|
# the trap, and the keepalive keys on $$, which is then the server's pid, so
|
||||||
|
# it would refresh the sudo timestamp for the whole life of the server.
|
||||||
|
stop_background_helpers() {
|
||||||
if [[ -n "$SPINNER_PID" ]]; then
|
if [[ -n "$SPINNER_PID" ]]; then
|
||||||
kill "$SPINNER_PID" 2>/dev/null || true
|
kill "$SPINNER_PID" 2>/dev/null || true
|
||||||
|
SPINNER_PID=""
|
||||||
printf '\r\033[K' >&2
|
printf '\r\033[K' >&2
|
||||||
fi
|
fi
|
||||||
if [[ -n "$SUDO_KEEPALIVE_PID" ]]; then
|
if [[ -n "$SUDO_KEEPALIVE_PID" ]]; then
|
||||||
kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
||||||
|
SUDO_KEEPALIVE_PID=""
|
||||||
fi
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
local exit_code=$?
|
||||||
|
stop_background_helpers
|
||||||
# The "partial install" advice is only true once the work phase has begun:
|
# The "partial install" advice is only true once the work phase has begun:
|
||||||
# a bad flag or a refused question exits before anything was written.
|
# a bad flag or a refused question exits before anything was written.
|
||||||
if [[ $exit_code -ne 0 && -n "$CURRENT_STEP" ]]; then
|
if [[ $exit_code -ne 0 && -n "$CURRENT_STEP" ]]; then
|
||||||
@@ -317,6 +328,12 @@ cleanup() {
|
|||||||
error "Step output was saved to $LOG_FILE"
|
error "Step output was saved to $LOG_FILE"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
# A rename takes our serve mapping down in the question phase and the
|
||||||
|
# after-the-build half puts it back; a failure in between leaves nothing
|
||||||
|
# fronting Codeman, and `install.sh tailscale` is what restores it.
|
||||||
|
if [[ $exit_code -ne 0 && "$TS_MAPPING_REMOVED_BY_RENAME" == "1" && -z "$TAILSCALE_SERVE_URL" ]]; then
|
||||||
|
error "The Tailscale serve mapping was taken down for the rename and not re-added. Restore it with: bash $INSTALL_DIR/install.sh tailscale"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
@@ -1566,14 +1583,25 @@ read_existing_binding() {
|
|||||||
# CODEMAN_HOST is preset. The server binary itself still defaults to 127.0.0.1
|
# CODEMAN_HOST is preset. The server binary itself still defaults to 127.0.0.1
|
||||||
# either way.
|
# either way.
|
||||||
choose_network_binding() {
|
choose_network_binding() {
|
||||||
|
# A previous install's choice is the baseline: re-installing must never
|
||||||
|
# silently loosen it. That holds for the preset paths below too: a flag
|
||||||
|
# re-run (`--lan --service` on a unit that carried a password) used to
|
||||||
|
# rewrite the unit without the password AND with the unauthenticated
|
||||||
|
# ack, and `--tailscale` dropped the password the same way (found in
|
||||||
|
# review, 2026-09-21). The caller's own CODEMAN_PASSWORD still wins.
|
||||||
|
read_existing_binding
|
||||||
|
|
||||||
# Preset via environment or flag: honor it and skip the prompt entirely.
|
# Preset via environment or flag: honor it and skip the prompt entirely.
|
||||||
# CODEMAN_TAILSCALE=1 composes with a loopback (or absent) CODEMAN_HOST.
|
# CODEMAN_TAILSCALE=1 composes with a loopback (or absent) CODEMAN_HOST.
|
||||||
if [[ -n "${CODEMAN_HOST:-}" ]]; then
|
if [[ -n "${CODEMAN_HOST:-}" ]]; then
|
||||||
BIND_HOST="$CODEMAN_HOST"
|
BIND_HOST="$CODEMAN_HOST"
|
||||||
BIND_PASSWORD="${CODEMAN_PASSWORD:-}"
|
BIND_PASSWORD="${CODEMAN_PASSWORD:-$EXISTING_PASSWORD}"
|
||||||
if [[ "$BIND_HOST" != "127.0.0.1" && -z "$BIND_PASSWORD" ]]; then
|
if [[ "$BIND_HOST" != "127.0.0.1" && -z "$BIND_PASSWORD" ]]; then
|
||||||
BIND_ACK="1"
|
BIND_ACK="1"
|
||||||
fi
|
fi
|
||||||
|
if [[ -n "$EXISTING_PASSWORD" && -z "${CODEMAN_PASSWORD:-}" ]]; then
|
||||||
|
info "Keeping the existing dashboard password"
|
||||||
|
fi
|
||||||
info "Network binding preset: $BIND_HOST"
|
info "Network binding preset: $BIND_HOST"
|
||||||
if [[ "${CODEMAN_TAILSCALE:-0}" == "1" ]]; then
|
if [[ "${CODEMAN_TAILSCALE:-0}" == "1" ]]; then
|
||||||
if [[ "$BIND_HOST" == "127.0.0.1" ]]; then
|
if [[ "$BIND_HOST" == "127.0.0.1" ]]; then
|
||||||
@@ -1586,16 +1614,15 @@ choose_network_binding() {
|
|||||||
fi
|
fi
|
||||||
if [[ "${CODEMAN_TAILSCALE:-0}" == "1" ]]; then
|
if [[ "${CODEMAN_TAILSCALE:-0}" == "1" ]]; then
|
||||||
BIND_HOST="127.0.0.1"
|
BIND_HOST="127.0.0.1"
|
||||||
BIND_PASSWORD="${CODEMAN_PASSWORD:-}"
|
BIND_PASSWORD="${CODEMAN_PASSWORD:-$EXISTING_PASSWORD}"
|
||||||
|
if [[ -n "$EXISTING_PASSWORD" && -z "${CODEMAN_PASSWORD:-}" ]]; then
|
||||||
|
info "Keeping the existing dashboard password"
|
||||||
|
fi
|
||||||
info "Tailscale access preset"
|
info "Tailscale access preset"
|
||||||
tailscale_prepare || true
|
tailscale_prepare || true
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# A previous install's choice is the baseline: re-installing must never
|
|
||||||
# silently loosen it.
|
|
||||||
read_existing_binding
|
|
||||||
|
|
||||||
if [[ "$NONINTERACTIVE" == "1" ]] || ! has_tty; then
|
if [[ "$NONINTERACTIVE" == "1" ]] || ! has_tty; then
|
||||||
if [[ "$EXISTING_FOUND" == "1" ]]; then
|
if [[ "$EXISTING_FOUND" == "1" ]]; then
|
||||||
BIND_HOST="$EXISTING_HOST"
|
BIND_HOST="$EXISTING_HOST"
|
||||||
@@ -1615,7 +1642,7 @@ choose_network_binding() {
|
|||||||
local ts_hint="will be installed for you" ts_ready="0" ts_detected_url="" ts_preview=""
|
local ts_hint="will be installed for you" ts_ready="0" ts_detected_url="" ts_preview=""
|
||||||
if check_tailscale; then
|
if check_tailscale; then
|
||||||
ts_hint="installed, needs login"
|
ts_hint="installed, needs login"
|
||||||
if command -v node &>/dev/null && [[ "$(ts_status_field 's.BackendState')" == "Running" ]]; then
|
if command -v node &>/dev/null && [[ "$(ts_backend_state)" == "Running" ]]; then
|
||||||
ts_ready="1"
|
ts_ready="1"
|
||||||
ts_hint="already connected"
|
ts_hint="already connected"
|
||||||
ts_preview="https://$(ts_dns_name)"
|
ts_preview="https://$(ts_dns_name)"
|
||||||
@@ -1916,11 +1943,30 @@ ts_serve_port_used() {
|
|||||||
' "$1" 2>/dev/null | grep -q 1
|
' "$1" 2>/dev/null | grep -q 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# This node's MagicDNS name (no trailing dot), its short name, and the tailnet
|
# The daemon state (Running, NeedsLogin, Stopped, ...) and this node's
|
||||||
# suffix. Empty when tailscale is not running.
|
# MagicDNS name (no trailing dot), plus its short name and the tailnet suffix.
|
||||||
|
# Both are read through node when it is there and with a line grep when it is
|
||||||
|
# not: `tailscale status --json` is printed one key per line and Self precedes
|
||||||
|
# Peer, so the first match is this node. The preflight summary is the one
|
||||||
|
# reader that runs before node is installed, and it used to report a
|
||||||
|
# logged-in node as "not logged in" on exactly the fresh box this installer
|
||||||
|
# is for; everything else runs after ask_dependencies. Empty when tailscale
|
||||||
|
# is not running.
|
||||||
|
ts_backend_state() {
|
||||||
|
if command -v node &>/dev/null; then
|
||||||
|
ts_status_field 's.BackendState'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ts_cmd status --json 2>/dev/null | sed -n 's/^ *"BackendState": *"\([^"]*\)".*/\1/p' | head -1 || true
|
||||||
|
}
|
||||||
|
|
||||||
ts_dns_name() {
|
ts_dns_name() {
|
||||||
local dns
|
local dns
|
||||||
dns=$(ts_status_field 's.Self && s.Self.DNSName')
|
if command -v node &>/dev/null; then
|
||||||
|
dns=$(ts_status_field 's.Self && s.Self.DNSName')
|
||||||
|
else
|
||||||
|
dns=$(ts_cmd status --json 2>/dev/null | sed -n 's/^ *"DNSName": *"\([^"]*\)".*/\1/p' | head -1 || true)
|
||||||
|
fi
|
||||||
printf '%s' "${dns%.}"
|
printf '%s' "${dns%.}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1949,7 +1995,7 @@ ts_sanitize_name() {
|
|||||||
detect_tailscale_serve_url() {
|
detect_tailscale_serve_url() {
|
||||||
check_tailscale || return 0
|
check_tailscale || return 0
|
||||||
command -v node &>/dev/null || return 0
|
command -v node &>/dev/null || return 0
|
||||||
[[ "$(ts_status_field 's.BackendState')" == "Running" ]] || return 0
|
[[ "$(ts_backend_state)" == "Running" ]] || return 0
|
||||||
local mapping
|
local mapping
|
||||||
mapping=$(ts_serve_find_port_mapping "${CODEMAN_PORT:-3000}")
|
mapping=$(ts_serve_find_port_mapping "${CODEMAN_PORT:-3000}")
|
||||||
[[ -n "$mapping" ]] || return 0
|
[[ -n "$mapping" ]] || return 0
|
||||||
@@ -2032,7 +2078,7 @@ offer_install_tailscale() {
|
|||||||
|
|
||||||
ensure_tailscale_login() {
|
ensure_tailscale_login() {
|
||||||
local state
|
local state
|
||||||
state=$(ts_status_field 's.BackendState')
|
state=$(ts_backend_state)
|
||||||
if [[ "$state" == "Running" ]]; then
|
if [[ "$state" == "Running" ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -2063,8 +2109,7 @@ ensure_tailscale_login() {
|
|||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if [[ "$(ts_status_field 's.BackendState')" == "Running" ]]; then
|
if [[ "$(ts_backend_state)" == "Running" ]]; then
|
||||||
TS_JOINED_HERE="1"
|
|
||||||
success "Connected to your tailnet as $(ts_node_name)"
|
success "Connected to your tailnet as $(ts_node_name)"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -2106,8 +2151,27 @@ ensure_tailscale_operator() {
|
|||||||
# browser, and the old "re-check now?" question was one more thing to answer);
|
# browser, and the old "re-check now?" question was one more thing to answer);
|
||||||
# opens the admin page where there is a browser to open it in.
|
# opens the admin page where there is a browser to open it in.
|
||||||
ensure_tailnet_https() {
|
ensure_tailnet_https() {
|
||||||
|
# Ctrl+C during the poll used to end the whole installer (the only trap
|
||||||
|
# was EXIT) while the prompt said "give up", and the user re-answered every
|
||||||
|
# question on the retry. The INT trap is armed for the poll only and
|
||||||
|
# restored on every way out; the poll then returns 1, which lands on the
|
||||||
|
# retrofit hint like any other Tailscale fallback.
|
||||||
|
local rc=0
|
||||||
|
TS_HTTPS_POLL_INTERRUPTED="0"
|
||||||
|
trap 'TS_HTTPS_POLL_INTERRUPTED=1' INT
|
||||||
|
tailnet_https_poll || rc=$?
|
||||||
|
trap - INT
|
||||||
|
return "$rc"
|
||||||
|
}
|
||||||
|
|
||||||
|
tailnet_https_poll() {
|
||||||
local waited=0 printed="0" magic cert
|
local waited=0 printed="0" magic cert
|
||||||
while true; do
|
while true; do
|
||||||
|
if [[ "$TS_HTTPS_POLL_INTERRUPTED" == "1" ]]; then
|
||||||
|
echo "" >&2
|
||||||
|
warn "Interrupted; skipping Tailscale for this run."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
magic=$(ts_status_field 's.CurrentTailnet && s.CurrentTailnet.MagicDNSEnabled ? "1" : ""')
|
magic=$(ts_status_field 's.CurrentTailnet && s.CurrentTailnet.MagicDNSEnabled ? "1" : ""')
|
||||||
cert=$(ts_status_field 'Array.isArray(s.CertDomains) && s.CertDomains.length > 0 ? "1" : ""')
|
cert=$(ts_status_field 'Array.isArray(s.CertDomains) && s.CertDomains.length > 0 ? "1" : ""')
|
||||||
if [[ "$magic" == "1" && "$cert" == "1" ]]; then
|
if [[ "$magic" == "1" && "$cert" == "1" ]]; then
|
||||||
@@ -2135,7 +2199,7 @@ ensure_tailnet_https() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
open_in_browser "https://login.tailscale.com/admin/dns"
|
open_in_browser "https://login.tailscale.com/admin/dns"
|
||||||
echo -e " ${DIM}Waiting for the toggle (checking every 5 s; Ctrl+C to give up)${NC}" >&2
|
echo -e " ${DIM}Waiting for the toggle (checking every 5 s; Ctrl+C skips Tailscale for this run)${NC}" >&2
|
||||||
fi
|
fi
|
||||||
if [[ "$waited" -ge 300 ]]; then
|
if [[ "$waited" -ge 300 ]]; then
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
@@ -2144,7 +2208,8 @@ ensure_tailnet_https() {
|
|||||||
fi
|
fi
|
||||||
waited=0
|
waited=0
|
||||||
fi
|
fi
|
||||||
sleep 5
|
# A Ctrl+C lands in this sleep; the trap only records it.
|
||||||
|
sleep 5 || true
|
||||||
waited=$((waited + 5))
|
waited=$((waited + 5))
|
||||||
printf '.' >&2
|
printf '.' >&2
|
||||||
done
|
done
|
||||||
@@ -2450,11 +2515,16 @@ sync_service_base_url() {
|
|||||||
BIND_PASSWORD="$EXISTING_PASSWORD"
|
BIND_PASSWORD="$EXISTING_PASSWORD"
|
||||||
BIND_ACK="$EXISTING_ACK"
|
BIND_ACK="$EXISTING_ACK"
|
||||||
info "Updating the service to run under ${BIND_BASE_URL:-/} ..."
|
info "Updating the service to run under ${BIND_BASE_URL:-/} ..."
|
||||||
|
local rc=0
|
||||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
setup_launchd_service
|
setup_launchd_service || rc=$?
|
||||||
else
|
else
|
||||||
setup_systemd_service
|
setup_systemd_service || rc=$?
|
||||||
fi
|
fi
|
||||||
|
# The unit now carries the new sub-path: that is what the done screen and
|
||||||
|
# the next re-run read back.
|
||||||
|
[[ "$rc" -ne 0 ]] || EXISTING_BASE_URL="$BIND_BASE_URL"
|
||||||
|
return "$rc"
|
||||||
}
|
}
|
||||||
|
|
||||||
# A loopback install with Tailscale already connected but nothing fronting
|
# A loopback install with Tailscale already connected but nothing fronting
|
||||||
@@ -2471,7 +2541,7 @@ maybe_offer_tailscale_repair() {
|
|||||||
fi
|
fi
|
||||||
check_tailscale || return 0
|
check_tailscale || return 0
|
||||||
command -v node &>/dev/null || return 0
|
command -v node &>/dev/null || return 0
|
||||||
[[ "$(ts_status_field 's.BackendState')" == "Running" ]] || return 0
|
[[ "$(ts_backend_state)" == "Running" ]] || return 0
|
||||||
# Already fronting Codeman: nothing to repair.
|
# Already fronting Codeman: nothing to repair.
|
||||||
[[ -z "$(detect_tailscale_serve_url)" ]] || return 0
|
[[ -z "$(detect_tailscale_serve_url)" ]] || return 0
|
||||||
|
|
||||||
@@ -2547,7 +2617,7 @@ setup_name_subcommand() {
|
|||||||
die "node is required. Install Codeman first (run the installer without arguments)."
|
die "node is required. Install Codeman first (run the installer without arguments)."
|
||||||
fi
|
fi
|
||||||
check_tailscale || die "Tailscale is not installed. Run: bash $INSTALL_DIR/install.sh tailscale"
|
check_tailscale || die "Tailscale is not installed. Run: bash $INSTALL_DIR/install.sh tailscale"
|
||||||
if [[ "$(ts_status_field 's.BackendState')" != "Running" ]]; then
|
if [[ "$(ts_backend_state)" != "Running" ]]; then
|
||||||
die "Tailscale is not connected. Run: bash $INSTALL_DIR/install.sh tailscale"
|
die "Tailscale is not connected. Run: bash $INSTALL_DIR/install.sh tailscale"
|
||||||
fi
|
fi
|
||||||
read_existing_binding
|
read_existing_binding
|
||||||
@@ -2564,6 +2634,9 @@ setup_name_subcommand() {
|
|||||||
fi
|
fi
|
||||||
if [[ "$TS_MAPPING_REMOVED_BY_RENAME" == "1" ]]; then
|
if [[ "$TS_MAPPING_REMOVED_BY_RENAME" == "1" ]]; then
|
||||||
if tailscale_choose_mapping; then
|
if tailscale_choose_mapping; then
|
||||||
|
# The shape can change across a rename (a root mapping freed :443,
|
||||||
|
# or the user picks a port this time), and the unit must follow.
|
||||||
|
sync_service_base_url || true
|
||||||
TS_READY="1"
|
TS_READY="1"
|
||||||
tailscale_apply || true
|
tailscale_apply || true
|
||||||
if codeman_answers_locally; then
|
if codeman_answers_locally; then
|
||||||
@@ -2575,7 +2648,8 @@ setup_name_subcommand() {
|
|||||||
BIND_HOST="${EXISTING_HOST:-127.0.0.1}"
|
BIND_HOST="${EXISTING_HOST:-127.0.0.1}"
|
||||||
BIND_PASSWORD="$EXISTING_PASSWORD"
|
BIND_PASSWORD="$EXISTING_PASSWORD"
|
||||||
BIND_ACK="$EXISTING_ACK"
|
BIND_ACK="$EXISTING_ACK"
|
||||||
BIND_BASE_URL="$EXISTING_BASE_URL"
|
# With no service on disk the sub-path lives only in this run's choice.
|
||||||
|
[[ -n "$BIND_BASE_URL" ]] || BIND_BASE_URL="$EXISTING_BASE_URL"
|
||||||
print_done_screen "" ""
|
print_done_screen "" ""
|
||||||
print_security_notice
|
print_security_notice
|
||||||
}
|
}
|
||||||
@@ -3013,13 +3087,10 @@ main() {
|
|||||||
# Source profile to pick up PATH changes, then exec codeman
|
# Source profile to pick up PATH changes, then exec codeman
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
source "$profile" 2>/dev/null || true
|
source "$profile" 2>/dev/null || true
|
||||||
if [[ -n "$BIND_HOST" ]]; then
|
export_bind_env
|
||||||
export CODEMAN_HOST="$BIND_HOST"
|
# exec skips the EXIT trap: end the sudo keepalive here, or it keeps
|
||||||
[[ -n "$BIND_PASSWORD" ]] && export CODEMAN_PASSWORD="$BIND_PASSWORD"
|
# refreshing the sudo timestamp for as long as the server runs.
|
||||||
[[ "$BIND_ACK" == "1" ]] && export CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1
|
stop_background_helpers
|
||||||
fi
|
|
||||||
[[ -n "$BIND_BASE_URL" ]] && export CODEMAN_BASE_URL="$BIND_BASE_URL"
|
|
||||||
[[ -n "${CODEMAN_PORT:-}" ]] && export CODEMAN_PORT
|
|
||||||
exec node "$INSTALL_DIR/dist/index.js" web
|
exec node "$INSTALL_DIR/dist/index.js" web
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -3045,8 +3116,11 @@ preflight_detect() {
|
|||||||
TS_STATE="absent"
|
TS_STATE="absent"
|
||||||
if check_tailscale; then
|
if check_tailscale; then
|
||||||
TS_STATE="installed"
|
TS_STATE="installed"
|
||||||
if command -v node &>/dev/null && [[ "$(ts_status_field 's.BackendState')" == "Running" ]]; then
|
if [[ "$(ts_backend_state)" == "Running" ]]; then
|
||||||
TS_STATE="connected"
|
TS_STATE="connected"
|
||||||
|
# "serving" needs the serve-status parser, and that needs node
|
||||||
|
# (installed by ask_dependencies when missing); the menu hint in
|
||||||
|
# choose_network_binding re-checks once it is there.
|
||||||
if [[ -n "$(detect_tailscale_serve_url)" ]]; then
|
if [[ -n "$(detect_tailscale_serve_url)" ]]; then
|
||||||
TS_STATE="serving"
|
TS_STATE="serving"
|
||||||
fi
|
fi
|
||||||
@@ -3252,6 +3326,37 @@ install_symlink() {
|
|||||||
# Phase 3: done
|
# Phase 3: done
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# The environment a hand-started `codeman web` needs in order to match what
|
||||||
|
# this run chose: every non-default value, composed in ONE place so the done
|
||||||
|
# screen's Start line and the exec branch of main() cannot disagree (the Start
|
||||||
|
# line used to print a bare `codeman web` under a URL that carried a sub-path
|
||||||
|
# and a port). start_command_hint prints the line with a placeholder for the
|
||||||
|
# password; export_bind_env exports the real values for the exec.
|
||||||
|
start_command_hint() {
|
||||||
|
local env=""
|
||||||
|
if [[ -n "$BIND_HOST" && "$BIND_HOST" != "127.0.0.1" ]]; then
|
||||||
|
env="CODEMAN_HOST=$BIND_HOST"
|
||||||
|
[[ -n "$BIND_PASSWORD" ]] && env="$env CODEMAN_PASSWORD='<your-password>'"
|
||||||
|
[[ "$BIND_ACK" == "1" ]] && env="$env CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1"
|
||||||
|
fi
|
||||||
|
[[ -n "$BIND_BASE_URL" ]] && env="${env:+$env }CODEMAN_BASE_URL=$BIND_BASE_URL"
|
||||||
|
if [[ -n "${CODEMAN_PORT:-}" && "$CODEMAN_PORT" != "3000" ]]; then
|
||||||
|
env="${env:+$env }CODEMAN_PORT=$CODEMAN_PORT"
|
||||||
|
fi
|
||||||
|
printf '%s' "${env:+$env }codeman web"
|
||||||
|
}
|
||||||
|
|
||||||
|
export_bind_env() {
|
||||||
|
if [[ -n "$BIND_HOST" ]]; then
|
||||||
|
export CODEMAN_HOST="$BIND_HOST"
|
||||||
|
[[ -n "$BIND_PASSWORD" ]] && export CODEMAN_PASSWORD="$BIND_PASSWORD"
|
||||||
|
[[ "$BIND_ACK" == "1" ]] && export CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1
|
||||||
|
fi
|
||||||
|
[[ -n "$BIND_BASE_URL" ]] && export CODEMAN_BASE_URL="$BIND_BASE_URL"
|
||||||
|
[[ -n "${CODEMAN_PORT:-}" ]] && export CODEMAN_PORT
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# A QR code of the URL, for the phone in the user's hand. Uses the qrcode
|
# A QR code of the URL, for the phone in the user's hand. Uses the qrcode
|
||||||
# package Codeman itself depends on, so nothing extra is installed; skipped on
|
# package Codeman itself depends on, so nothing extra is installed; skipped on
|
||||||
# a terminal without color support or too narrow to draw it.
|
# a terminal without color support or too narrow to draw it.
|
||||||
@@ -3291,7 +3396,7 @@ print_done_screen() {
|
|||||||
codeman_answers_locally && running="1"
|
codeman_answers_locally && running="1"
|
||||||
|
|
||||||
# Which supervisor, when this run did not decide: whatever is on disk.
|
# Which supervisor, when this run did not decide: whatever is on disk.
|
||||||
local svc="$SERVICE_TYPE"
|
local svc="$SERVICE_TYPE" this_run="$launch"
|
||||||
if [[ -z "$launch" ]]; then
|
if [[ -z "$launch" ]]; then
|
||||||
svc=""
|
svc=""
|
||||||
if [[ -f "$unit" ]]; then svc="systemd"
|
if [[ -f "$unit" ]]; then svc="systemd"
|
||||||
@@ -3310,7 +3415,11 @@ print_done_screen() {
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}${BOLD}============================================================${NC}"
|
echo -e "${GREEN}${BOLD}============================================================${NC}"
|
||||||
if [[ "$running" == "1" ]]; then
|
if [[ "$running" == "1" && -n "$this_run" && "$svc" == "launchd-daemon" ]]; then
|
||||||
|
# This run built a new dist/ but left the daemon alone, so what answers
|
||||||
|
# on the port is still the previous build.
|
||||||
|
echo -e "${GREEN}${BOLD} Codeman${version:+ $version} is built${NC} ${DIM}(the LaunchDaemon still runs the previous build until restarted)${NC}"
|
||||||
|
elif [[ "$running" == "1" ]]; then
|
||||||
echo -e "${GREEN}${BOLD} Codeman${version:+ $version} is running${NC}"
|
echo -e "${GREEN}${BOLD} Codeman${version:+ $version} is running${NC}"
|
||||||
else
|
else
|
||||||
echo -e "${GREEN}${BOLD} Codeman${version:+ $version} is installed${NC} ${DIM}(not running yet)${NC}"
|
echo -e "${GREEN}${BOLD} Codeman${version:+ $version} is installed${NC} ${DIM}(not running yet)${NC}"
|
||||||
@@ -3344,7 +3453,8 @@ print_done_screen() {
|
|||||||
echo -e " ${DIM}A LaunchAgent starts after you log in to this Mac. For a headless Mac see the wiki (Running As A Service).${NC}"
|
echo -e " ${DIM}A LaunchAgent starts after you log in to this Mac. For a headless Mac see the wiki (Running As A Service).${NC}"
|
||||||
;;
|
;;
|
||||||
launchd-daemon)
|
launchd-daemon)
|
||||||
echo -e " ${BOLD}Manage${NC} ${CYAN}sudo launchctl print system/com.codeman.web${NC} # the LaunchDaemon that supervises it"
|
echo -e " ${BOLD}Manage${NC} ${CYAN}sudo launchctl kickstart -k system/com.codeman.web${NC} # restart (picks up a new build)"
|
||||||
|
echo -e " ${CYAN}sudo launchctl print system/com.codeman.web${NC} # the LaunchDaemon that supervises it"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e " ${BOLD}Manage${NC} ${CYAN}systemctl --user restart codeman-web${NC} ${CYAN}journalctl --user -u codeman-web -f${NC}"
|
echo -e " ${BOLD}Manage${NC} ${CYAN}systemctl --user restart codeman-web${NC} ${CYAN}journalctl --user -u codeman-web -f${NC}"
|
||||||
@@ -3352,17 +3462,15 @@ print_done_screen() {
|
|||||||
esac
|
esac
|
||||||
elif [[ "$launch" == "2" ]]; then
|
elif [[ "$launch" == "2" ]]; then
|
||||||
echo -e " ${YELLOW}${BOLD}The service was set up but is not running yet${NC} (see the warnings above)."
|
echo -e " ${YELLOW}${BOLD}The service was set up but is not running yet${NC} (see the warnings above)."
|
||||||
echo -e " ${DIM}You can always run it directly:${NC} ${CYAN}codeman web${NC}"
|
echo -e " ${DIM}You can always run it directly:${NC} ${CYAN}$(start_command_hint)${NC}"
|
||||||
elif [[ "$launch" == "3" ]]; then
|
elif [[ "$launch" == "3" ]]; then
|
||||||
|
echo -e " ${BOLD}Start${NC} ${CYAN}$(start_command_hint)${NC}"
|
||||||
if [[ "$BIND_HOST" == "0.0.0.0" ]]; then
|
if [[ "$BIND_HOST" == "0.0.0.0" ]]; then
|
||||||
if [[ -n "$BIND_PASSWORD" ]]; then
|
|
||||||
echo -e " ${BOLD}Start${NC} ${CYAN}CODEMAN_HOST=0.0.0.0 CODEMAN_PASSWORD='<your-password>' codeman web${NC}"
|
|
||||||
else
|
|
||||||
echo -e " ${BOLD}Start${NC} ${CYAN}CODEMAN_HOST=0.0.0.0 codeman web${NC}"
|
|
||||||
fi
|
|
||||||
echo -e " ${DIM}(a bare 'codeman web' binds 127.0.0.1, this machine only)${NC}"
|
echo -e " ${DIM}(a bare 'codeman web' binds 127.0.0.1, this machine only)${NC}"
|
||||||
|
elif [[ "$(start_command_hint)" == "codeman web" ]]; then
|
||||||
|
echo -e " ${DIM}(or: codeman web -d to detach; codeman service install for boot)${NC}"
|
||||||
else
|
else
|
||||||
echo -e " ${BOLD}Start${NC} ${CYAN}codeman web${NC} ${DIM}(or: codeman web -d to detach; codeman service install for boot)${NC}"
|
echo -e " ${DIM}(the same variables apply to: codeman web -d)${NC}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo -e " ${BOLD}Update${NC} re-run the install line, or App Settings -> System -> Updates"
|
echo -e " ${BOLD}Update${NC} re-run the install line, or App Settings -> System -> Updates"
|
||||||
@@ -3433,6 +3541,11 @@ update() {
|
|||||||
launchctl unload "$agent_plist" 2>/dev/null || true
|
launchctl unload "$agent_plist" 2>/dev/null || true
|
||||||
launchctl load "$agent_plist" 2>/dev/null || true
|
launchctl load "$agent_plist" 2>/dev/null || true
|
||||||
success "LaunchAgent restarted"
|
success "LaunchAgent restarted"
|
||||||
|
elif [[ -f "/Library/LaunchDaemons/com.codeman.web.plist" ]]; then
|
||||||
|
# Left alone on purpose (see setup_launchd_service); it keeps running
|
||||||
|
# the previous build until its owner restarts it.
|
||||||
|
info "A system LaunchDaemon supervises Codeman; restart it to run the new build:"
|
||||||
|
echo -e " ${CYAN}sudo launchctl kickstart -k system/com.codeman.web${NC}"
|
||||||
else
|
else
|
||||||
echo -e " ${DIM}Restart codeman web to use the new version:${NC}"
|
echo -e " ${DIM}Restart codeman web to use the new version:${NC}"
|
||||||
echo -e " ${CYAN}codeman web --stop; codeman web -d${NC}"
|
echo -e " ${CYAN}codeman web --stop; codeman web -d${NC}"
|
||||||
@@ -3453,6 +3566,9 @@ update() {
|
|||||||
# chance at remote access; the fresh-install path asks outright.
|
# chance at remote access; the fresh-install path asks outright.
|
||||||
maybe_offer_tailscale_repair
|
maybe_offer_tailscale_repair
|
||||||
|
|
||||||
|
# The re-run is how everyone updates, and the first thing people try when
|
||||||
|
# they want the URL back: end on the same screen the install ends on.
|
||||||
|
print_done_screen "" ""
|
||||||
print_security_notice
|
print_security_notice
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3488,9 +3604,18 @@ uninstall() {
|
|||||||
success "Removed LaunchAgent"
|
success "Removed LaunchAgent"
|
||||||
fi
|
fi
|
||||||
if [[ -f "$daemon_plist" ]]; then
|
if [[ -f "$daemon_plist" ]]; then
|
||||||
sudo launchctl unload "$daemon_plist" 2>/dev/null || true
|
# This installer never writes a LaunchDaemon (setup_launchd_service
|
||||||
sudo rm -f "$daemon_plist"
|
# leaves one alone), so this one is the user's own headless-Mac setup:
|
||||||
success "Removed LaunchDaemon"
|
# ask before touching it. The default stays yes, because a daemon left
|
||||||
|
# pointing at a removed install restarts into failure every 10 s.
|
||||||
|
warn "A system LaunchDaemon supervises Codeman ($daemon_plist); this installer did not write it."
|
||||||
|
if prompt_yes_no "Remove that LaunchDaemon too (needs sudo)?" "y"; then
|
||||||
|
sudo launchctl unload "$daemon_plist" 2>/dev/null || true
|
||||||
|
sudo rm -f "$daemon_plist"
|
||||||
|
success "Removed LaunchDaemon"
|
||||||
|
else
|
||||||
|
info "Kept $daemon_plist. Remove it later with: sudo launchctl unload $daemon_plist && sudo rm $daemon_plist"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Remove OUR tailscale serve mapping (whatever shape it has) only. Other
|
# Remove OUR tailscale serve mapping (whatever shape it has) only. Other
|
||||||
@@ -3607,7 +3732,9 @@ EOF
|
|||||||
# function below reads one source of truth. A flag that changes how an existing
|
# function below reads one source of truth. A flag that changes how an existing
|
||||||
# install is reached or run also flips RECONFIGURE, so a bare re-run takes the
|
# install is reached or run also flips RECONFIGURE, so a bare re-run takes the
|
||||||
# full flow (which re-asks nothing the flag already answered) instead of the
|
# full flow (which re-asks nothing the flag already answered) instead of the
|
||||||
# quiet update.
|
# quiet update. A password or a port lives in the unit, so those two reconfigure
|
||||||
|
# as well: the quiet update never rewrites the unit and used to drop them
|
||||||
|
# silently.
|
||||||
parse_flags() {
|
parse_flags() {
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -3627,13 +3754,13 @@ parse_flags() {
|
|||||||
--password)
|
--password)
|
||||||
shift
|
shift
|
||||||
[[ $# -gt 0 ]] || die "--password needs a value"
|
[[ $# -gt 0 ]] || die "--password needs a value"
|
||||||
CODEMAN_PASSWORD="$1" ;;
|
CODEMAN_PASSWORD="$1"; RECONFIGURE="1" ;;
|
||||||
--password=*) CODEMAN_PASSWORD="${1#--password=}" ;;
|
--password=*) CODEMAN_PASSWORD="${1#--password=}"; RECONFIGURE="1" ;;
|
||||||
--port)
|
--port)
|
||||||
shift
|
shift
|
||||||
[[ $# -gt 0 ]] || die "--port needs a value"
|
[[ $# -gt 0 ]] || die "--port needs a value"
|
||||||
CODEMAN_PORT="$1"; export CODEMAN_PORT ;;
|
CODEMAN_PORT="$1"; export CODEMAN_PORT; RECONFIGURE="1" ;;
|
||||||
--port=*) CODEMAN_PORT="${1#--port=}"; export CODEMAN_PORT ;;
|
--port=*) CODEMAN_PORT="${1#--port=}"; export CODEMAN_PORT; RECONFIGURE="1" ;;
|
||||||
--help|-h) usage; exit 0 ;;
|
--help|-h) usage; exit 0 ;;
|
||||||
update|uninstall|tailscale|name|status|cloudflared)
|
update|uninstall|tailscale|name|status|cloudflared)
|
||||||
[[ -z "$SUBCOMMAND" ]] || die "Only one subcommand at a time ($SUBCOMMAND and $1 given)."
|
[[ -z "$SUBCOMMAND" ]] || die "Only one subcommand at a time ($SUBCOMMAND and $1 given)."
|
||||||
|
|||||||
@@ -407,3 +407,127 @@ describe('install.sh detect_all_clis and a disabled entry', () => {
|
|||||||
expect(run.stdout).toContain('found=0');
|
expect(run.stdout).toContain('found=0');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('install.sh review fixes for #460', () => {
|
||||||
|
// Each pin here is a finding from the two reviews of PR #460 (the DeepSeek Harness
|
||||||
|
// pass, then the Claude pass), kept as a static guard so the fix cannot quietly rot.
|
||||||
|
const fn = (name: string, until: string) => {
|
||||||
|
const start = SOURCE.indexOf(`${name}() {`);
|
||||||
|
expect(start, `${name}() missing`).toBeGreaterThan(-1);
|
||||||
|
const end = SOURCE.indexOf(until, start);
|
||||||
|
expect(end, `${until} missing after ${name}()`).toBeGreaterThan(start);
|
||||||
|
return SOURCE.slice(start, end);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('keeps an existing password on the flag and env preset paths', () => {
|
||||||
|
// `--lan --service` on a unit that carried a password used to rewrite it without the
|
||||||
|
// password and with the unauthenticated ack; `--tailscale` dropped it the same way.
|
||||||
|
const body = fn('choose_network_binding', 'get_tailscale_path() {');
|
||||||
|
expect(body.match(/BIND_PASSWORD="\$\{CODEMAN_PASSWORD:-\$EXISTING_PASSWORD\}"/g)?.length).toBe(2);
|
||||||
|
expect(body).not.toMatch(/BIND_PASSWORD="\$\{CODEMAN_PASSWORD:-\}"/);
|
||||||
|
// The presets can only keep what was read, so the read comes first.
|
||||||
|
expect(body.indexOf('read_existing_binding')).toBeLessThan(body.indexOf('CODEMAN_HOST:-'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('composes the hand-start environment in one place', () => {
|
||||||
|
// "Do not start" under a sub-path or a custom port used to print a bare `codeman web`
|
||||||
|
// under URLs that carried both.
|
||||||
|
const hint = fn('start_command_hint', 'export_bind_env() {');
|
||||||
|
const exported = fn('export_bind_env', '# A QR code of the URL');
|
||||||
|
for (const key of [
|
||||||
|
'CODEMAN_HOST',
|
||||||
|
'CODEMAN_PASSWORD',
|
||||||
|
'CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK',
|
||||||
|
'CODEMAN_BASE_URL',
|
||||||
|
'CODEMAN_PORT',
|
||||||
|
]) {
|
||||||
|
expect(hint, `${key} missing from start_command_hint`).toContain(key);
|
||||||
|
expect(exported, `${key} missing from export_bind_env`).toContain(key);
|
||||||
|
}
|
||||||
|
const done = fn('print_done_screen', '\nupdate() {');
|
||||||
|
expect(done).toContain('$(start_command_hint)');
|
||||||
|
expect(done).not.toMatch(/CODEMAN_HOST=0\.0\.0\.0 codeman web/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flips RECONFIGURE for --password and --port', () => {
|
||||||
|
// Neither used to, so on a completed install both took the quiet update path, which
|
||||||
|
// never rewrites the unit: the password never landed and the port stayed at 3000.
|
||||||
|
const parse = fn('parse_flags', '# Sourcing guard');
|
||||||
|
for (const label of ['--password)', '--password=*)', '--port)', '--port=*)']) {
|
||||||
|
const at = parse.indexOf(label);
|
||||||
|
expect(at, `${label} missing`).toBeGreaterThan(-1);
|
||||||
|
expect(parse.slice(at, parse.indexOf(';;', at)), `${label} does not reconfigure`).toContain('RECONFIGURE="1"');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ends the sudo keepalive and exports the binding before the exec', () => {
|
||||||
|
// exec skips the EXIT trap, and the keepalive keys on $$, which becomes the server's
|
||||||
|
// pid: it refreshed the sudo timestamp for the server's whole life.
|
||||||
|
const execAt = SOURCE.indexOf('exec node "$INSTALL_DIR/dist/index.js" web');
|
||||||
|
expect(execAt).toBeGreaterThan(-1);
|
||||||
|
const before = SOURCE.slice(SOURCE.lastIndexOf('source "$profile"', execAt), execAt);
|
||||||
|
expect(before).toContain('export_bind_env');
|
||||||
|
expect(before).toContain('stop_background_helpers');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets Ctrl+C skip the HTTPS-toggle poll instead of ending the run', () => {
|
||||||
|
const body = fn('ensure_tailnet_https', 'tailnet_https_poll() {');
|
||||||
|
expect(body).toMatch(/trap '[^']*' INT/);
|
||||||
|
expect(body).toContain('trap - INT');
|
||||||
|
expect(fn('tailnet_https_poll', '# Everything Tailscale that needs a human')).toContain('sleep 5 || true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('asks before removing a LaunchDaemon it never wrote', () => {
|
||||||
|
const body = fn('uninstall', '\nusage() {');
|
||||||
|
const ask = body.indexOf('prompt_yes_no "Remove that LaunchDaemon too');
|
||||||
|
expect(ask).toBeGreaterThan(-1);
|
||||||
|
expect(body.indexOf('sudo rm -f "$daemon_plist"')).toBeGreaterThan(ask);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('re-syncs the unit after `install.sh name` re-adds the mapping, and ends an update on the done screen', () => {
|
||||||
|
const name = fn('setup_name_subcommand', '\nstatus_subcommand() {');
|
||||||
|
expect(name.indexOf('sync_service_base_url')).toBeGreaterThan(name.indexOf('tailscale_choose_mapping'));
|
||||||
|
expect(name.indexOf('sync_service_base_url')).toBeLessThan(name.indexOf('tailscale_apply'));
|
||||||
|
expect(fn('update', '\nuninstall() {')).toContain('print_done_screen "" ""');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads the Tailscale state in the preflight without node, and no longer records TS_JOINED_HERE', () => {
|
||||||
|
const preflight = fn('preflight_detect', '\nprint_preflight_summary() {');
|
||||||
|
expect(preflight).toContain('ts_backend_state');
|
||||||
|
expect(preflight).not.toContain('command -v node');
|
||||||
|
expect(fn('ts_backend_state', '\nts_dns_name() {')).toContain('sed -n');
|
||||||
|
expect(SOURCE).not.toContain('TS_JOINED_HERE');
|
||||||
|
expect(CODE).not.toContain('at port 3000');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drives the kept password and the start line in a real bash', () => {
|
||||||
|
const DRIVER = `
|
||||||
|
set -euo pipefail
|
||||||
|
export CODEMAN_INSTALL_SH_LIB=1
|
||||||
|
. "$1"
|
||||||
|
read_existing_binding() { EXISTING_FOUND=1; EXISTING_HOST=0.0.0.0; EXISTING_PASSWORD=s3cret; EXISTING_ACK=0; EXISTING_BASE_URL=""; }
|
||||||
|
tailscale_prepare() { return 0; }
|
||||||
|
parse_flags $DRIVE_FLAGS
|
||||||
|
choose_network_binding >/dev/null 2>&1
|
||||||
|
echo "host=$BIND_HOST pw=$BIND_PASSWORD ack=$BIND_ACK"
|
||||||
|
BIND_HOST=0.0.0.0; BIND_PASSWORD=x; BIND_ACK=0; BIND_BASE_URL=/codeman; CODEMAN_PORT=4000
|
||||||
|
echo "hint=$(start_command_hint)"
|
||||||
|
BIND_HOST=127.0.0.1; BIND_PASSWORD=""; BIND_BASE_URL=""; CODEMAN_PORT=""
|
||||||
|
echo "bare=$(start_command_hint)"
|
||||||
|
`;
|
||||||
|
const drive = (flags: string) => {
|
||||||
|
const env = { ...process.env, DRIVE_FLAGS: flags };
|
||||||
|
delete env.CODEMAN_PASSWORD;
|
||||||
|
const result = spawnSync('bash', ['-c', DRIVER, 'bash', INSTALL_SH], { encoding: 'utf-8', timeout: 30_000, env });
|
||||||
|
expect(result.status, result.stderr).toBe(0);
|
||||||
|
return result.stdout;
|
||||||
|
};
|
||||||
|
const lan = drive('--lan');
|
||||||
|
expect(lan).toContain('host=0.0.0.0 pw=s3cret ack=0');
|
||||||
|
expect(lan).toContain(
|
||||||
|
"hint=CODEMAN_HOST=0.0.0.0 CODEMAN_PASSWORD='<your-password>' CODEMAN_BASE_URL=/codeman CODEMAN_PORT=4000 codeman web"
|
||||||
|
);
|
||||||
|
expect(lan).toContain('bare=codeman web');
|
||||||
|
expect(drive('--tailscale')).toContain('host=127.0.0.1 pw=s3cret ack=0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user