Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:
- run: npm ci
- run: npm run check
- run: npm run build:sea
- name: Test packaged executable updates
run: EXTRABRAIN_TEST_EXECUTABLE="$PWD/dist/extrabrain" npm test -- test/install.test.ts
- name: Smoke test macOS executable
shell: bash
run: |
Expand All @@ -37,3 +39,55 @@ jobs:
set -e
test "$code" = 3
echo "$result" | jq -e '.code == 3'
- name: Install macOS command for the current user
shell: bash
run: |
set -euo pipefail
version=$(node -p 'require("./package.json").version')
arch=$(uname -m)
if [ "$arch" = x86_64 ]; then arch=x64; fi
release="$RUNNER_TEMP/installer-releases/download/v$version"
mkdir -p "$release"
asset="extrabrain-v$version-darwin-$arch.tar.gz"
tar -czf "$release/$asset" -C dist extrabrain
(cd "$release" && shasum -a 256 "$asset" > SHA256SUMS)
install_home="$RUNNER_TEMP/installer-home"
mkdir -p "$install_home"
echo "EXTRABRAIN_TEST_HOME=$install_home" >> "$GITHUB_ENV"
cat install.sh | env HOME="$install_home" ZDOTDIR="$install_home" SHELL=/bin/zsh EXTRABRAIN_RELEASE_BASE="file://$RUNNER_TEMP/installer-releases" EXTRABRAIN_VERSION="v$version" EXTRABRAIN_INSTALL_DIR= bash
- name: Verify user installation in new terminals
shell: bash
run: |
set -euo pipefail
version=$(node -p 'require("./package.json").version')
for mode in -lc -ic; do
test "$(env HOME="$EXTRABRAIN_TEST_HOME" ZDOTDIR="$EXTRABRAIN_TEST_HOME" PATH=/usr/bin:/bin:/usr/sbin:/sbin /bin/zsh "$mode" 'extrabrain --version')" = "$version"
done
test "$(stat -f '%Su:%Lp' "$EXTRABRAIN_TEST_HOME/.local/bin/extrabrain")" = "$(id -un):755"
- name: Install optional system-wide command
shell: bash
run: |
set -euo pipefail
version=$(node -p 'require("./package.json").version')
cat install.sh | env EXTRABRAIN_RELEASE_BASE="file://$RUNNER_TEMP/installer-releases" EXTRABRAIN_VERSION="v$version" EXTRABRAIN_INSTALL_DIR=/usr/local/bin bash
- name: Verify installed version and access from another account
shell: bash
run: |
set -euo pipefail
version=$(node -p 'require("./package.json").version')
test "$(PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin extrabrain --version)" = "$version"
test "$(stat -f '%Su:%Sg:%Lp' /usr/local/bin/extrabrain)" = root:wheel:755
test "$(sudo -n -u nobody /usr/bin/env PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin extrabrain --version)" = "$version"
- name: Update system-wide command
shell: bash
run: |
set -euo pipefail
version=$(node -p 'require("./package.json").version')
result=$(EXTRABRAIN_RELEASE_BASE="file://$RUNNER_TEMP/installer-releases" EXTRABRAIN_VERSION="v$version" EXTRABRAIN_INSTALL_DIR= /usr/local/bin/extrabrain --json update)
echo "$result" | jq -e '.code == 0'
- name: Verify version after update
shell: bash
run: |
set -euo pipefail
test "$(PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin extrabrain --version)" = "$(node -p 'require("./package.json").version')"
test "$(stat -f '%Su:%Sg:%Lp' /usr/local/bin/extrabrain)" = root:wheel:755
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,34 @@ Supported release targets are macOS arm64 and x64. The current release is not De
macOS:

```sh
curl -fsSL https://raw.githubusercontent.com/ExtraBrainApp/ExtraBrain-cli/master/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
curl -fsSL https://raw.githubusercontent.com/ExtraBrainApp/ExtraBrain-cli/master/install.sh | bash
```

Open a new terminal window, then check the installed version:

```sh
extrabrain --version
```

The installer needs no administrator privileges. It uses `curl`, `tar`, and `shasum` or `sha256sum`.
The installer puts the standalone executable in `~/.local/bin` for your account. It needs no administrator password, Node, or Python. For zsh and bash, it adds a small PATH block to your shell startup files automatically, so no manual export command is needed. It uses `curl`, `tar`, and `shasum` or `sha256sum`.

For zsh, it updates `.zprofile` and `.zshrc` in `ZDOTDIR` or your home directory. For bash, it updates `.bashrc` and the first existing login file among `.bash_profile`, `.bash_login`, and `.profile`, creating `.bash_profile` if none exists. Existing content is preserved, and reinstalling does not duplicate the block. Set `EXTRABRAIN_NO_MODIFY_PATH=1` in the installer's environment to leave shell configuration untouched. Other shells can use `~/.local/bin/extrabrain` directly or a custom destination already on PATH.

Set `EXTRABRAIN_INSTALL_DIR` in the installer's environment for a custom destination. Custom destinations outside `~/.local/bin` do not change shell profiles. The installer warns if a custom destination is outside PATH or another ExtraBrain executable takes precedence. If automatic shell setup cannot write a profile, it reports the file and the installed executable's full path.

