@atomiton-dev/gap-cli 0.0.0-stage → 0.9.0-mvp

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,3 +1,133 @@
1
- # Temporary Holding Version
1
+ # @atomiton-dev/gap-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Installs and runs the GAP platform on your own machine with Docker. Your data is kept when you run
4
+ the installer again.
5
+
6
+ ## What you need
7
+
8
+ - Node.js 18 or newer.
9
+ - Docker, installed **and running**: Docker Desktop on macOS and Windows; Docker Engine with the Compose
10
+ plugin (v2) on Linux. On Linux your user must be able to run `docker` without `sudo`, and `curl` must be
11
+ installed.
12
+ - On macOS and Linux, `bash`. On Windows, Windows PowerShell 5.1.
13
+ - Access to GAP's images (see "Get access to the images" below). They are private.
14
+
15
+ ## Install the command
16
+
17
+ ```
18
+ npm install -g @atomiton-dev/gap-cli
19
+ ```
20
+
21
+ This puts the `gap` command on your computer. It does not download GAP yet.
22
+
23
+ If you use oh-my-zsh, the install also adds one small file, `~/.oh-my-zsh/custom/atomiton-gap.zsh`, so that a new
24
+ terminal runs `gap` and not oh-my-zsh's `alias gap='git apply'` (which would answer "can't open patch"). It turns
25
+ off only that one alias, only while this package is installed (with several Node versions it follows every copy
26
+ that is still installed), and changes nothing else; delete the file to undo it. It holds a path on this machine, so do
27
+ not copy it to another one. Open a new terminal (or run `exec zsh`) after the install. This step writes nothing else
28
+ outside the package.
29
+
30
+ ## Get access to the images
31
+
32
+ GAP's images are stored in a private registry (ghcr.io). You need a GitHub account that has been given access.
33
+
34
+ 1. Ask your contact at Atomiton to add your GitHub account to the GAP customers group. GitHub emails you an
35
+ invitation; accept it.
36
+ 2. Create a personal access token (classic) that has the `read:packages` permission. This link selects the
37
+ permission for you: <https://github.com/settings/tokens/new?scopes=read:packages>. Nobody else can create the
38
+ token for you.
39
+ 3. Log in to the registry:
40
+
41
+ ```
42
+ gap login
43
+ ```
44
+
45
+ It asks for your GitHub username and the token (the token is hidden as you type), logs Docker in to ghcr.io, and then
46
+ checks that your account can read GAP's images. It needs a terminal, and it exits with status 0 only when you are logged
47
+ in and can read the images. `gap` does not store the token anywhere: Docker keeps the login, in the system's credential
48
+ store when it has one (for example the macOS keychain). Where it has none, as on many Linux
49
+ machines, Docker stores it in `~/.docker/config.json` and says so. If you prefer Docker's own command,
50
+ `docker login ghcr.io -u <your GitHub username>` does the same login (paste the token when it asks for the password);
51
+ `gap doctor` then checks the access. (If `gap` still runs another program, see "If something goes wrong"; `command gap login` works meanwhile.)
52
+
53
+ You do this once per machine. When the token expires, create a new one and run `gap login` again.
54
+
55
+ ## Install GAP
56
+
57
+ ```
58
+ gap install
59
+ ```
60
+
61
+ `gap install` pulls the images, starts GAP and prints the address of the Admin UI
62
+ (`http://127.0.0.1:<port>`, a port between 8061 and 8070) and the sign-in details. It is safe to run again at
63
+ any time: your data and password are kept. If it cannot pull the images it stops before changing anything on
64
+ your machine and tells you what to do.
65
+
66
+ ## Update
67
+
68
+ ```
69
+ npm install -g @atomiton-dev/gap-cli@latest
70
+ gap update
71
+ ```
72
+
73
+ `gap update` moves GAP to the version of the package you just installed. In this
74
+ order: it pulls the new images, makes a backup of your database and your app files in
75
+ `~/.gap/backups/<date and time>` (readable by you only; on Windows it has the permissions of your user folder), and only then starts the new version. If pulling or the backup fails,
76
+ it stops and nothing has changed. It does not go back to an older version, and it does nothing when you are already on
77
+ the version of the package. If the new version does not start, it undoes nothing by itself: it prints where the backup
78
+ is and the commands to go back to your old version with your data. It never deletes a backup.
79
+ `gap install` does not go back to an older version either: it stops and says so. `gap uninstall` keeps the `backups` folder
80
+ (`gap uninstall --purge` erases it with everything else).
81
+
82
+ ## Commands
83
+
84
+ | Command | What it does |
85
+ |---|---|
86
+ | `gap login` | asks for your GitHub username and token, logs in to the image registry, and checks that you can read the images |
87
+ | `gap install` | installs, or installs again |
88
+ | `gap update` | moves GAP to the version of this package, after a backup |
89
+ | `gap status` | shows whether GAP is running and healthy |
90
+ | `gap start` | starts GAP again (after `gap stop` or a reboot) |
91
+ | `gap stop` | stops GAP; your data is kept |
92
+ | `gap logs [service] [-f]` | shows logs (`kernel`, `postgres` or `admin-ui`) |
93
+ | `gap version` | shows which GAP version is installed, and the version of this package |
94
+ | `gap doctor` | checks this machine and the install, and says what to do about each problem; it changes nothing |
95
+ | `gap uninstall [--purge] [--yes]` | removes GAP from this machine; your data is kept unless you add `--purge` (it asks you to type `delete`) |
96
+
97
+ `gap help` prints the list.
98
+
99
+ ## One way of installing per machine
100
+
101
+ Use either this package or the ZIP package of GAP on a machine, not both. They share the same data. If you
102
+ used the ZIP before, run `gap install`. On macOS and Linux it reports a leftover `~/.local/bin/gap` launcher
103
+ from the ZIP, which may run instead of the npm command; remove it with `rm ~/.local/bin/gap`.
104
+
105
+ ## Your data
106
+
107
+ Your data lives in two Docker volumes, `gap_pgdata` and `gap_data`. Do not run `docker volume prune` or
108
+ `docker system prune --volumes`: they can erase it.
109
+
110
+ ## If something goes wrong
111
+
112
+ Run `gap doctor` first. It checks Node.js, Docker, the Admin UI port, the `gap` command, your access to the images and
113
+ the state of GAP, and says what to do about each problem. It exits with status 1 when it finds one, and it changes
114
+ nothing.
115
+
116
+ - **`gap: command not found`** after installing: npm's global command folder is not on your `PATH`. Run
117
+ `npm prefix -g`; add that folder's `bin` subfolder (on Windows, the folder itself) to your `PATH`, then open a
118
+ new terminal.
119
+ - **`gap` fails with "can't open patch 'login'" (or another word), or runs another program:** your terminal has
120
+ its own command named `gap`. With oh-my-zsh it is `alias gap='git apply'`; the install turns it off for new
121
+ terminals, so open a new terminal (or run `exec zsh`). If it still happens, the install could not do it (for
122
+ example it ran with `--ignore-scripts`, under `sudo`, or with yarn or pnpm, which may not run it; or your
123
+ `ZSH_CUSTOM` is set only in `~/.zshrc`; or you use another framework such as prezto). Add
124
+ `unalias gap 2>/dev/null` at the end of `~/.zshrc`, or run `command gap login` each time.
125
+ `gap install` also finds this and offers to turn it off.
126
+ - **"GAP's images are private and this machine is not logged in to ghcr.io"** (`unauthorized`): follow steps 2
127
+ and 3 of "Get access to the images": create a token, then run `gap login`.
128
+ - **"ghcr.io refused access to ..."** (`denied`): either your token has expired or been revoked, or your GitHub
129
+ account has not been given access yet. Create a new token and run `gap login` again; if it still fails, accept the
130
+ invitation GitHub emailed you and ask your contact at Atomiton to check that you are in the GAP customers group.
131
+ - **"Docker daemon stopped":** start Docker Desktop (or the Docker service on Linux), wait until it is running,
132
+ then run `gap install` again.
133
+ - **Something does not start:** `gap logs kernel` (or `postgres`, `admin-ui`) shows why.
package/bin/gap.js ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // `gap`: a thin Node shell over the installer scripts (#1986, #2010, D2). It runs bash (macOS, Linux) or
4
+ // PowerShell (Windows) with the same arguments and exits with the script's exit code. ES5 only: on an old
5
+ // Node this file must still get as far as printing why it cannot go on.
6
+ var path = require('path');
7
+ var childProcess = require('child_process');
8
+ var launcher = require('../lib/launcher.js');
9
+
10
+
11
+ function die(message) {
12
+ process.stderr.write(message + '\n');
13
+ process.exit(1);
14
+ }
15
+
16
+ if (!launcher.nodeSupported(process.versions.node)) {
17
+ die('Node ' + launcher.MIN_NODE_MAJOR + ' or newer is required (found ' + process.versions.node + '). ' +
18
+ 'Install a current Node.js from https://nodejs.org and run the command again.');
19
+ }
20
+
21
+ var SIGNALS = require('os').constants.signals;
22
+ var win = process.platform === 'win32';
23
+ var command = launcher.buildCommand({
24
+ platform: process.platform,
25
+ pkgRoot: path.resolve(__dirname, '..'),
26
+ args: process.argv.slice(2)
27
+ });
28
+
29
+ var child = childProcess.spawn(command.cmd, command.args, { stdio: 'inherit' });
30
+
31
+ child.on('error', function (err) {
32
+ if (err && err.code === 'ENOENT') {
33
+ die(win
34
+ ? 'powershell is required but was not found on PATH. Use Windows PowerShell 5.1 or newer.'
35
+ : 'bash is required but was not found on PATH. Install bash and run the command again.');
36
+ }
37
+ die('Could not start ' + command.cmd + ': ' + (err && err.message ? err.message : err));
38
+ });
39
+
40
+ ['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(function (sig) {
41
+ process.on(sig, function () { child.kill(sig); });
42
+ });
43
+
44
+ child.on('exit', function (code, signal) {
45
+ if (signal) process.exit(128 + (SIGNALS[signal] || 1));
46
+ process.exit(code === null ? 1 : code);
47
+ });
@@ -0,0 +1 @@
1
+ npm
@@ -0,0 +1 @@
1
+ 0.9.0-mvp
@@ -0,0 +1,86 @@
1
+ # GAP local installer stack: postgres + kernel + admin-ui.
2
+ #
3
+ # Ships in the installer zip next to env.template, images.tar.gz and install.sh / install.ps1; the
4
+ # installer copies it to ~/.gap, where .env is written and where it runs from (D28)
5
+ # (#1750, plan .claude/plan/issue-1750/03-compose-and-config.md). Always run with `-p gap`:
6
+ # COMPOSE_PROJECT_NAME in the user's shell overrides the `name:` key below.
7
+ #
8
+ # Two channels share this file, told apart only by two keys of .env (#1986):
9
+ # ZIP (default, offline): the images are loaded from images.tar.gz (`docker load`) under their
10
+ # local names and never pulled: GAP_IMAGE_PREFIX is empty and GAP_PULL_POLICY is `never`, so a
11
+ # missing image fails with a clear error instead of reaching for Docker Hub.
12
+ # npm (online): the images are pulled from a registry: GAP_IMAGE_PREFIX is
13
+ # `ghcr.io/atomiton-gap/` and GAP_PULL_POLICY is `missing`.
14
+ # An .env without either key (every ZIP installed before #1986) behaves exactly as the ZIP channel.
15
+ # Required variables use `${VAR:?...}` so Compose stops with a message instead of running with an
16
+ # empty value.
17
+ name: gap
18
+
19
+ services:
20
+ postgres:
21
+ # Hard-coded on purpose: changing the major version breaks an existing volume. The official
22
+ # postgres:16, packaged under a GAP name so that `docker load` never moves the user's own
23
+ # postgres:16 tag (build-package.sh).
24
+ image: ${GAP_IMAGE_PREFIX:-}gap-postgres:16
25
+ pull_policy: ${GAP_PULL_POLICY:-never}
26
+ environment:
27
+ POSTGRES_USER: postgres
28
+ # Only read when the data volume is still empty (first start).
29
+ POSTGRES_PASSWORD: ${GAP_DB_PASSWORD:?GAP_DB_PASSWORD missing, re-run the installer}
30
+ volumes:
31
+ - pgdata:/var/lib/postgresql/data
32
+ healthcheck:
33
+ # -h 127.0.0.1: without it pg_isready reaches the temporary socket-only server that the
34
+ # image runs during first-time initialisation and reports ready too early.
35
+ test: ["CMD", "pg_isready", "-h", "127.0.0.1", "-U", "postgres"]
36
+ interval: 5s
37
+ timeout: 3s
38
+ retries: 30
39
+ restart: unless-stopped
40
+ # No `ports`: the database is reachable only from the other services.
41
+
42
+ kernel:
43
+ image: ${GAP_IMAGE_PREFIX:-}gap-kernel:${GAP_VERSION:?GAP_VERSION missing, re-run the installer}
44
+ pull_policy: ${GAP_PULL_POLICY:-never}
45
+ depends_on:
46
+ postgres:
47
+ condition: service_healthy
48
+ environment:
49
+ ENVIRONMENT: dev
50
+ DEBUG: "false"
51
+ # Required: without it the App UI endpoints answer 500 (main.py warns at start).
52
+ GAP_DATA_ROOT: /data/gap-data
53
+ DB_HOST: postgres
54
+ DB_PORT: "5432"
55
+ DB_USER: postgres
56
+ DB_PASSWORD: ${GAP_DB_PASSWORD:?GAP_DB_PASSWORD missing, re-run the installer}
57
+ DB_ADMIN_USER: postgres
58
+ DB_ADMIN_PASSWORD: ${GAP_DB_PASSWORD:?GAP_DB_PASSWORD missing, re-run the installer}
59
+ # Explicit: the Kernel default follows the CPU count of the machine.
60
+ OFFLOAD_MAX_WORKERS: "2"
61
+ # JWT_SECRET and APP_CREDENTIAL_ENCRYPTION_KEY are left to the Kernel defaults (D7); that is
62
+ # only acceptable while nothing but 127.0.0.1 can reach the Admin UI (D9).
63
+ volumes:
64
+ - data:/data
65
+ # The default 10 s equals worker_drain_timeout_seconds, which leaves no time for the rest
66
+ # of the shutdown.
67
+ stop_grace_period: 30s
68
+ restart: unless-stopped
69
+ # No `ports`: every API call goes through the Admin UI (nginx).
70
+
71
+ admin-ui:
72
+ image: ${GAP_IMAGE_PREFIX:-}gap-admin-ui:${GAP_VERSION:?GAP_VERSION missing, re-run the installer}
73
+ pull_policy: ${GAP_PULL_POLICY:-never}
74
+ depends_on:
75
+ kernel:
76
+ condition: service_healthy
77
+ ports:
78
+ # Loopback only: the seeded admin account and the default JWT secret are acceptable only
79
+ # because of this binding (D6, D7, D9). Never bind 0.0.0.0 without revisiting those.
80
+ - "127.0.0.1:${GAP_UI_PORT:?GAP_UI_PORT missing, re-run the installer}:8080"
81
+ restart: unless-stopped
82
+
83
+ # Real names on Docker are gap_pgdata and gap_data (project name + key).
84
+ volumes:
85
+ pgdata:
86
+ data:
@@ -0,0 +1,13 @@
1
+ # Written by the installer on first install, as .env in ~/.gap (not in the unzipped folder, D28);
2
+ # do not commit a filled-in copy. The installer owns these values and reads them back on later runs.
3
+ GAP_VERSION=@GAP_VERSION@
4
+ GAP_UI_PORT=@GAP_UI_PORT@
5
+ # Random, generated once. Postgres keeps the password it was first started with, so editing it by
6
+ # hand makes the Kernel fail to authenticate. If this file is lost, install.sh / install.ps1
7
+ # generates a new password and sets it inside Postgres, so the data is kept (D28).
8
+ GAP_DB_PASSWORD=@GAP_DB_PASSWORD@
9
+ # Which channel the images come from (#1986). The ZIP installer keeps these two as they are: images
10
+ # loaded from the zip under their local names, never pulled. The npm installer sets the registry
11
+ # prefix (ghcr.io/atomiton-gap/) and `missing`. docker-compose.yml reads them.
12
+ GAP_IMAGE_PREFIX=
13
+ GAP_PULL_POLICY=never
package/installer/gap ADDED
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env bash
2
+ # gap: status / start / stop / logs / uninstall / version / doctor (and update, npm channel) for the local GAP stack (#1750, D27, D28). In the npm package
3
+ # (#1986) `gap install` is the other script, install.sh, which the Node launcher (bin/gap.js) runs.
4
+ # install.sh copies this file to ~/.gap and puts a launcher for it on the PATH, so `gap status`
5
+ # works in any terminal. It always works on the instance in ~/.gap (GAP_HOME, from gap-lib.sh),
6
+ # also when run from the unzipped installer folder.
7
+ set -u
8
+
9
+ GAP_SRC=$(cd "$(dirname "$0")" && pwd -P)
10
+ # shellcheck source=gap-lib.sh
11
+ . "$GAP_SRC/gap-lib.sh"
12
+
13
+ usage() {
14
+ printf 'Usage: gap <command>\n\n'
15
+ # `install` and `update` are commands of the npm package only: the ZIP's gap has none, its installer is install.sh /
16
+ # install.ps1 (and a ZIP updates by running the new one).
17
+ if [ "$(channel)" = npm ]; then
18
+ printf ' install install GAP on this machine (pulls the images; run it again any time)\n'
19
+ printf ' login log in to the registry that holds the images (asks for your GitHub username and token)\n'
20
+ printf ' update move GAP to the version of the installed npm package, after a backup\n'
21
+ fi
22
+ cat <<'EOF'
23
+ status show whether GAP is running and healthy
24
+ version show the installed GAP version
25
+ doctor check this machine and the install for problems; changes nothing
26
+ start start GAP again (after `gap stop` or a reboot)
27
+ stop stop GAP; your data is kept
28
+ logs [service] [-f]
29
+ show logs (service: kernel, postgres or admin-ui)
30
+ uninstall [--purge] [--yes]
31
+ remove GAP from this machine; your data is kept
32
+ unless you add --purge (it asks you to type "delete")
33
+ EOF
34
+ }
35
+
36
+ need_install() {
37
+ [ -f "$GAP_HOME/.env" ] && return 0
38
+ warn "GAP is not installed in $GAP_HOME"
39
+ if [ "$(channel)" = npm ]; then printf 'Run: gap install\n'
40
+ elif [ -f "$GAP_SRC/install.sh" ]; then printf 'Run: %s/install.sh\n' "$GAP_SRC"
41
+ else printf 'Run install.sh from the GAP installer folder.\n'; fi
42
+ return 1
43
+ }
44
+
45
+ cmd_status() {
46
+ local pg kernel ui runtime overall=READY port
47
+ check_prereqs "gap status" || return 1
48
+ pg=$(svc_state postgres); kernel=$(svc_state kernel); ui=$(svc_state admin-ui)
49
+ # D19: the Runtime lives inside the Kernel; /system/health answering 200 proves the database
50
+ # session and the app manager are up. The `status` field is not read: it says "error" on a
51
+ # fresh install that has no app worker yet.
52
+ if [ "$kernel" = Healthy ] && [ "$ui" = Healthy ] && http_ok /system/health; then runtime=Healthy
53
+ elif [ "$kernel" = Stopped ]; then runtime=Stopped
54
+ else runtime=Unreachable; fi
55
+ for s in "$pg" "$kernel" "$runtime" "$ui"; do [ "$s" = Healthy ] || overall="NOT READY"; done
56
+ port=$(env_get GAP_UI_PORT)
57
+ printf '%-15s %s\n' "PostgreSQL" "$pg" "GAP Kernel" "$kernel" "GAP Runtime" "$runtime" "GAP Admin UI" "$ui"
58
+ printf 'Version: %s\n' "$(env_get GAP_VERSION)"
59
+ newer_package_note
60
+ [ "$overall" = READY ] && printf 'URL: http://127.0.0.1:%s\n' "$port"
61
+ printf 'Overall: %s\n' "$overall"
62
+ [ "$overall" = READY ]
63
+ }
64
+
65
+ # version and doctor work before GAP is installed: doctor is what to run when the install did not work.
66
+ cmd_version() {
67
+ local installed pkg
68
+ installed=$(installed_version); pkg=$(cli_version)
69
+ printf 'Installed: %s\n' "${installed:-not installed}"
70
+ [ -z "$pkg" ] || printf 'CLI package: %s\n' "$pkg"
71
+ if [ -n "$pkg" ] && [ -n "$installed" ] && [ "$installed" != unknown ] && [ "$installed" != "$pkg" ]; then
72
+ if package_is_newer; then printf 'The installed version differs from this package. Run: gap update\n'
73
+ elif package_is_older; then
74
+ printf 'The installed version is newer than this package. Install the newer package: npm install -g @atomiton-dev/gap-cli@latest\n'
75
+ else printf 'The installed version differs from this package. Run: gap install\n'; fi
76
+ fi
77
+ }
78
+
79
+ # doctor_* are in gap-lib.sh (where the tests reach them). Exits 0 when no check found a problem, 1 otherwise.
80
+ cmd_doctor() {
81
+ DOCTOR_PROBLEMS=0
82
+ doctor_docker
83
+ doctor_node
84
+ doctor_ports
85
+ doctor_gap_command
86
+ doctor_registry
87
+ doctor_install
88
+ case "$DOCTOR_PROBLEMS" in
89
+ 0) printf 'Overall: OK\n' ;;
90
+ 1) printf 'Overall: 1 problem\n' ;;
91
+ *) printf 'Overall: %s problems\n' "$DOCTOR_PROBLEMS" ;;
92
+ esac
93
+ [ "$DOCTOR_PROBLEMS" = 0 ]
94
+ }
95
+
96
+ # cannot_hide_token: what `gap login` says when the terminal cannot be told to stop echoing (see cmd_login). Uses `host` of the caller.
97
+ cannot_hide_token() {
98
+ warn "Cannot hide the token on this terminal, so gap login did not ask for it"
99
+ printf 'Log in with Docker instead (it asks for the token itself): docker login %s -u <your GitHub username>\n' "${host:-ghcr.io}"
100
+ }
101
+
102
+ # `gap login` (#1986): npm channel only (the ZIP carries its images). The way in to the private registry that holds them, in
103
+ # GAP's own words: it asks for a GitHub username and a token, hands the token to `docker login --password-stdin` (on stdin, so it
104
+ # is in no argument list and no log), and then reads the three images the way `gap doctor` does, so that "logged in" and "can
105
+ # read GAP's images" are not mistaken for each other: a valid token of an account that was never given access logs in fine.
106
+ # Needs a terminal (the token is typed hidden); without one it says how to do it by hand. Docker keeps the credential, in its
107
+ # own store (the macOS keychain, Windows' credential manager, ...); gap writes it nowhere.
108
+ cmd_login() {
109
+ local host user token out rc saved traced=0
110
+ if [ "$(channel)" != npm ]; then
111
+ warn "The ZIP has no \`gap login\`"
112
+ printf 'Its images are inside the ZIP, so there is no registry to log in to.\n'
113
+ return 1
114
+ fi
115
+ host=$(channel_prefix); host=${host%%/*}
116
+ if [ ! -t 0 ]; then
117
+ warn "gap login asks for your GitHub username and token, so it needs a terminal"
118
+ printf 'Without one, log in with Docker and pipe the token into it:\n'
119
+ printf ' docker login %s -u <your GitHub username> --password-stdin\n' "$host"
120
+ return 1
121
+ fi
122
+ check_prereqs "gap login" || return 1
123
+ # The questions go to stderr, as `read -p` does: with `gap login > log` they are still on the screen.
124
+ {
125
+ printf 'GAP'"'"'s images are in %s, a private registry. You need a GitHub personal access token (classic)\n' "$host"
126
+ printf 'with the read:packages permission. Create one here if you have none:\n'
127
+ printf ' https://github.com/settings/tokens/new?scopes=read:packages\n\n'
128
+ printf 'GitHub username: '
129
+ } >&2
130
+ # `read` (IFS at its default) drops the blanks a paste brings in front of and behind the text.
131
+ read -r user || { printf '\n' >&2; user=""; }
132
+ if [ -z "$user" ]; then warn "No username was given"; return 1; fi
133
+ # The echo is turned off BEFORE the prompt is printed, not by `read -s` after it: a token pasted right after the prompt
134
+ # appears (or typed ahead together with the username) would be echoed in the meantime. A Ctrl-C used to leave the echo off
135
+ # (measured on bash 3.2), so that what the user typed next was invisible: put the terminal back as it was, whichever way this
136
+ # ends.
137
+ # If the terminal cannot be asked how it is set, or cannot be told to stop echoing, the token would be shown while the prompt says
138
+ # it is hidden (a missing or broken stty): then nothing is asked, rather than a lie.
139
+ saved=$(stty -g 2>/dev/null)
140
+ if [ -z "$saved" ]; then cannot_hide_token; return 1; fi
141
+ trap 'stty "$saved" 2>/dev/null; printf "\n" >&2; exit 130' INT TERM HUP QUIT
142
+ # A job that was stopped (Ctrl-Z) and resumed (fg) gets the terminal back as the shell saved it, echo on: hide the token again.
143
+ trap 'stty -echo 2>/dev/null' CONT
144
+ if ! stty -echo 2>/dev/null; then
145
+ trap - INT TERM HUP QUIT CONT; stty "$saved" 2>/dev/null
146
+ cannot_hide_token; return 1
147
+ fi
148
+ printf 'Token (hidden as you type): ' >&2
149
+ # From here the token is in a variable: no shell trace (`bash -x`, SHELLOPTS=xtrace) may print it, so it is switched off
150
+ # until the token has gone to Docker, and back on afterwards if it was on.
151
+ case $- in *x*) traced=1 ;; esac
152
+ { set +x; } 2>/dev/null
153
+ read -r token || token=""
154
+ trap - INT TERM HUP QUIT CONT
155
+ stty "$saved" 2>/dev/null
156
+ printf '\n' >&2
157
+ if [ -z "$token" ]; then [ "$traced" = 0 ] || set -x; warn "No token was given"; return 1; fi
158
+ out=$(printf '%s' "$token" | docker login "$host" -u "$user" --password-stdin 2>&1); rc=$?
159
+ out=${out//"$token"/***} # Docker does not print the password; if a text ever carried it, it would not reach the screen
160
+ token=""
161
+ [ "$traced" = 0 ] || set -x
162
+ if [ "$rc" != 0 ]; then
163
+ case "$(classify_login_error "$out")" in
164
+ rejected)
165
+ # ghcr.io ignores the username of a valid token (measured: a made-up name logs in), so only the token is blamed.
166
+ warn "$host did not accept that token"
167
+ printf ' - Paste the personal access token (classic) itself, not your GitHub password.\n'
168
+ printf ' - It needs the read:packages permission and must not be expired or revoked.\n'
169
+ printf ' Nothing was changed. Run gap login to try again.\n' ;;
170
+ network)
171
+ warn "Could not reach $host"
172
+ printf ' Check your internet connection (and proxy settings), then run gap login again.\n' ;;
173
+ *)
174
+ warn "docker login failed"
175
+ printf '%s\n' "$out" | tail -n 3 | sed 's/^/ /' ;;
176
+ esac
177
+ return 1
178
+ fi
179
+ ok "Logged in to $host as $user"
180
+ # What Docker said besides "Login Succeeded" is worth reading (it warns when the credential is kept unencrypted).
181
+ printf '%s\n' "$out" | grep -v '^Login Succeeded$' | sed -e '/^$/d' -e 's/^/ /'
182
+ LOGIN_JUST_DONE=1; DOCTOR_PROBLEMS=0; DOCTOR_DOCKER=1
183
+ doctor_registry
184
+ [ "$DOCTOR_PROBLEMS" = 0 ] || return 1
185
+ # Exit 0 means logged in AND able to read the images. When the check could not be made (no network for it, no VERSION file, a
186
+ # text Docker gave that is not known) neither is claimed: it is said, the status is 1, and no "Next:" is offered.
187
+ if [ "$REGISTRY_READABLE" != 1 ]; then
188
+ printf 'Logged in, but whether your account can read the images could not be checked. Run gap doctor when that is possible.\n'
189
+ return 1
190
+ fi
191
+ is_installed || printf 'Next: gap install\n'
192
+ return 0
193
+ }
194
+
195
+ # update_failed OLD NEW: the new version did not come up. Nothing is rolled back (the Kernel may already have migrated the
196
+ # database, and a migration is not undone): say where the backup is, what .env says now, and how to go back (print_restore_help).
197
+ update_failed() {
198
+ warn "The update to GAP $2 did not finish, and GAP $1 was not restored automatically"
199
+ printf ' The backup made before the update: %s\n' "$BACKUP_DIR"
200
+ printf ' .env now says GAP_VERSION=%s. Nothing was rolled back and nothing was deleted.\n' "$(env_get GAP_VERSION)"
201
+ printf ' First look at what the new version says: gap logs kernel\n'
202
+ print_restore_help "$BACKUP_DIR" "$1"
203
+ }
204
+
205
+ # update_not_applied OLD NEW: the new .env or compose file could not be written. The stack was not touched (the old version still
206
+ # runs as before); .env and the compose file may be half moved, and the backup holds the old ones.
207
+ update_not_applied() {
208
+ warn "The update to GAP $2 could not be applied: the files in $GAP_HOME could not be written"
209
+ printf ' The running stack was not touched; GAP %s runs as before. The backup made before: %s\n' "$1" "$BACKUP_DIR"
210
+ printf ' .env now says GAP_VERSION=%s. To put the old .env and compose file back:\n' "$(env_get GAP_VERSION)"
211
+ print_files_back_help "$BACKUP_DIR"
212
+ printf ' Check the permissions of %s, then run: gap update\n' "$GAP_HOME"
213
+ }
214
+
215
+ # D-update (#2014): npm channel only. Order matters: nothing of the old stack is touched until the images are here AND the
216
+ # backup was made and checked. Equal versions are "nothing to do"; an older package or a version it cannot compare is refused.
217
+ cmd_update() {
218
+ local installed pkg cmp
219
+ if [ "$(channel)" != npm ]; then
220
+ warn "The ZIP has no \`gap update\`"
221
+ printf 'To update: unzip the new installer and run its install.sh. It keeps your data and your secrets.\n'
222
+ return 1
223
+ fi
224
+ need_install || return 1
225
+ installed=$(installed_version); pkg=$(cli_version)
226
+ if [ -z "$pkg" ]; then
227
+ warn "This package has no VERSION file, so it cannot tell which version it brings"
228
+ return 1
229
+ fi
230
+ if ! cmp=$(version_cmp "$installed" "$pkg"); then
231
+ warn "Cannot compare the installed version ($installed) with the version of this package ($pkg)"
232
+ printf ' gap update only moves between versions it can compare (major.minor.patch, with the same -suffix).\n'
233
+ printf ' Nothing was changed.\n'
234
+ return 1
235
+ fi
236
+ if [ "$cmp" = 0 ]; then
237
+ printf 'GAP %s is already the version of this package. Nothing to do.\n' "$installed"
238
+ return 0
239
+ fi
240
+ if [ "$cmp" = 1 ]; then
241
+ warn "This package ($pkg) is older than the installed GAP ($installed)"
242
+ older_package_help
243
+ printf ' Nothing was changed.\n'
244
+ return 1
245
+ fi
246
+ printf 'Updating GAP %s -> %s\n\n' "$installed" "$pkg"
247
+ check_prereqs "gap update" up || return 1
248
+ pull_images "$pkg" "gap update" || return 1 # has said what to do; the old stack is untouched
249
+ ok "Images ready"
250
+ make_backup || return 1 # has said what is wrong; nothing was changed
251
+ ok "Backup saved in $BACKUP_DIR"
252
+ # The files first: if they cannot be written the running stack has not been touched, and the way back is two `cp` lines, not
253
+ # the restore (which starts with `down -v` and would erase a healthy stack).
254
+ if ! { put_file "$GAP_SRC/docker-compose.yml" "$GAP_HOME/docker-compose.yml" 644 \
255
+ && env_set GAP_VERSION "$pkg" && env_set GAP_IMAGE_PREFIX "$(channel_prefix)" \
256
+ && env_set GAP_PULL_POLICY "$(channel_pull_policy)"; }; then
257
+ update_not_applied "$installed" "$pkg"
258
+ return 1
259
+ fi
260
+ if ! { bring_up && health_checks; }; then
261
+ update_failed "$installed" "$pkg"
262
+ return 1
263
+ fi
264
+ printf '\nGAP updated: %s -> %s\n' "$installed" "$pkg"
265
+ printf ' Backup of GAP %s: %s\n' "$installed" "$BACKUP_DIR"
266
+ printf ' It is kept; delete it when you no longer need it. Check GAP with: gap status\n'
267
+ }
268
+
269
+ cmd_start() {
270
+ local stored port
271
+ check_prereqs "gap start" up || return 1
272
+ stored=$(env_get GAP_UI_PORT)
273
+ port=$(pick_port "$stored") || { warn "No free port for the Admin UI"; printf 'Action: free a port between %s and %s\n' "$GAP_PORT_FIRST" "$GAP_PORT_LAST"; return 1; }
274
+ if [ "$port" != "$stored" ]; then
275
+ warn "Port $stored is in use, using $port instead"
276
+ env_set GAP_UI_PORT "$port"
277
+ fi
278
+ bring_up || return 1
279
+ ok "GAP is running"
280
+ printf 'Admin UI: %s\n' "$(ui_url)"
281
+ newer_package_note
282
+ open_url "$(ui_url)"
283
+ }
284
+
285
+ cmd_stop() {
286
+ check_prereqs "gap stop" || return 1
287
+ dc stop || return 1
288
+ printf 'GAP stopped. Your data is kept. Run: gap start\n'
289
+ }
290
+
291
+ cmd_logs() {
292
+ check_prereqs "gap logs" || return 1
293
+ dc logs "$@"
294
+ }
295
+
296
+ # D28: stop the stack and remove everything the installer put on this machine. Without --purge the
297
+ # Docker volumes (gap_pgdata, gap_data) stay, and a later install finds and reuses them. With
298
+ # --purge the volumes and this version's GAP images go too (gap-kernel, gap-admin-ui and gap-postgres:16,
299
+ # under the names the install gave them: the registry prefix of .env, empty for a ZIP; the user's own
300
+ # postgres:16, if any, is a different name and is never touched). Nothing is removed until the stack has
301
+ # been taken down. Public since #1986 (#2015): it was hidden in #1750 (Scott, 21/10) and is now in usage()
302
+ # and the READMEs. It removes ~/.gap, the launcher and the rc lines the installer added, and nothing else: the
303
+ # unzipped installer folder (ZIP) and the package folder (npm) are never touched, and `npm uninstall -g` is the
304
+ # user's separate, later step.
305
+ cmd_uninstall() {
306
+ local purge=0 yes=0 arg answer version prefix removed had_backups=0
307
+ for arg in "$@"; do
308
+ case "$arg" in
309
+ --purge) purge=1 ;;
310
+ --yes) yes=1 ;;
311
+ *) usage >&2; return 2 ;;
312
+ esac
313
+ done
314
+ check_prereqs "gap uninstall" || return 1
315
+ version=$(env_get GAP_VERSION)
316
+ [ ! -d "$GAP_HOME/backups" ] || had_backups=1
317
+ if [ "$purge" = 1 ] && [ "$yes" = 0 ]; then
318
+ if [ ! -t 0 ]; then
319
+ warn "--purge erases all GAP data; run it in a terminal, or add --yes to confirm"
320
+ return 1
321
+ fi
322
+ if [ "$had_backups" = 1 ]; then
323
+ printf 'This erases ALL GAP data (database and files), the GAP images and the backups in %s/backups. Type "delete" to continue: ' "$GAP_HOME"
324
+ else
325
+ printf 'This erases ALL GAP data (database and files) and the GAP images. Type "delete" to continue: '
326
+ fi
327
+ read -r answer
328
+ [ "$answer" = delete ] || { printf 'Cancelled. Nothing was changed.\n'; return 1; }
329
+ fi
330
+ if [ "$purge" = 1 ]; then dc down -v || return 1; else dc down || return 1; fi
331
+ if [ "$purge" = 1 ]; then
332
+ prefix=$(installed_prefix)
333
+ docker rmi "$(image_ref "$prefix" gap-kernel "$version")" "$(image_ref "$prefix" gap-admin-ui "$version")" \
334
+ "$(image_ref "$prefix" gap-postgres 16)" >/dev/null 2>&1 || true
335
+ fi
336
+ # The launcher only if it is ours: a `gap` of another program, or a symlink, stays.
337
+ if launcher_is_ours "$LAUNCHER_DIR/gap"; then rm -f "$LAUNCHER_DIR/gap"; fi
338
+ removed=$(remove_rc_lines)
339
+ # The backups of `gap update` are data: without --purge they stay, everything else of ~/.gap goes (they hold the database
340
+ # and the old password, so nothing here makes them readable by others: the folder is 700).
341
+ if [ "$purge" = 0 ] && [ "$had_backups" = 1 ]; then
342
+ find "$GAP_HOME" -mindepth 1 -maxdepth 1 ! -name backups -exec rm -rf {} +
343
+ else
344
+ rm -rf "$GAP_HOME"
345
+ fi
346
+ ok "GAP removed"
347
+ [ -z "$removed" ] || printf 'Removed the lines the installer added to:%s\n' "$removed"
348
+ if [ "$purge" = 1 ]; then
349
+ printf 'All GAP data was erased.\n'
350
+ [ "$had_backups" = 0 ] || printf 'The backups in %s/backups were erased too.\n' "$GAP_HOME"
351
+ else
352
+ [ "$had_backups" = 0 ] || printf 'The backups of gap update are kept in %s/backups (delete that folder when you no longer need them).\n' "$GAP_HOME"
353
+ printf 'Your data is kept in the Docker volumes %s_pgdata and %s_data; installing again brings it back.\n' "$GAP_PROJECT" "$GAP_PROJECT"
354
+ printf 'To erase it for good, install again and run: gap uninstall --purge\n'
355
+ fi
356
+ }
357
+
358
+ cmd=${1:-}
359
+ [ $# -gt 0 ] && shift
360
+ case "$cmd" in
361
+ version|doctor|update|login) [ $# -eq 0 ] || { usage >&2; exit 2; }; "cmd_$cmd" ;;
362
+ status|start|stop|logs|uninstall) need_install || exit 1; "cmd_$cmd" "$@" ;;
363
+ help|-h|--help) usage ;;
364
+ *) usage >&2; exit 2 ;;
365
+ esac