@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 +132 -2
- package/bin/gap.js +47 -0
- package/installer/CHANNEL +1 -0
- package/installer/VERSION +1 -0
- package/installer/docker-compose.yml +86 -0
- package/installer/env.template +13 -0
- package/installer/gap +365 -0
- package/installer/gap-lib.ps1 +1248 -0
- package/installer/gap-lib.sh +1203 -0
- package/installer/gap.ps1 +361 -0
- package/installer/install.ps1 +262 -0
- package/installer/install.sh +252 -0
- package/lib/launcher.js +31 -0
- package/lib/postinstall.js +137 -0
- package/package.json +31 -4
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Installs the local GAP stack from this folder (macOS, Linux). Safe to run again: secrets stay.
|
|
3
|
+
# Folder contents (#1750, D27): docker-compose.yml, env.template, VERSION, images.tar.gz,
|
|
4
|
+
# install.sh, gap, gap-lib.sh, README.TXT, BUILD-INFO.txt.
|
|
5
|
+
#
|
|
6
|
+
# D28: the installed instance lives in ~/.gap (docker-compose.yml, .env, gap, gap-lib.sh). This
|
|
7
|
+
# folder is only the installer: after a successful run it can be moved or deleted.
|
|
8
|
+
#
|
|
9
|
+
# #1986 (#2011): the same script is the installer of the npm package `@atomiton-dev/gap-cli` (`gap install`), told
|
|
10
|
+
# apart by the CHANNEL file next to it (gap-lib.sh: channel). There the images are pulled from ghcr.io instead of
|
|
11
|
+
# loaded, no launcher and no PATH or rc line is made (npm put `gap` on the PATH), and only docker-compose.yml is
|
|
12
|
+
# copied into ~/.gap, because this folder is the package that provides `gap` and must stay.
|
|
13
|
+
set -u
|
|
14
|
+
|
|
15
|
+
GAP_SRC=$(cd "$(dirname "$0")" && pwd -P)
|
|
16
|
+
# shellcheck source=gap-lib.sh
|
|
17
|
+
. "$GAP_SRC/gap-lib.sh"
|
|
18
|
+
|
|
19
|
+
fail() { warn "$1"; shift; for line in "$@"; do printf '%s\n' "$line"; done; exit 1; }
|
|
20
|
+
|
|
21
|
+
# sync_files: copy what the installed instance needs into GAP_HOME (D28). A newer installer replaces
|
|
22
|
+
# the older compose file and scripts; .env is never touched here.
|
|
23
|
+
sync_files() {
|
|
24
|
+
local f
|
|
25
|
+
mkdir -p "$GAP_HOME" || return 1
|
|
26
|
+
[ "$GAP_SRC" != "$(cd "$GAP_HOME" && pwd -P)" ] || return 0 # running from GAP_HOME itself
|
|
27
|
+
put_file "$GAP_SRC/docker-compose.yml" "$GAP_HOME/docker-compose.yml" 644 || return 1
|
|
28
|
+
[ "$(channel)" != npm ] || return 0 # npm: `dc` needs the compose file here; the scripts stay in the package
|
|
29
|
+
put_file "$GAP_SRC/gap-lib.sh" "$GAP_HOME/gap-lib.sh" 644 || return 1
|
|
30
|
+
put_file "$GAP_SRC/gap" "$GAP_HOME/gap" 755 || return 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# reset_db_password: set the Postgres password to the one in .env (D28, replaces the stop of D24).
|
|
34
|
+
# Postgres keeps the password it was first started with, so a lost .env would lock the Kernel out of
|
|
35
|
+
# its own data. The official image trusts connections made on the local socket inside the container
|
|
36
|
+
# (pg_hba.conf: `local all all trust`), so the password can be set without knowing the old one.
|
|
37
|
+
# Starts only postgres. The result is checked over the container's own IP, which (unlike 127.0.0.1)
|
|
38
|
+
# is not trusted, so a wrong password fails there. Leaves the reason in RESET_ERR.
|
|
39
|
+
RESET_ERR=""
|
|
40
|
+
reset_db_password() {
|
|
41
|
+
local pw id ip out
|
|
42
|
+
pw=$(env_get GAP_DB_PASSWORD)
|
|
43
|
+
if ! out=$(dc up -d --wait --wait-timeout 120 postgres 2>&1); then RESET_ERR="postgres did not start: $(printf '%s' "$out" | tail -n 2)"; return 1; fi
|
|
44
|
+
id=$(dc ps -q postgres | head -n 1)
|
|
45
|
+
[ -n "$id" ] || { RESET_ERR="postgres container not found"; return 1; }
|
|
46
|
+
if ! out=$(printf "ALTER USER postgres PASSWORD '%s';\n" "$pw" | docker exec -i "$id" psql -U postgres -v ON_ERROR_STOP=1 -q 2>&1); then
|
|
47
|
+
RESET_ERR="setting the password failed: $out"; return 1
|
|
48
|
+
fi
|
|
49
|
+
ip=$(docker exec "$id" hostname -i 2>/dev/null | awk '{print $1}')
|
|
50
|
+
[ -n "$ip" ] || { RESET_ERR="could not read the postgres container's IP"; return 1; }
|
|
51
|
+
if ! out=$(docker exec -e PGPASSWORD="$pw" "$id" psql -h "$ip" -U postgres -tAc 'select 1' 2>&1); then
|
|
52
|
+
RESET_ERR="the new password was not accepted: $out"; return 1
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Makes `gap` runnable from any terminal: a small launcher in ~/.local/bin, and ~/.local/bin on the
|
|
57
|
+
# PATH of new terminals. Never needs sudo. A ~/.local/bin/gap that is not ours (another program
|
|
58
|
+
# called gap, or a symlink) is left alone, and then nothing else is changed either. Sets
|
|
59
|
+
# LAUNCHER_STATE (installed, foreign or failed) and RC_ADDED (the rc files that got the PATH line).
|
|
60
|
+
LAUNCHER_STATE=""
|
|
61
|
+
RC_ADDED=""
|
|
62
|
+
install_launcher() {
|
|
63
|
+
local rc target="$LAUNCHER_DIR/gap"
|
|
64
|
+
LAUNCHER_STATE=failed
|
|
65
|
+
mkdir -p "$LAUNCHER_DIR" 2>/dev/null || return 0
|
|
66
|
+
if { [ -e "$target" ] || [ -L "$target" ]; } && ! launcher_is_ours "$target"; then
|
|
67
|
+
LAUNCHER_STATE=foreign; return 0
|
|
68
|
+
fi
|
|
69
|
+
# Through a new file and a rename, so an old launcher is replaced, never written into.
|
|
70
|
+
{ launcher_text > "$target.new" && chmod +x "$target.new" && mv -f "$target.new" "$target"; } 2>/dev/null \
|
|
71
|
+
|| { rm -f "$target.new" 2>/dev/null; return 0; }
|
|
72
|
+
LAUNCHER_STATE=installed
|
|
73
|
+
case ":$PATH:" in
|
|
74
|
+
*":$LAUNCHER_DIR:"*) ;;
|
|
75
|
+
*)
|
|
76
|
+
while IFS= read -r rc; do
|
|
77
|
+
if ! grep -qF "$RC_MARKER" "$rc" 2>/dev/null; then
|
|
78
|
+
mkdir -p "$(dirname "$rc")" 2>/dev/null # fish: ~/.config/fish may not exist yet
|
|
79
|
+
# Only a line that was written is reported; braces, so that a refused write stays quiet.
|
|
80
|
+
if { printf '\n%s\n%s\n' "$RC_MARKER" "$RC_EXPORT" >> "$rc"; } 2>/dev/null; then
|
|
81
|
+
RC_ADDED="$RC_ADDED $rc"
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
done < <(rc_files) ;;
|
|
85
|
+
esac
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
printf 'Installing GAP from %s\n\n' "$GAP_SRC"
|
|
89
|
+
|
|
90
|
+
# 1. Prerequisites
|
|
91
|
+
check_prereqs "$(install_cmd)" up || exit 1
|
|
92
|
+
ok "Prerequisites passed"
|
|
93
|
+
|
|
94
|
+
for f in VERSION docker-compose.yml env.template gap gap-lib.sh; do
|
|
95
|
+
[ -f "$GAP_SRC/$f" ] || fail "$f is missing in $GAP_SRC" "Download the installer again."
|
|
96
|
+
done
|
|
97
|
+
version=$(tr -d '[:space:]' < "$GAP_SRC/VERSION")
|
|
98
|
+
|
|
99
|
+
# npm channel: never go back. An older package over a newer GAP would start an old Kernel on a database a newer one has
|
|
100
|
+
# migrated (migrations are not undone), and `gap update` refuses it as well. Only a comparison that can be made refuses:
|
|
101
|
+
# equal, newer, another suffix or an unreadable version run as they always did.
|
|
102
|
+
if [ "$(channel)" = npm ] && is_installed && [ "$(version_cmp "$(installed_version)" "$version" 2>/dev/null)" = 1 ]; then
|
|
103
|
+
warn "This package ($version) is older than the installed GAP ($(installed_version))"
|
|
104
|
+
older_package_help
|
|
105
|
+
printf ' Nothing was changed.\n'
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# 2. Images. npm channel: pulled from the registry, the missing ones only, before anything is written (the user may
|
|
110
|
+
# not be logged in or allowed, and must then be left with nothing half installed). ZIP channel: from the archive in
|
|
111
|
+
# this folder (no network needed). Postgres is packaged as
|
|
112
|
+
# gap-postgres:16, so loading never moves the user's own postgres:16 tag. The archive is loaded when
|
|
113
|
+
# an image is missing, and also when this package differs from the one loaded last time (its
|
|
114
|
+
# BUILD-INFO.txt is recorded in ~/.gap), so a rebuilt package with the same VERSION still replaces
|
|
115
|
+
# the images. Only missing images make images.tar.gz mandatory.
|
|
116
|
+
pkg_id=""
|
|
117
|
+
need_load=0
|
|
118
|
+
if [ "$(channel)" = npm ]; then
|
|
119
|
+
pull_images "$version" || exit 1 # has said what to do
|
|
120
|
+
ok "Images ready"
|
|
121
|
+
else
|
|
122
|
+
[ -f "$GAP_SRC/BUILD-INFO.txt" ] && pkg_id=$(cksum < "$GAP_SRC/BUILD-INFO.txt" | awk '{print $1 "-" $2}')
|
|
123
|
+
for img in "gap-kernel:$version" "gap-admin-ui:$version" gap-postgres:16; do
|
|
124
|
+
docker image inspect "$img" >/dev/null 2>&1 || need_load=1
|
|
125
|
+
done
|
|
126
|
+
if [ "$need_load" = 0 ] && [ -n "$pkg_id" ] && [ -f "$GAP_SRC/images.tar.gz" ] \
|
|
127
|
+
&& [ "$(cat "$GAP_HOME/images.loaded" 2>/dev/null)" != "$pkg_id" ]; then
|
|
128
|
+
need_load=1
|
|
129
|
+
fi
|
|
130
|
+
if [ "$need_load" = 1 ]; then
|
|
131
|
+
[ -f "$GAP_SRC/images.tar.gz" ] || fail "images.tar.gz is missing and the images are not on this machine" "Download the installer again."
|
|
132
|
+
printf 'Loading images, this can take a minute...\n'
|
|
133
|
+
load_out=$(docker load -i "$GAP_SRC/images.tar.gz" 2>&1) || fail "Loading images failed" "$load_out"
|
|
134
|
+
fi
|
|
135
|
+
ok "Images loaded"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# 3. The installed instance (D28)
|
|
139
|
+
sync_files || fail "Could not write to $GAP_HOME" "Check the permissions of that folder, then run $(install_name) again."
|
|
140
|
+
if [ "$need_load" = 1 ] && [ -n "$pkg_id" ]; then printf '%s\n' "$pkg_id" > "$GAP_HOME/images.loaded"; fi
|
|
141
|
+
|
|
142
|
+
# 4. Configuration. First run creates .env with fresh secrets; later runs only update version and
|
|
143
|
+
# port (D23, D9) and keep the password, which Postgres has already stored in its volume.
|
|
144
|
+
had_volume=0
|
|
145
|
+
docker volume inspect "${GAP_PROJECT}_pgdata" >/dev/null 2>&1 && had_volume=1
|
|
146
|
+
if [ -f "$GAP_HOME/.env" ]; then
|
|
147
|
+
stored_port=$(env_get GAP_UI_PORT)
|
|
148
|
+
env_set GAP_VERSION "$version"
|
|
149
|
+
else
|
|
150
|
+
stored_port=""
|
|
151
|
+
( umask 077
|
|
152
|
+
sed -e "s|@GAP_VERSION@|$version|" \
|
|
153
|
+
-e "s|@GAP_UI_PORT@|0|" \
|
|
154
|
+
-e "s|@GAP_DB_PASSWORD@|$(od -An -N24 -tx1 /dev/urandom | tr -d ' \n')|" \
|
|
155
|
+
"$GAP_SRC/env.template" > "$GAP_HOME/.env" )
|
|
156
|
+
# D28: data from an earlier install is still in the volume but its password is gone with the old
|
|
157
|
+
# .env (the install folder was moved or deleted). Set the new password inside Postgres.
|
|
158
|
+
if [ "$had_volume" = 1 ]; then
|
|
159
|
+
if reset_db_password; then
|
|
160
|
+
ok "GAP data from an earlier install was found; its lost .env was rebuilt and the data is kept"
|
|
161
|
+
else
|
|
162
|
+
rm -f "$GAP_HOME/.env" # back to "no .env", so the next run starts from the same place
|
|
163
|
+
fail "GAP data from an earlier install exists, but its database password could not be reset" \
|
|
164
|
+
"$RESET_ERR" \
|
|
165
|
+
"Either: put the old .env back into $GAP_HOME and run $(install_name) again," \
|
|
166
|
+
"or: erase the old data (it is lost for good) with" \
|
|
167
|
+
" docker compose -p $GAP_PROJECT down -v" \
|
|
168
|
+
"and run $(install_name) again."
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
fi
|
|
172
|
+
# Where this channel gets its images from (#1986): the ZIP's own, loaded above under their local names and never
|
|
173
|
+
# pulled (empty prefix, `never`); the npm channel's, pulled above from the registry (its prefix, `missing`). The
|
|
174
|
+
# channel's value, not .env's: written on every run, not only on the first, so that a machine that used another
|
|
175
|
+
# channel (or an older package that did not know these keys) takes this channel's names from now on.
|
|
176
|
+
env_set GAP_IMAGE_PREFIX "$(channel_prefix)"
|
|
177
|
+
env_set GAP_PULL_POLICY "$(channel_pull_policy)"
|
|
178
|
+
port=$(pick_port "$stored_port") || fail "No free port for the Admin UI" "Action: free a port between $GAP_PORT_FIRST and $GAP_PORT_LAST, then run $(install_name) again."
|
|
179
|
+
[ -z "$stored_port" ] || [ "$port" = "$stored_port" ] || warn "Port $stored_port is in use, using $port instead"
|
|
180
|
+
env_set GAP_UI_PORT "$port"
|
|
181
|
+
|
|
182
|
+
# 5. Start. Both lines are true once the Kernel is healthy: it depends on a healthy Postgres, and
|
|
183
|
+
# its entrypoint finishes the schema before the server that answers /health starts (D16).
|
|
184
|
+
bring_up || exit 1
|
|
185
|
+
ok "Database initialized"
|
|
186
|
+
ok "Migrations completed"
|
|
187
|
+
|
|
188
|
+
# 6. Health checks through the Admin UI port, the path the user will take.
|
|
189
|
+
health_checks || exit 1 # has said which path did not answer (gap-lib.sh)
|
|
190
|
+
ok "Health checks passed"
|
|
191
|
+
|
|
192
|
+
# 7. The gap command
|
|
193
|
+
other=$(command -v gap 2>/dev/null || true)
|
|
194
|
+
if [ "$(channel)" = npm ]; then
|
|
195
|
+
# npm put `gap` on the PATH: no launcher, no PATH or rc line (D5). A launcher left by a ZIP install is only reported (D6).
|
|
196
|
+
LAUNCHER_STATE=npm
|
|
197
|
+
if launcher_is_ours "$LAUNCHER_DIR/gap"; then
|
|
198
|
+
add_note " Warning: $LAUNCHER_DIR/gap is a launcher from a ZIP install of GAP and may run instead of the npm command."
|
|
199
|
+
add_note " Use one channel per machine; the data is shared. To keep only the npm command: rm $LAUNCHER_DIR/gap"
|
|
200
|
+
fi
|
|
201
|
+
else
|
|
202
|
+
install_launcher
|
|
203
|
+
fi
|
|
204
|
+
# Does `gap` mean GAP in the user's own terminal? An alias or function of that name (oh-my-zsh has
|
|
205
|
+
# `alias gap='git apply'`) is invisible to this script, so the user's shell is asked, and the user is
|
|
206
|
+
# offered to turn it off (gap-lib.sh). Done before the summary below, so that the summary stays the
|
|
207
|
+
# last thing on the screen.
|
|
208
|
+
asked=1
|
|
209
|
+
if [ "$LAUNCHER_STATE" = installed ] || [ "$LAUNCHER_STATE" = npm ]; then
|
|
210
|
+
printf 'Checking the gap command in your terminal...\n'
|
|
211
|
+
check_gap_resolution && asked=0
|
|
212
|
+
fi
|
|
213
|
+
printf '\nGAP Platform is ready!\n\n'
|
|
214
|
+
printf ' Admin UI: %s\n' "$(ui_url)"
|
|
215
|
+
printf ' Login: admin@example.com / changeme\n\n'
|
|
216
|
+
case "$LAUNCHER_STATE" in
|
|
217
|
+
installed|npm)
|
|
218
|
+
case "$GAP_CHECK_CMD" in
|
|
219
|
+
"gap status") also="gap start, gap stop, gap logs" ;;
|
|
220
|
+
"command gap status") also="command gap start, stop, logs" ;;
|
|
221
|
+
*) also="start, stop, logs" ;;
|
|
222
|
+
esac
|
|
223
|
+
printf ' Check it any time with: %s (also: %s)\n' "$GAP_CHECK_CMD" "$also"
|
|
224
|
+
# This terminal still has the alias; only new ones read the line that turns it off.
|
|
225
|
+
if [ -n "$GAP_FIX_ADDED" ] && [ "$GAP_CHECK_CMD" = "gap status" ]; then
|
|
226
|
+
printf ' Open a new terminal first; in this one, use: command gap status\n'
|
|
227
|
+
fi ;;
|
|
228
|
+
foreign)
|
|
229
|
+
printf ' Check it any time with: %s/gap status (also: start, stop, logs)\n' "$GAP_HOME"
|
|
230
|
+
printf ' %s/gap already exists and is not from GAP, so it was left alone.\n' "$LAUNCHER_DIR" ;;
|
|
231
|
+
*)
|
|
232
|
+
printf ' Check it any time with: %s/gap status (also: start, stop, logs)\n' "$GAP_HOME"
|
|
233
|
+
printf ' Could not write %s/gap; check the permissions of %s.\n' "$LAUNCHER_DIR" "$LAUNCHER_DIR" ;;
|
|
234
|
+
esac
|
|
235
|
+
if [ "$LAUNCHER_STATE" = npm ]; then
|
|
236
|
+
# $GAP_SRC is the package that provides `gap`: deleting it would remove the command.
|
|
237
|
+
printf ' GAP now runs from %s. Keep the npm package: it is the gap command.\n' "$GAP_HOME"
|
|
238
|
+
else
|
|
239
|
+
printf ' GAP now runs from %s; you can delete the installer folder %s\n' "$GAP_HOME" "$GAP_SRC"
|
|
240
|
+
fi
|
|
241
|
+
if [ -n "$RC_ADDED" ]; then
|
|
242
|
+
printf ' Added %s to the PATH in:%s\n' "$LAUNCHER_DIR" "$RC_ADDED"
|
|
243
|
+
printf ' Open a new terminal for that to take effect, or run: %s/gap status\n' "$GAP_HOME"
|
|
244
|
+
fi
|
|
245
|
+
[ -z "$GAP_NOTES" ] || printf '%s' "$GAP_NOTES"
|
|
246
|
+
# Only when no shell could be asked, fall back to a gap that this script can see on its own PATH.
|
|
247
|
+
if [ "$LAUNCHER_STATE" = installed ] && [ "$asked" != 0 ] && [ -n "$other" ] && [ "$other" != "$LAUNCHER_DIR/gap" ]; then
|
|
248
|
+
printf ' Note: another "gap" is already on your PATH (%s) and may run instead; use %s/gap\n' "$other" "$GAP_HOME"
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
# 8. Open the browser; a machine without a desktop just ignores it.
|
|
252
|
+
open_url "$(ui_url)"
|
package/lib/launcher.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Pure helpers of the `gap` shell (#1986, #2010). No I/O, so they can be tested on any machine.
|
|
3
|
+
// ES5 on purpose: this file is loaded by bin/gap.js after the Node version check, but keeping it plain
|
|
4
|
+
// costs nothing and keeps it readable on the oldest Node that can still print the message.
|
|
5
|
+
var path = require('path');
|
|
6
|
+
|
|
7
|
+
var MIN_NODE_MAJOR = 18;
|
|
8
|
+
|
|
9
|
+
// true when `version` ("18.20.8", no leading "v") names Node 18 or newer; false for anything unreadable.
|
|
10
|
+
function nodeSupported(version) {
|
|
11
|
+
var m = /^(\d+)\.\d+\.\d+/.exec(String(version === undefined || version === null ? '' : version));
|
|
12
|
+
return m !== null && parseInt(m[1], 10) >= MIN_NODE_MAJOR;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// The command that runs the installer script for `args`. `gap install ...` runs install.sh / install.ps1
|
|
16
|
+
// (without the word "install"); everything else goes to the `gap` launcher. bash is named explicitly so
|
|
17
|
+
// the result does not depend on the execute bit surviving the npm unpack. Arguments are never joined.
|
|
18
|
+
function buildCommand(opts) {
|
|
19
|
+
var win = opts.platform === 'win32';
|
|
20
|
+
var join = win ? path.win32.join : path.posix.join;
|
|
21
|
+
var args = opts.args.slice();
|
|
22
|
+
var install = args[0] === 'install';
|
|
23
|
+
if (install) args.shift();
|
|
24
|
+
var script = join(opts.pkgRoot, 'installer', install ? (win ? 'install.ps1' : 'install.sh') : (win ? 'gap.ps1' : 'gap'));
|
|
25
|
+
if (win) {
|
|
26
|
+
return { cmd: 'powershell', args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script].concat(args) };
|
|
27
|
+
}
|
|
28
|
+
return { cmd: 'bash', args: [script].concat(args) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { nodeSupported: nodeSupported, buildCommand: buildCommand, MIN_NODE_MAJOR: MIN_NODE_MAJOR };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Runs after `npm install -g @atomiton-dev/gap-cli` (#1986, #2012). One job: on a Mac or Linux box with
|
|
3
|
+
// oh-my-zsh, let a NEW terminal run `gap login` at once.
|
|
4
|
+
//
|
|
5
|
+
// Why: oh-my-zsh's git plugin (on by default, plugins=(git)) defines `alias gap='git apply'`. zsh applies an
|
|
6
|
+
// alias before it looks at PATH, so `gap login` ran `git apply login` and failed with "can't open patch 'login'"
|
|
7
|
+
// before any code of ours could run or explain it. A package cannot print a warning at install time (npm 7+ hides
|
|
8
|
+
// the output of install scripts), so the fix has to act.
|
|
9
|
+
//
|
|
10
|
+
// What it does: it creates ONE new file, <oh-my-zsh>/custom/atomiton-gap.zsh. oh-my-zsh reads every custom/*.zsh
|
|
11
|
+
// after its plugins, which is where it expects a user's overrides. The file turns off that one alias, and only
|
|
12
|
+
// while a package it was installed with is still on the disk (npm 7+ runs no uninstall script, so the file cannot
|
|
13
|
+
// remove itself: without the package it does nothing and `gap` is `git apply` again, as the user had it). It lists
|
|
14
|
+
// the packages: with nvm there is one global package per Node version, and removing one must not break the others.
|
|
15
|
+
// The file does not depend on PATH: nvm usually adds the global bin to PATH from .zshrc, after oh-my-zsh has
|
|
16
|
+
// loaded custom/.
|
|
17
|
+
//
|
|
18
|
+
// What it does not do: edit ~/.zshrc or any file it did not create; touch an alias the user made himself
|
|
19
|
+
// (only the exact `git apply` one is turned off); write through a symlink; run for a local install (`npm install`
|
|
20
|
+
// in a project), under sudo, or as root into a directory somebody else owns (a root-owned file in a user's
|
|
21
|
+
// directory); ever fail the install (every error is swallowed, exit 0; package.json adds `|| exit 0` for the
|
|
22
|
+
// case where node itself cannot be found by npm's shell).
|
|
23
|
+
// Not covered: --ignore-scripts, a ZSH_CUSTOM set only in .zshrc, prezto, other shells.
|
|
24
|
+
// ES5, like the rest of the shell: it must run on the oldest Node npm still installs this on.
|
|
25
|
+
var fs = require('fs');
|
|
26
|
+
var os = require('os');
|
|
27
|
+
var path = require('path');
|
|
28
|
+
|
|
29
|
+
var FILE_NAME = 'atomiton-gap.zsh';
|
|
30
|
+
var OWNER_LINE = '# Written by @atomiton-dev/gap-cli (npm install -g). Delete this file to undo it.';
|
|
31
|
+
|
|
32
|
+
// A zsh single-quoted string: nothing inside is special, except the quote itself.
|
|
33
|
+
function zshQuote(text) {
|
|
34
|
+
return "'" + String(text).replace(/'/g, "'\\''") + "'";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The directory oh-my-zsh reads custom/*.zsh from: ZSH_CUSTOM, else $ZSH/custom, else ~/.oh-my-zsh/custom.
|
|
38
|
+
// Only when it already exists and is an absolute path (a relative one would be read from the folder npm runs this
|
|
39
|
+
// in, not from where the shell means): this never creates an oh-my-zsh directory for somebody who has none.
|
|
40
|
+
function customDir(env, home) {
|
|
41
|
+
var dir = env.ZSH_CUSTOM || path.join(env.ZSH || path.join(home, '.oh-my-zsh'), 'custom');
|
|
42
|
+
if (!path.isAbsolute(dir)) return null;
|
|
43
|
+
try {
|
|
44
|
+
return fs.statSync(dir).isDirectory() ? dir : null;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var PATH_LINE = '# gap-cli package: ';
|
|
51
|
+
|
|
52
|
+
// The package paths a file of ours lists, one per line, each a JSON string (so a newline in a path cannot break the
|
|
53
|
+
// line, and the file is read back the way it was written).
|
|
54
|
+
function listedPaths(text) {
|
|
55
|
+
var found = [];
|
|
56
|
+
text.split('\n').forEach(function (line) {
|
|
57
|
+
if (line.indexOf(PATH_LINE) !== 0) return;
|
|
58
|
+
try {
|
|
59
|
+
var one = JSON.parse(line.slice(PATH_LINE.length));
|
|
60
|
+
if (typeof one === 'string' && one) found.push(one);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// a line we cannot read is dropped
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return found;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function fileText(binPaths) {
|
|
69
|
+
var exists = binPaths.map(function (p) { return '-e ' + zshQuote(p); }).join(' || ');
|
|
70
|
+
return [
|
|
71
|
+
OWNER_LINE,
|
|
72
|
+
"# oh-my-zsh's git plugin defines alias gap='git apply'. zsh uses an alias before it looks at PATH, so it hides",
|
|
73
|
+
'# the `gap` command of GAP. This turns off that one alias, and only while a package listed below is installed.',
|
|
74
|
+
'# If you uninstall the package, or delete this file, `gap` is `git apply` again.'
|
|
75
|
+
].concat(binPaths.map(function (p) { return PATH_LINE + JSON.stringify(p); }), [
|
|
76
|
+
"if [[ ${aliases[gap]:-} == 'git apply' ]] && [[ " + exists + ' ]]; then unalias gap; fi',
|
|
77
|
+
''
|
|
78
|
+
]).join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Returns the path of the file it wrote, or null when it did nothing (and why is not worth a message).
|
|
82
|
+
function run(env, packageRoot) {
|
|
83
|
+
if (env.npm_config_global !== 'true') return null; // a local install must leave the home directory alone
|
|
84
|
+
if (env.SUDO_UID) return null;
|
|
85
|
+
var home = env.HOME || os.homedir();
|
|
86
|
+
if (!home) return null;
|
|
87
|
+
var dir = customDir(env, home);
|
|
88
|
+
if (!dir) return null;
|
|
89
|
+
// su, doas or a root shell that keeps the user's HOME: no SUDO_UID, but a root-owned file would land in his directory
|
|
90
|
+
if (typeof process.getuid === 'function' && fs.statSync(dir).uid !== process.getuid()) return null;
|
|
91
|
+
var file = path.join(dir, FILE_NAME);
|
|
92
|
+
var binPath = path.join(packageRoot, 'bin', 'gap.js');
|
|
93
|
+
var current = null;
|
|
94
|
+
var st = null;
|
|
95
|
+
try {
|
|
96
|
+
st = fs.lstatSync(file);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
st = null; // not there
|
|
99
|
+
}
|
|
100
|
+
if (st) {
|
|
101
|
+
if (st.isSymbolicLink() || !st.isFile()) return null; // a link (dotfile managers make them) or a directory: not ours to write through
|
|
102
|
+
current = fs.readFileSync(file, 'utf8');
|
|
103
|
+
if (current.indexOf(OWNER_LINE) !== 0) return null; // somebody else's file: leave it
|
|
104
|
+
}
|
|
105
|
+
// the packages of earlier installs that are still on the disk (one per Node version under nvm), then this one
|
|
106
|
+
var paths = (current === null ? [] : listedPaths(current)).filter(function (p) {
|
|
107
|
+
return p !== binPath && fs.existsSync(p);
|
|
108
|
+
});
|
|
109
|
+
paths.push(binPath);
|
|
110
|
+
var text = fileText(paths);
|
|
111
|
+
if (current === text) return file;
|
|
112
|
+
// written beside it and renamed: a terminal opened meanwhile reads the old file or the new one, not half of one
|
|
113
|
+
var tmp = file + '.tmp-' + process.pid;
|
|
114
|
+
try {
|
|
115
|
+
fs.writeFileSync(tmp, text, { mode: 420 }); // 0644
|
|
116
|
+
fs.renameSync(tmp, file);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
try { fs.unlinkSync(tmp); } catch (e2) { /* nothing to remove */ }
|
|
119
|
+
throw e;
|
|
120
|
+
}
|
|
121
|
+
return file;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { run: run, fileText: fileText, zshQuote: zshQuote, FILE_NAME: FILE_NAME };
|
|
125
|
+
|
|
126
|
+
if (require.main === module) {
|
|
127
|
+
try {
|
|
128
|
+
var written = run(process.env, path.resolve(__dirname, '..'));
|
|
129
|
+
if (written) {
|
|
130
|
+
process.stdout.write("gap: turned off oh-my-zsh's `gap` alias (git apply) for new terminals, in " + written +
|
|
131
|
+
'. Delete that file to undo it.\n');
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
// never fail the install over a convenience
|
|
135
|
+
}
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atomiton-dev/gap-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.9.0-mvp",
|
|
4
|
+
"description": "Install and run the GAP platform on your own machine with Docker.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"gap": "bin/gap.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node lib/postinstall.js || exit 0"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"license": "UNLICENSED",
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/gap.js",
|
|
20
|
+
"lib/launcher.js",
|
|
21
|
+
"lib/postinstall.js",
|
|
22
|
+
"installer/VERSION",
|
|
23
|
+
"installer/CHANNEL",
|
|
24
|
+
"installer/gap",
|
|
25
|
+
"installer/gap-lib.sh",
|
|
26
|
+
"installer/install.sh",
|
|
27
|
+
"installer/gap.ps1",
|
|
28
|
+
"installer/gap-lib.ps1",
|
|
29
|
+
"installer/install.ps1",
|
|
30
|
+
"installer/docker-compose.yml",
|
|
31
|
+
"installer/env.template"
|
|
32
|
+
]
|
|
33
|
+
}
|