To install for all local accounts, explicitly select `/usr/local/bin`:

```sh
curl -fsSL https://raw.githubusercontent.com/ExtraBrainApp/ExtraBrain-cli/master/install.sh | env EXTRABRAIN_INSTALL_DIR=/usr/local/bin bash
```

This optional mode requests administrator approval when needed and installs a root-owned executable with permissions `755`. Downloads and checksum verification run before elevation; only installation filesystem operations use administrator privileges. Run it in a terminal if authentication is needed.

If you previously installed in `~/.local/bin`, rerun the default installer to configure your shell automatically. If you have another installation, check `command -v extrabrain` in your new terminal and remove an older copy only if it still takes precedence over the intended installation.

Install a particular stable release with `EXTRABRAIN_VERSION=v0.1.1` in the installer environment. The default is the latest stable GitHub release. Release tags use `vMAJOR.MINOR.PATCH`; CLI and app API versions are independent. An installed executable stays at its installed version during document commands. Run `extrabrain update` or rerun the installer to update it explicitly. `extrabrain --version` reports the installed version.
Install a particular stable release with `EXTRABRAIN_VERSION=v0.1.1` in the installer environment. The default is the latest stable GitHub release. Release tags use `vMAJOR.MINOR.PATCH`; CLI and app API versions are independent. An installed executable stays at its installed version during document commands. Run `extrabrain update` or rerun the installer to update it explicitly. `extrabrain update` replaces the running packaged executable in its current directory unless `EXTRABRAIN_INSTALL_DIR` explicitly overrides the destination. Updating a system-wide installation requires administrator approval when needed, just like installation. Check the version separately with `extrabrain --version` after the update completes.

To uninstall, remove only `~/.local/bin/extrabrain`. Removing the command leaves ExtraBrain application data, imported documents, and import resume manifests intact.
To uninstall the default installation, run `rm ~/.local/bin/extrabrain`. You can leave the PATH block in place for other tools in that directory, or remove the block marked `# ExtraBrain CLI: user-local PATH` from the configured shell files. For a system-wide installation, run `sudo rm /usr/local/bin/extrabrain`; for a custom installation, remove only `extrabrain` from that directory. Removing the command leaves ExtraBrain application data, imported documents, and import resume manifests intact.

## App compatibility

Expand Down
123 changes: 118 additions & 5 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
#!/bin/sh
set -eu
umask 022

repository='ExtraBrainApp/ExtraBrain-cli'
release_base=${EXTRABRAIN_RELEASE_BASE:-"https://github.com/$repository/releases"}
install_dir=${EXTRABRAIN_INSTALL_DIR:-"$HOME/.local/bin"}
use_sudo=0
system_install=0
user_install=0
case "$install_dir" in
/usr/local/bin|/usr/local/bin/) system_install=1 ;;
"$HOME/.local/bin"|"$HOME/.local/bin/") user_install=1 ;;
esac

install_command() {
if [ "$use_sudo" -eq 1 ]; then
sudo -n "$@"
else
"$@"
fi
}

case "$(uname -s)" in
Darwin) platform=darwin ;;
Expand All @@ -30,7 +46,7 @@ asset="extrabrain-$version-$platform-$arch.tar.gz"
temporary=$(mktemp -d)
staging=''
cleanup() {
[ -z "$staging" ] || rm -f "$staging"
[ -z "$staging" ] || install_command /bin/rm -f "$staging" || :
rm -rf "$temporary"
}
trap cleanup EXIT HUP INT TERM
Expand All @@ -56,9 +72,106 @@ if [ "$actual" != "$expected" ]; then
fi

tar -xzf "$temporary/$asset" -C "$temporary" extrabrain
mkdir -p "$install_dir"
staging=$(mktemp "$install_dir/.extrabrain.XXXXXXXX")
install -m 755 "$temporary/extrabrain" "$staging"
mv -f "$staging" "$install_dir/extrabrain"
if [ ! -f "$temporary/extrabrain" ] || [ -L "$temporary/extrabrain" ]; then
echo 'Release executable is missing or invalid' >&2
exit 1
fi
if [ -L "$install_dir/extrabrain" ] || { [ -e "$install_dir/extrabrain" ] && [ ! -f "$install_dir/extrabrain" ]; }; then
echo "Refusing to replace a symlink or non-regular file at $install_dir/extrabrain" >&2
exit 1
fi

# System installs are root-owned. Custom writable destinations remain user-owned.
parent=$install_dir
while [ ! -d "$parent" ]; do
parent=$(dirname "$parent")
done
if [ "$user_install" -eq 1 ] && [ ! -w "$parent" ]; then
echo "User installation directory is not writable: $install_dir. Fix its ownership or choose a writable EXTRABRAIN_INSTALL_DIR." >&2
exit 1
fi
if [ "$(id -u)" -ne 0 ] && { [ "$system_install" -eq 1 ] || [ ! -w "$parent" ]; }; then
use_sudo=1
echo "Administrator access is required to install at $install_dir" >&2
if ! command -v sudo >/dev/null 2>&1; then
echo 'Ask an administrator to run the installer, or choose a writable EXTRABRAIN_INSTALL_DIR already on PATH.' >&2
exit 1
fi
if ! sudo -n -v 2>/dev/null; then
if ! ( : </dev/tty ) 2>/dev/null; then
echo 'No terminal is available for administrator authentication. Rerun the installer in a terminal, or ask an administrator to install it.' >&2
exit 1
fi
if ! sudo -v </dev/tty; then
echo 'Administrator authorization failed. The existing executable was not replaced.' >&2
exit 1
fi
fi
fi

install_command /bin/mkdir -p -m 755 "$install_dir"
staging=$(install_command /usr/bin/mktemp "$install_dir/.extrabrain.XXXXXXXX")
if [ "$system_install" -eq 1 ] || [ "$use_sudo" -eq 1 ]; then
install_command /usr/bin/install -o root -g wheel -m 755 "$temporary/extrabrain" "$staging"
else
install_command /usr/bin/install -m 755 "$temporary/extrabrain" "$staging"
fi
install_command /bin/mv -f "$staging" "$install_dir/extrabrain"
staging=''
echo "Installed extrabrain $version at $install_dir/extrabrain"

path_configured=0
if [ "$user_install" -eq 1 ] && [ "${EXTRABRAIN_NO_MODIFY_PATH:-0}" != 1 ]; then
cat > "$temporary/path-setup" <<'EOF'
# ExtraBrain CLI: user-local PATH
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) export PATH="$HOME/.local/bin:$PATH" ;;
esac
EOF
configure_profile() {
profile_file=$1
if [ -f "$profile_file" ] && grep -Fqx '# ExtraBrain CLI: user-local PATH' "$profile_file"; then
return 0
fi
if mkdir -p "$(dirname "$profile_file")" && { printf '\n'; cat "$temporary/path-setup"; } >> "$profile_file"; then
echo "Configured PATH in $profile_file"
return 0
fi
echo "Warning: could not configure PATH in $profile_file. The executable is installed at $install_dir/extrabrain." >&2
return 1
}
path_configured=1
case "${SHELL:-/bin/zsh}" in
*/zsh)
configure_profile "${ZDOTDIR:-$HOME}/.zprofile" || path_configured=0
configure_profile "${ZDOTDIR:-$HOME}/.zshrc" || path_configured=0
;;
*/bash)
login_profile="$HOME/.bash_profile"
if [ -e "$HOME/.bash_profile" ] || [ -L "$HOME/.bash_profile" ]; then
login_profile="$HOME/.bash_profile"
elif [ -e "$HOME/.bash_login" ] || [ -L "$HOME/.bash_login" ]; then
login_profile="$HOME/.bash_login"
elif [ -e "$HOME/.profile" ] || [ -L "$HOME/.profile" ]; then
login_profile="$HOME/.profile"
fi
configure_profile "$login_profile" || path_configured=0
configure_profile "$HOME/.bashrc" || path_configured=0
;;
*)
path_configured=0
echo "Warning: automatic PATH setup supports zsh and bash. Use $install_dir/extrabrain directly with your shell." >&2
;;
esac
fi
if [ "$path_configured" -eq 1 ]; then
echo 'Open a new terminal, then run: extrabrain --version'
fi

resolved=$(command -v extrabrain || :)
if [ -z "$resolved" ] && [ "$path_configured" -eq 0 ]; then
echo "Warning: $install_dir is not on this terminal's PATH. Use $install_dir/extrabrain directly, or install into a directory already on PATH." >&2
elif [ -n "$resolved" ] && [ ! "$resolved" -ef "$install_dir/extrabrain" ]; then
echo "Warning: PATH currently resolves extrabrain to $resolved. Check command -v extrabrain in a new terminal; remove or update an older copy if it still takes precedence." >&2
fi
12 changes: 11 additions & 1 deletion src/update.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { spawn, spawnSync } from 'node:child_process'
import { dirname } from 'node:path'
import { isSea } from 'node:sea'
import installShell from '../install.sh?raw'
import installPowerShell from '../install.ps1?raw'
import { CliApiError } from './apiClient.service'
Expand All @@ -21,10 +23,18 @@ export const updateCli = (): { scheduled: boolean } => {
const result = spawnSync('sh', ['-s'], {
encoding: 'utf8',
input: installShell,
env: {
...process.env,
...(isSea() && !process.env.EXTRABRAIN_INSTALL_DIR
? { EXTRABRAIN_INSTALL_DIR: dirname(process.execPath) }
: {})
},
timeout: 120_000
})
if (result.error || result.status !== 0) {
throw new CliApiError('UPDATE_FAILED', 'Verified CLI update failed')
const detail = result.stderr?.trim() || result.error?.message || `Installer exited with status ${result.status}`
throw new CliApiError('UPDATE_FAILED', `Verified CLI update failed: ${detail}`)
}
if (result.stderr) process.stderr.write(result.stderr)
return { scheduled: false }
}
Loading
Loading